getPoints() {
64 | return mPoints;
65 | }
66 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/java/io/codetail/circualrevealsample/PathPoint.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2012 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package io.codetail.circualrevealsample;
17 |
18 | /**
19 | * A class that holds information about a location and how the path should get to that
20 | * location from the previous path location (if any). Any PathPoint holds the information for
21 | * its location as well as the instructions on how to traverse the preceding interval from the
22 | * previous location.
23 | */
24 | public class PathPoint {
25 |
26 | /**
27 | * The possible path operations that describe how to move from a preceding PathPoint to the
28 | * location described by this PathPoint.
29 | */
30 | public static final int MOVE = 0;
31 | public static final int LINE = 1;
32 | public static final int CURVE = 2;
33 |
34 | /**
35 | * The location of this PathPoint
36 | */
37 | float mX, mY;
38 |
39 | /**
40 | * The first control point, if any, for a PathPoint of type CURVE
41 | */
42 | float mControl0X, mControl0Y;
43 |
44 | /**
45 | * The second control point, if any, for a PathPoint of type CURVE
46 | */
47 | float mControl1X, mControl1Y;
48 |
49 | /**
50 | * The motion described by the path to get from the previous PathPoint in an AnimatorPath
51 | * to the location of this PathPoint. This can be one of MOVE, LINE, or CURVE.
52 | */
53 | int mOperation;
54 |
55 | /**
56 | * Line/Move constructor
57 | */
58 | private PathPoint(int operation, float x, float y) {
59 | mOperation = operation;
60 | mX = x;
61 | mY = y;
62 | }
63 |
64 | /**
65 | * Curve constructor
66 | */
67 | private PathPoint(float c0X, float c0Y, float c1X, float c1Y, float x, float y) {
68 | mControl0X = c0X;
69 | mControl0Y = c0Y;
70 | mControl1X = c1X;
71 | mControl1Y = c1Y;
72 | mX = x;
73 | mY = y;
74 | mOperation = CURVE;
75 | }
76 |
77 | /**
78 | * Constructs and returns a PathPoint object that describes a line to the given xy location.
79 | */
80 | public static PathPoint lineTo(float x, float y) {
81 | return new PathPoint(LINE, x, y);
82 | }
83 |
84 | /**
85 | * Constructs and returns a PathPoint object that describes a cubic B�zier curve to the
86 | * given xy location with the control points at c0 and c1.
87 | */
88 | public static PathPoint curveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y) {
89 | return new PathPoint(c0X, c0Y, c1X, c1Y, x, y);
90 | }
91 |
92 | /**
93 | * Constructs and returns a PathPoint object that describes a discontinuous move to the given
94 | * xy location.
95 | */
96 | public static PathPoint moveTo(float x, float y) {
97 | return new PathPoint(MOVE, x, y);
98 | }
99 | }
--------------------------------------------------------------------------------
/circualreveal/src/main/java/io/codetail/animation/ViewAnimationUtils.java:
--------------------------------------------------------------------------------
1 | package io.codetail.animation;
2 |
3 | import android.animation.Animator;
4 | import android.view.View;
5 | import io.codetail.animation.ViewRevealManager.ChangeViewLayerTypeAdapter;
6 | import io.codetail.view.BuildConfig;
7 |
8 | import static android.os.Build.VERSION.SDK_INT;
9 | import static android.os.Build.VERSION_CODES.LOLLIPOP;
10 | import static io.codetail.animation.ViewRevealManager.RevealValues;
11 |
12 | public final class ViewAnimationUtils {
13 | private final static boolean DEBUG = BuildConfig.DEBUG;
14 |
15 | private final static boolean LOLLIPOP_PLUS = SDK_INT >= LOLLIPOP;
16 |
17 | /**
18 | * Returns an Animator which can animate a clipping circle.
19 | *
20 | * Any shadow cast by the View will respect the circular clip from this animator.
21 | *
22 | * Only a single non-rectangular clip can be applied on a View at any time.
23 | * Views clipped by a circular reveal animation take priority over
24 | * {@link android.view.View#setClipToOutline(boolean) View Outline clipping}.
25 | *
26 | * Note that the animation returned here is a one-shot animation. It cannot
27 | * be re-used, and once started it cannot be paused or resumed.
28 | *
29 | * @param view The View will be clipped to the clip circle.
30 | * @param centerX The x coordinate of the center of the clip circle.
31 | * @param centerY The y coordinate of the center of the clip circle.
32 | * @param startRadius The starting radius of the clip circle.
33 | * @param endRadius The ending radius of the clip circle.
34 | */
35 | public static Animator createCircularReveal(View view, int centerX, int centerY,
36 | float startRadius, float endRadius) {
37 |
38 | return createCircularReveal(view, centerX, centerY, startRadius, endRadius,
39 | View.LAYER_TYPE_SOFTWARE);
40 | }
41 |
42 | /**
43 | * Returns an Animator which can animate a clipping circle.
44 | *
45 | * Any shadow cast by the View will respect the circular clip from this animator.
46 | *
47 | * Only a single non-rectangular clip can be applied on a View at any time.
48 | * Views clipped by a circular reveal animation take priority over
49 | * {@link android.view.View#setClipToOutline(boolean) View Outline clipping}.
50 | *
51 | * Note that the animation returned here is a one-shot animation. It cannot
52 | * be re-used, and once started it cannot be paused or resumed.
53 | *
54 | * @param view The View will be clipped to the clip circle.
55 | * @param centerX The x coordinate of the center of the clip circle.
56 | * @param centerY The y coordinate of the center of the clip circle.
57 | * @param startRadius The starting radius of the clip circle.
58 | * @param endRadius The ending radius of the clip circle.
59 | * @param layerType View layer type {@link View#LAYER_TYPE_HARDWARE} or {@link
60 | * View#LAYER_TYPE_SOFTWARE}
61 | */
62 | public static Animator createCircularReveal(View view, int centerX, int centerY,
63 | float startRadius, float endRadius, int layerType) {
64 |
65 | if (!(view.getParent() instanceof RevealViewGroup)) {
66 | throw new IllegalArgumentException("Parent must be instance of RevealViewGroup");
67 | }
68 |
69 | final RevealViewGroup viewGroup = (RevealViewGroup) view.getParent();
70 | final ViewRevealManager rm = viewGroup.getViewRevealManager();
71 |
72 | if (!rm.overrideNativeAnimator() && LOLLIPOP_PLUS) {
73 | return android.view.ViewAnimationUtils.createCircularReveal(view, centerX, centerY,
74 | startRadius, endRadius);
75 | }
76 |
77 | final RevealValues viewData = new RevealValues(view, centerX, centerY, startRadius, endRadius);
78 | final Animator animator = rm.dispatchCreateAnimator(viewData);
79 |
80 | if (layerType != view.getLayerType()) {
81 | animator.addListener(new ChangeViewLayerTypeAdapter(viewData, layerType));
82 | }
83 |
84 | return animator;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/circualreveal/gradle_mvn_push.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Chris Banes
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | apply plugin: 'maven'
18 | apply plugin: 'signing'
19 |
20 | def isReleaseBuild() {
21 | return VERSION_NAME.contains("SNAPSHOT") == false
22 | }
23 |
24 | def getReleaseRepositoryUrl() {
25 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
26 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
27 | }
28 |
29 | def getSnapshotRepositoryUrl() {
30 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
31 | : "https://oss.sonatype.org/content/repositories/snapshots/"
32 | }
33 |
34 | def getRepositoryUsername() {
35 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : ""
36 | }
37 |
38 | def getRepositoryPassword() {
39 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : ""
40 | }
41 |
42 | afterEvaluate { project ->
43 | uploadArchives {
44 | repositories {
45 | mavenDeployer {
46 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
47 |
48 | pom.groupId = GROUP
49 | pom.artifactId = POM_ARTIFACT_ID
50 | pom.version = VERSION_NAME
51 |
52 | repository(url: getReleaseRepositoryUrl()) {
53 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
54 | }
55 | snapshotRepository(url: getSnapshotRepositoryUrl()) {
56 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
57 | }
58 |
59 | pom.project {
60 | name POM_NAME
61 | packaging POM_PACKAGING
62 | description POM_DESCRIPTION
63 | url POM_URL
64 |
65 | scm {
66 | url POM_SCM_URL
67 | connection POM_SCM_CONNECTION
68 | developerConnection POM_SCM_DEV_CONNECTION
69 | }
70 |
71 | licenses {
72 | license {
73 | name POM_LICENCE_NAME
74 | url POM_LICENCE_URL
75 | distribution POM_LICENCE_DIST
76 | }
77 | }
78 |
79 | developers {
80 | developer {
81 | id POM_DEVELOPER_ID
82 | name POM_DEVELOPER_NAME
83 | }
84 | }
85 | }
86 | }
87 | }
88 | }
89 |
90 | signing {
91 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
92 | sign configurations.archives
93 | }
94 |
95 | task androidJavadocs(type: Javadoc) {
96 | source = android.sourceSets.main.java.srcDirs
97 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
98 | }
99 |
100 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
101 | classifier = 'javadoc'
102 | from androidJavadocs.destinationDir
103 | }
104 |
105 | task androidSourcesJar(type: Jar) {
106 | classifier = 'sources'
107 | from android.sourceSets.main.java.sourceFiles
108 | }
109 |
110 | artifacts {
111 | archives androidSourcesJar
112 | archives androidJavadocsJar
113 | }
114 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.jitpack.io/#Ozodrukh/CircularReveal)
2 |
3 | CircularReveal
4 | ==============
5 |
6 | Lollipop ViewAnimationUtils.createCircularReveal for everyone 14+
7 |
8 | Yotube Video
11 |
12 | #### [Checout demo application ](https://github.com/ozodrukh/CircularReveal/releases)
13 |
14 |
15 | How to use:
16 | ======
17 |
18 | Use regular `RevealFrameLayout` & `RevealLinearLayout` don't worry, only target will be clipped :)
19 |
20 | ```xml
21 |
25 |
26 |
27 |
28 |
43 |
44 |
45 | ```
46 |
47 | ```java
48 |
49 | View myView = findView(R.id.awesome_card);
50 |
51 | // get the center for the clipping circle
52 | int cx = (myView.getLeft() + myView.getRight()) / 2;
53 | int cy = (myView.getTop() + myView.getBottom()) / 2;
54 |
55 | // get the final radius for the clipping circle
56 | int dx = Math.max(cx, myView.getWidth() - cx);
57 | int dy = Math.max(cy, myView.getHeight() - cy);
58 | float finalRadius = (float) Math.hypot(dx, dy);
59 |
60 | // Android native animator
61 | Animator animator =
62 | ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0, finalRadius);
63 | animator.setInterpolator(new AccelerateDecelerateInterpolator());
64 | animator.setDuration(1500);
65 | animator.start();
66 |
67 | ```
68 |
69 | How to add dependency
70 | =====================
71 |
72 | This library is not released in Maven Central, but instead you can use [JitPack](https://www.jitpack.io/#ozodrukh/CircularReveal)
73 |
74 | add remote maven url
75 |
76 | ```groovy
77 | repositories {
78 | maven {
79 | url "https://jitpack.io"
80 | }
81 | }
82 | ```
83 |
84 | then add a library dependency
85 |
86 | ```groovy
87 | dependencies {
88 | implementation ('com.github.ozodrukh:CircularReveal:2.0.1@aar') {
89 | transitive = true;
90 | }
91 | }
92 | ```
93 |
94 |
95 | License
96 | --------
97 |
98 | The MIT License (MIT)
99 |
100 | Copyright (c) 2016 Abdullaev Ozodrukh
101 |
102 | Permission is hereby granted, free of charge, to any person obtaining a copy
103 | of this software and associated documentation files (the "Software"), to deal
104 | in the Software without restriction, including without limitation the rights
105 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
106 | copies of the Software, and to permit persons to whom the Software is
107 | furnished to do so, subject to the following conditions:
108 |
109 | The above copyright notice and this permission notice shall be included in
110 | all copies or substantial portions of the Software.
111 |
112 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
113 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
114 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
115 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
116 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
117 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
118 | THE SOFTWARE.
119 |
--------------------------------------------------------------------------------
/spring-revealmanager/src/main/java/io/codetail/animation/SpringViewAnimatorManager.java:
--------------------------------------------------------------------------------
1 | package io.codetail.animation;
2 |
3 | import android.animation.Animator;
4 | import android.view.View;
5 |
6 | /**
7 | * created at 3/15/17
8 | *
9 | * @author Ozodrukh
10 | * @version 1.0
11 | */
12 | public class SpringViewAnimatorManager extends ViewRevealManager {
13 |
14 | private final static DynamicAnimation.Property RADIUS_PROPERTY =
15 | new DynamicAnimation.Property("radius") {
16 | @Override public void setValue(RevealValues view, float value) {
17 | view.radius = value;
18 | view.target.invalidate();
19 | }
20 |
21 | @Override public float getValue(RevealValues view) {
22 | return view.radius;
23 | }
24 | };
25 |
26 | private SpringForce force = new SpringForce();
27 |
28 | public SpringViewAnimatorManager() {
29 | super(new PathTransformation());
30 | }
31 |
32 | /**
33 | * In order to enable spring animation on devices running Lollipop & higher we override default
34 | * {@link ViewAnimationUtils#createCircularReveal(View, int, int, float, float)} running way
35 | */
36 | @Override protected boolean overrideNativeAnimator() {
37 | return true;
38 | }
39 |
40 | /**
41 | * Sets the stiffness of a spring. The more stiff a spring is, the more force it applies to
42 | * the object attached when the spring is not at the final position. Default stiffness is
43 | * {@link SpringForce#STIFFNESS_MEDIUM}.
44 | *
45 | * @param stiffness non-negative stiffness constant of a spring
46 | * @return the spring force that the given stiffness is set on
47 | * @throws IllegalArgumentException if the given spring stiffness is negative.
48 | */
49 | public void setStiffness(float stiffness) {
50 | force.setStiffness(stiffness);
51 | }
52 |
53 | /**
54 | * Gets the stiffness of the spring.
55 | *
56 | * @return the stiffness of the spring
57 | */
58 | public float getStiffness() {
59 | return force.getStiffness();
60 | }
61 |
62 | /**
63 | * Spring damping ratio describes how oscillations in a system decay after a disturbance.
64 | *
65 | * When damping ratio > 1 (over-damped), the object will quickly return to the rest position
66 | * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
67 | * return to equilibrium within the shortest amount of time. When damping ratio is less than 1
68 | * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without
69 | * any damping (i.e. damping ratio = 0), the mass will oscillate forever.
70 | *
71 | * Default damping ratio is {@link SpringForce#DAMPING_RATIO_MEDIUM_BOUNCY}.
72 | *
73 | * @param dampingRatio damping ratio of the spring, it should be non-negative
74 | * @return the spring force that the given damping ratio is set on
75 | * @throws IllegalArgumentException if the {@param dampingRatio} is negative.
76 | */
77 | public void setDampingRatio(float dampingRatio) {
78 | force.setDampingRatio(dampingRatio);
79 | }
80 |
81 | /**
82 | * Returns the damping ratio of the spring.
83 | *
84 | * @return damping ratio of the spring
85 | */
86 | public float getDampingRatio() {
87 | return force.getDampingRatio();
88 | }
89 |
90 | /**
91 | * This threshold defines how close the animation value needs to be before the animation can
92 | * finish. This default value is based on the property being animated, e.g. animations on alpha,
93 | * scale, translation or rotation would have different thresholds. This value should be small
94 | * enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that
95 | * animations take seconds to finish.
96 | *
97 | * @param threshold the difference between the animation value and final spring position that is
98 | * allowed to end the animation when velocity is very low
99 | */
100 | public void setDefaultThreshold(double threshold) {
101 | force.setDefaultThreshold(threshold);
102 | }
103 |
104 | @Override protected Animator createAnimator(final RevealValues data) {
105 | force.setFinalPosition(data.endRadius);
106 |
107 | final SpringAnimation animation = new SpringAnimation(data, RADIUS_PROPERTY);
108 | animation.setStartValue(data.startRadius);
109 | animation.setSpring(force);
110 |
111 | final DynamicAnimator animator = new DynamicAnimator<>(animation);
112 | animator.addListener(getAnimatorCallback());
113 | return animator;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
17 |
18 |
27 |
28 |
39 |
40 |
51 |
52 |
60 |
61 |
62 |
63 |
72 |
73 |
82 |
83 |
92 |
93 |
102 |
103 |
112 |
113 |
114 |
115 |
116 |
117 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/app/src/main/java/io/codetail/circualrevealsample/SpringSettingsBottomDialog.java:
--------------------------------------------------------------------------------
1 | package io.codetail.circualrevealsample;
2 |
3 | import android.content.Context;
4 | import android.support.animation.SpringForce;
5 | import android.support.v7.widget.AppCompatTextView;
6 | import android.support.v7.widget.SwitchCompat;
7 | import android.util.AttributeSet;
8 | import android.widget.CompoundButton;
9 | import android.widget.LinearLayout;
10 | import android.widget.SeekBar;
11 | import android.widget.TextView;
12 | import io.codetail.animation.RevealViewGroup;
13 | import io.codetail.animation.SpringViewAnimatorManager;
14 |
15 | import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
16 | import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
17 |
18 | /**
19 | * created at 3/16/17
20 | *
21 | * @author Ozodrukh
22 | * @version 1.0
23 | */
24 | public class SpringSettingsBottomDialog extends LinearLayout {
25 | private boolean springManagerAdded = false;
26 | private boolean switchAdded = false;
27 |
28 | private SeekBar stiffnessView;
29 | private SeekBar dampingView;
30 |
31 | public SpringSettingsBottomDialog(Context context, AttributeSet attrs) {
32 | super(context, attrs);
33 | final int padding = dp(16);
34 |
35 | setOrientation(VERTICAL);
36 | setPadding(padding, padding, padding, padding);
37 | }
38 |
39 | public void setAnimatorManager(final SpringViewAnimatorManager animatorManager) {
40 | if (springManagerAdded) {
41 | // already inflated progress bars
42 | return;
43 | }
44 |
45 | springManagerAdded = true;
46 |
47 | final int stiffnessVal =
48 | (int) ((animatorManager.getStiffness() / SpringForce.STIFFNESS_HIGH) * 100f);
49 |
50 | final int dampingVal =
51 | (int) (animatorManager.getDampingRatio() / SpringForce.DAMPING_RATIO_NO_BOUNCY * 100f);
52 |
53 | stiffnessView =
54 | createConfigurationView("Stiffness", stiffnessVal, new OnProgressChangeListener() {
55 | @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
56 | animatorManager.setStiffness(3000 * (progress / 100f));
57 | }
58 | });
59 |
60 | dampingView =
61 | createConfigurationView("Damping Ratio", dampingVal, new OnProgressChangeListener() {
62 | @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
63 | animatorManager.setDampingRatio(progress / 100f);
64 | }
65 | });
66 | }
67 |
68 | public void addSwitch(String label, boolean defaultState,
69 | final CompoundButton.OnCheckedChangeListener listener) {
70 | if (switchAdded) {
71 | return;
72 | }
73 |
74 | switchAdded = true;
75 |
76 | final SwitchCompat switchView = new SwitchCompat(getContext());
77 | switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
78 | @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
79 | listener.onCheckedChanged(buttonView, isChecked);
80 |
81 | if (springManagerAdded) {
82 | stiffnessView.setEnabled(isChecked);
83 | dampingView.setEnabled(isChecked);
84 | }
85 | }
86 | });
87 | switchView.setChecked(defaultState);
88 | switchView.setText(label);
89 |
90 | addView(switchView, createMarginLayoutParams(MATCH_PARENT, WRAP_CONTENT, 0, 0, 0, dp(16)));
91 |
92 | if (springManagerAdded) {
93 | stiffnessView.setEnabled(defaultState);
94 | dampingView.setEnabled(defaultState);
95 | }
96 | }
97 |
98 | private SeekBar createConfigurationView(CharSequence label, int defaultVal,
99 | SeekBar.OnSeekBarChangeListener changeListener) {
100 |
101 | final TextView labelView =
102 | new AppCompatTextView(getContext(), null, R.style.TextAppearance_AppCompat_Caption);
103 | labelView.setText(label);
104 | labelView.setLayoutParams(createMarginLayoutParams(MATCH_PARENT, WRAP_CONTENT, 0, 0, 0, dp(8)));
105 |
106 | final SeekBar seekBar = new SeekBar(getContext());
107 | seekBar.setProgress(defaultVal);
108 | seekBar.setMax(100);
109 | seekBar.setOnSeekBarChangeListener(changeListener);
110 |
111 | seekBar.setLayoutParams(createMarginLayoutParams(MATCH_PARENT, WRAP_CONTENT, 0, 0, 0, dp(16)));
112 |
113 | addView(labelView);
114 | addView(seekBar);
115 | return seekBar;
116 | }
117 |
118 | private int dp(float px) {
119 | return (int) (getContext().getResources().getDisplayMetrics().density * px);
120 | }
121 |
122 | private static MarginLayoutParams createMarginLayoutParams(int w, int h, int l, int t, int r,
123 | int b) {
124 | MarginLayoutParams lp = new MarginLayoutParams(w, h);
125 | lp.leftMargin = l;
126 | lp.topMargin = t;
127 | lp.rightMargin = r;
128 | lp.bottomMargin = b;
129 | return lp;
130 | }
131 |
132 | private static abstract class OnProgressChangeListener
133 | implements SeekBar.OnSeekBarChangeListener {
134 |
135 | @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
136 |
137 | }
138 |
139 | @Override public void onStartTrackingTouch(SeekBar seekBar) {
140 |
141 | }
142 |
143 | @Override public void onStopTrackingTouch(SeekBar seekBar) {
144 |
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
45 |
46 |
47 |
49 |
50 |
52 |
53 |
54 |
55 |
56 |
57 |
59 |
60 |
62 |
63 |
65 |
66 |
68 |
69 |
70 |
71 |
73 |
74 |
75 |
76 |
77 |
78 | 8dp
79 | 24dp
80 | 56dp
81 |
82 |
83 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/app/src/main/java/io/codetail/circualrevealsample/RadialTransformationActivity.java:
--------------------------------------------------------------------------------
1 | package io.codetail.circualrevealsample;
2 |
3 | import android.animation.Animator;
4 | import android.graphics.Point;
5 | import android.media.MediaPlayer;
6 | import android.net.Uri;
7 | import android.os.Bundle;
8 | import android.support.animation.SpringForce;
9 | import android.support.annotation.Nullable;
10 | import android.support.design.widget.BottomSheetBehavior;
11 | import android.support.v4.view.animation.FastOutLinearInInterpolator;
12 | import android.support.v7.app.AppCompatActivity;
13 | import android.view.GestureDetector;
14 | import android.view.MotionEvent;
15 | import android.view.View;
16 | import android.view.ViewGroup;
17 | import android.view.ViewTreeObserver;
18 | import android.widget.CompoundButton;
19 | import android.widget.ImageView;
20 | import android.widget.VideoView;
21 | import butterknife.BindView;
22 | import butterknife.ButterKnife;
23 | import com.squareup.picasso.Picasso;
24 | import io.codetail.animation.SpringViewAnimatorManager;
25 | import io.codetail.animation.ViewAnimationUtils;
26 | import io.codetail.animation.ViewRevealManager;
27 | import io.codetail.widget.RevealFrameLayout;
28 |
29 | /**
30 | * https://www.google.com/design/spec/motion/choreography.html#choreography-radial-reaction
31 | */
32 | @SuppressWarnings("ConstantConditions") public class RadialTransformationActivity
33 | extends AppCompatActivity {
34 |
35 | private final static String VIDEO_URL =
36 | "https://material-design.storage.googleapis.com/publish/material_v_8/material_ext_publish/0B14F_FSUCc01WUt2SFZkbGVuNVk/RR_Point_of_Contact_001.mp4";
37 |
38 | @BindView(R.id.view_stack) RevealFrameLayout stack;
39 | @BindView(R.id.san_francisco) ImageView sanFranciscoView;
40 | @BindView(R.id.video) VideoView videoView;
41 | @BindView(R.id.springSettings) SpringSettingsBottomDialog settingsView;
42 |
43 | private int currentViewIndex = 0;
44 |
45 | @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
46 | super.onCreate(savedInstanceState);
47 | setContentView(R.layout.activity_sample_2);
48 | ButterKnife.bind(this);
49 |
50 | Picasso.with(this)
51 | .load("http://camp-campbell.com/wp-content/uploads/2014/09/847187872-san-francisco.jpg")
52 | .resizeDimen(R.dimen.radial_card_width, R.dimen.radial_card_height)
53 | .centerCrop()
54 | .into(sanFranciscoView);
55 |
56 | videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
57 | @Override public void onPrepared(MediaPlayer mp) {
58 | mp.setLooping(true);
59 | }
60 | });
61 | videoView.setVideoURI(Uri.parse(VIDEO_URL));
62 | videoView.start();
63 |
64 | final GestureDetector detector = new GestureDetector(this, tapDetector);
65 |
66 | for (int i = 0; i < stack.getChildCount(); i++) {
67 | View view = stack.getChildAt(i);
68 | view.setOnTouchListener(new View.OnTouchListener() {
69 | @Override public boolean onTouch(View v, MotionEvent event) {
70 | return detector.onTouchEvent(event);
71 | }
72 | });
73 | }
74 |
75 | final ViewRevealManager revealManager = new ViewRevealManager();
76 | final SpringViewAnimatorManager springManager = new SpringViewAnimatorManager();
77 | springManager.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
78 | springManager.setStiffness(SpringForce.STIFFNESS_LOW);
79 |
80 | stack.setViewRevealManager(revealManager);
81 |
82 | settingsView.addSwitch("Enable Spring", false, new CompoundButton.OnCheckedChangeListener() {
83 | @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
84 | stack.setViewRevealManager(isChecked ? springManager : revealManager);
85 | }
86 | });
87 | settingsView.setAnimatorManager(springManager);
88 |
89 | final BottomSheetBehavior behavior = BottomSheetBehavior.from(settingsView);
90 | behavior.setPeekHeight(getResources().getDimensionPixelSize(R.dimen.bottom_peek_height));
91 | behavior.setSkipCollapsed(false);
92 | behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
93 | }
94 |
95 | private GestureDetector.OnGestureListener tapDetector =
96 | new GestureDetector.SimpleOnGestureListener() {
97 | @Override public boolean onDown(MotionEvent e) {
98 | return true;
99 | }
100 |
101 | @Override public boolean onSingleTapUp(MotionEvent e) {
102 | View nextView = getNext();
103 | nextView.bringToFront();
104 | nextView.setVisibility(View.VISIBLE);
105 |
106 | final float finalRadius =
107 | (float) Math.hypot(nextView.getWidth() / 2f, nextView.getHeight() / 2f) + hypo(
108 | nextView, e);
109 |
110 | Animator revealAnimator =
111 | ViewAnimationUtils.createCircularReveal(nextView, (int) e.getX(), (int) e.getY(), 0,
112 | finalRadius, View.LAYER_TYPE_HARDWARE);
113 |
114 | revealAnimator.setDuration(MainActivity.SLOW_DURATION);
115 | revealAnimator.setInterpolator(new FastOutLinearInInterpolator());
116 | revealAnimator.start();
117 |
118 | return true;
119 | }
120 | };
121 |
122 | private float hypo(View view, MotionEvent event) {
123 | Point p1 = new Point((int) event.getX(), (int) event.getY());
124 | Point p2 = new Point(view.getWidth() / 2, view.getHeight() / 2);
125 |
126 | return (float) Math.sqrt(Math.pow(p1.y - p2.y, 2) + Math.pow(p1.x - p2.x, 2));
127 | }
128 |
129 | private View getCurrentView() {
130 | return stack.getChildAt(currentViewIndex);
131 | }
132 |
133 | private View getNext() {
134 | return getViewAt(++currentViewIndex);
135 | }
136 |
137 | private View getViewAt(int index) {
138 | if (index >= stack.getChildCount()) {
139 | index = 0;
140 | } else if (index < 0) {
141 | index = stack.getChildCount() - 1;
142 | }
143 | return stack.getChildAt(index);
144 | }
145 |
146 | @Override protected void onDestroy() {
147 | super.onDestroy();
148 | videoView.suspend();
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/spring-revealmanager/src/main/java/io/codetail/animation/AnimationHandler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.codetail.animation;
18 |
19 | import android.os.SystemClock;
20 | import android.support.v4.util.SimpleArrayMap;
21 | import android.view.Choreographer;
22 |
23 | import java.util.ArrayList;
24 |
25 | /**
26 | * This custom, static handler handles the timing pulse that is shared by all active
27 | * ValueAnimators. This approach ensures that the setting of animation values will happen on the
28 | * same thread that animations start on, and that all animations will share the same times for
29 | * calculating their values, which makes synchronizing animations possible.
30 | *
31 | * The handler uses the Choreographer by default for doing periodic callbacks. A custom
32 | * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
33 | * may be independent of UI frame update. This could be useful in testing.
34 | *
35 | * @hide
36 | */
37 | class AnimationHandler {
38 | /**
39 | * Callbacks that receives notifications for animation timing
40 | */
41 | interface AnimationFrameCallback {
42 | /**
43 | * Run animation based on the frame time.
44 | *
45 | * @param frameTime The frame start time
46 | */
47 | boolean doAnimationFrame(long frameTime);
48 | }
49 |
50 | /**
51 | * Internal per-thread collections used to avoid set collisions as animations start and end
52 | * while being processed.
53 | *
54 | * @hide
55 | */
56 | private final SimpleArrayMap mDelayedCallbackStartTime =
57 | new SimpleArrayMap<>();
58 | public static final ThreadLocal sAnimatorHandler = new ThreadLocal<>();
59 | private final ArrayList mAnimationCallbacks = new ArrayList<>();
60 | private AnimationFrameCallbackProvider mProvider;
61 |
62 | private long mCurrentFrameTime = 0;
63 | private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
64 | @Override
65 | public void doFrame(long frameTimeNanos) {
66 | mCurrentFrameTime = System.currentTimeMillis();
67 | doAnimationFrame(mCurrentFrameTime);
68 | if (mAnimationCallbacks.size() > 0) {
69 | getProvider().postFrameCallback(this);
70 | }
71 | }
72 | };
73 |
74 | private boolean mListDirty = false;
75 |
76 | public static AnimationHandler getInstance() {
77 | if (sAnimatorHandler.get() == null) {
78 | sAnimatorHandler.set(new AnimationHandler());
79 | }
80 | return sAnimatorHandler.get();
81 | }
82 |
83 | public static long getFrameTime() {
84 | if (sAnimatorHandler.get() == null) {
85 | return 0;
86 | }
87 | return sAnimatorHandler.get().mCurrentFrameTime;
88 | }
89 |
90 | /**
91 | * By default, the Choreographer is used to provide timing for frame callbacks. A custom
92 | * provider can be used here to provide different timing pulse.
93 | */
94 | public void setProvider(AnimationFrameCallbackProvider provider) {
95 | if (provider == null) {
96 | mProvider = new MyFrameCallbackProvider();
97 | } else {
98 | mProvider = provider;
99 | }
100 | }
101 |
102 | private AnimationFrameCallbackProvider getProvider() {
103 | if (mProvider == null) {
104 | mProvider = new MyFrameCallbackProvider();
105 | }
106 | return mProvider;
107 | }
108 |
109 | /**
110 | * Register to get a callback on the next frame after the delay.
111 | */
112 | public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
113 | if (mAnimationCallbacks.size() == 0) {
114 | getProvider().postFrameCallback(mFrameCallback);
115 | }
116 | if (!mAnimationCallbacks.contains(callback)) {
117 | mAnimationCallbacks.add(callback);
118 | }
119 |
120 | if (delay > 0) {
121 | mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
122 | }
123 | }
124 |
125 | /**
126 | * Removes the given callback from the list, so it will no longer be called for frame related
127 | * timing.
128 | */
129 | public void removeCallback(AnimationFrameCallback callback) {
130 | mDelayedCallbackStartTime.remove(callback);
131 | int id = mAnimationCallbacks.indexOf(callback);
132 | if (id >= 0) {
133 | mAnimationCallbacks.set(id, null);
134 | mListDirty = true;
135 | }
136 | }
137 |
138 | private void doAnimationFrame(long frameTime) {
139 | long currentTime = SystemClock.uptimeMillis();
140 | for (int i = 0; i < mAnimationCallbacks.size(); i++) {
141 | final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
142 | if (callback == null) {
143 | continue;
144 | }
145 | if (isCallbackDue(callback, currentTime)) {
146 | callback.doAnimationFrame(frameTime);
147 | }
148 | }
149 | cleanUpList();
150 | }
151 |
152 | /**
153 | * Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay
154 | * so that they can start getting frame callbacks.
155 | *
156 | * @return true if they have passed the initial delay or have no delay, false otherwise.
157 | */
158 | private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) {
159 | Long startTime = mDelayedCallbackStartTime.get(callback);
160 | if (startTime == null) {
161 | return true;
162 | }
163 | if (startTime < currentTime) {
164 | mDelayedCallbackStartTime.remove(callback);
165 | return true;
166 | }
167 | return false;
168 | }
169 |
170 | private void cleanUpList() {
171 | if (mListDirty) {
172 | for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
173 | if (mAnimationCallbacks.get(i) == null) {
174 | mAnimationCallbacks.remove(i);
175 | }
176 | }
177 | mListDirty = false;
178 | }
179 | }
180 |
181 | private int getCallbackSize() {
182 | int count = 0;
183 | int size = mAnimationCallbacks.size();
184 | for (int i = size - 1; i >= 0; i--) {
185 | if (mAnimationCallbacks.get(i) != null) {
186 | count++;
187 | }
188 | }
189 | return count;
190 | }
191 |
192 | /**
193 | * Default provider of timing pulse that uses Choreographer for frame callbacks.
194 | */
195 | private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
196 |
197 | final Choreographer mChoreographer = Choreographer.getInstance();
198 |
199 | @Override
200 | public void postFrameCallback(Choreographer.FrameCallback callback) {
201 | mChoreographer.postFrameCallback(callback);
202 | }
203 | }
204 |
205 | /**
206 | * The intention for having this interface is to increase the testability of ValueAnimator.
207 | * Specifically, we can have a custom implementation of the interface below and provide
208 | * timing pulse without using Choreographer. That way we could use any arbitrary interval for
209 | * our timing pulse in the tests.
210 | *
211 | * @hide
212 | */
213 | public interface AnimationFrameCallbackProvider {
214 | void postFrameCallback(Choreographer.FrameCallback callback);
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/spring-revealmanager/src/main/java/io/codetail/animation/SpringAnimation.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.codetail.animation;
18 |
19 | import android.os.Looper;
20 | import android.util.AndroidRuntimeException;
21 |
22 | /**
23 | * SpringAnimation is an animation that is driven by a {@link SpringForce}. The spring force defines
24 | * the spring's stiffness, damping ratio, as well as the rest position. Once the SpringAnimation is
25 | * started, on each frame the spring force will update the animation's value and velocity.
26 | * The animation will continue to run until the spring force reaches equilibrium. If the spring used
27 | * in the animation is undamped, the animation will never reach equilibrium. Instead, it will
28 | * oscillate forever.
29 | */
30 | final class SpringAnimation extends DynamicAnimation {
31 |
32 | private SpringForce mSpring = null;
33 | private float mPendingPosition = UNSET;
34 | private static final float UNSET = Float.MAX_VALUE;
35 |
36 | /**
37 | * This creates a SpringAnimation that animates the property of the given view.
38 | * Note, a spring will need to setup through {@link #setSpring(SpringForce)} before
39 | * the animation starts.
40 | *
41 | * @param v The View whose property will be animated
42 | * @param property the property index of the view
43 | */
44 | public SpringAnimation(T v, Property property) {
45 | super(v, property);
46 | }
47 |
48 | /**
49 | * This creates a SpringAnimation that animates the property of the given view. A Spring will be
50 | * created with the given final position and default stiffness and damping ratio.
51 | * This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}.
52 | *
53 | * @param v The View whose property will be animated
54 | * @param property the property index of the view
55 | * @param finalPosition the final position of the spring to be created.
56 | */
57 | public SpringAnimation(T v, Property property, float finalPosition) {
58 | super(v, property);
59 | mSpring = new SpringForce(finalPosition);
60 | setSpringThreshold();
61 | }
62 |
63 | /**
64 | * Returns the spring that the animation uses for animations.
65 | *
66 | * @return the spring that the animation uses for animations
67 | */
68 | public SpringForce getSpring() {
69 | return mSpring;
70 | }
71 |
72 | /**
73 | * Uses the given spring as the force that drives this animation. If this spring force has its
74 | * parameters re-configured during the animation, the new configuration will be reflected in the
75 | * animation immediately.
76 | *
77 | * @param force a pre-defined spring force that drives the animation
78 | * @return the animation that the spring force is set on
79 | */
80 | public SpringAnimation setSpring(SpringForce force) {
81 | mSpring = force;
82 | setSpringThreshold();
83 | return this;
84 | }
85 |
86 | @Override
87 | public void start() {
88 | sanityCheck();
89 | super.start();
90 | }
91 |
92 | /**
93 | * Updates the final position of the spring.
94 | *
95 | * When the animation is running, calling this method would assume the position change of the
96 | * spring as a continuous movement since last frame, which yields more accurate results than
97 | * changing the spring position directly through {@link SpringForce#setFinalPosition(float)}.
98 | *
99 | * If the animation hasn't started, calling this method will change the spring position, and
100 | * immediately start the animation.
101 | *
102 | * @param finalPosition rest position of the spring
103 | */
104 | public void animateToFinalPosition(float finalPosition) {
105 | if (isRunning()) {
106 | mPendingPosition = finalPosition;
107 | } else {
108 | if (mSpring == null) {
109 | mSpring = new SpringForce(finalPosition);
110 | }
111 | mSpring.setFinalPosition(finalPosition);
112 | start();
113 | }
114 | }
115 |
116 | /**
117 | * Skips to the end of the animation. If the spring is undamped, an
118 | * {@link IllegalStateException} will be thrown, as the animation would never reach to an end.
119 | * It is recommended to check {@link #canSkipToEnd()} before calling this method. This method
120 | * should only be called on main thread. If animation is not running, no-op.
121 | *
122 | * @throws IllegalStateException if the spring is undamped (i.e. damping ratio = 0)
123 | * @throws AndroidRuntimeException if this method is not called on the main thread
124 | */
125 | public void skipToEnd() {
126 | if (!canSkipToEnd()) {
127 | throw new UnsupportedOperationException("Spring animations can only come to an end"
128 | + " when there is damping");
129 | }
130 | if (Looper.myLooper() != Looper.getMainLooper()) {
131 | throw new AndroidRuntimeException("Animations may only be started on the main thread");
132 | }
133 | if (mRunning) {
134 | if (mPendingPosition != UNSET) {
135 | mSpring.setFinalPosition(mPendingPosition);
136 | mPendingPosition = UNSET;
137 | }
138 | mValue = mSpring.getFinalPosition();
139 | mVelocity = 0;
140 | cancel();
141 | }
142 | }
143 |
144 | /**
145 | * Queries whether the spring can eventually come to the rest position.
146 | *
147 | * @return {@code true} if the spring is damped, otherwise {@code false}
148 | */
149 | public boolean canSkipToEnd() {
150 | return mSpring.mDampingRatio > 0;
151 | }
152 |
153 | /************************ Below are private APIs *************************/
154 |
155 | private void setSpringThreshold() {
156 | if (mViewProperty == ROTATION || mViewProperty == ROTATION_X
157 | || mViewProperty == ROTATION_Y) {
158 | mSpring.setDefaultThreshold(SpringForce.VALUE_THRESHOLD_ROTATION);
159 | } else if (mViewProperty == ALPHA) {
160 | mSpring.setDefaultThreshold(SpringForce.VALUE_THRESHOLD_ALPHA);
161 | } else if (mViewProperty == SCALE_X || mViewProperty == SCALE_Y) {
162 | mSpring.setDefaultThreshold(SpringForce.VALUE_THRESHOLD_SCALE);
163 | } else {
164 | mSpring.setDefaultThreshold(SpringForce.VALUE_THRESHOLD_IN_PIXEL);
165 | }
166 | }
167 |
168 | private void sanityCheck() {
169 | if (mSpring == null) {
170 | throw new UnsupportedOperationException("Incomplete SpringAnimation: Either final"
171 | + " position or a spring force needs to be set.");
172 | }
173 | double finalPosition = mSpring.getFinalPosition();
174 | if (finalPosition > mMaxValue) {
175 | throw new UnsupportedOperationException("Final position of the spring cannot be greater"
176 | + " than the max value.");
177 | } else if (finalPosition < mMinValue) {
178 | throw new UnsupportedOperationException("Final position of the spring cannot be less"
179 | + " than the min value.");
180 | }
181 | }
182 |
183 | @Override
184 | boolean updateValueAndVelocity(long deltaT) {
185 | if (mPendingPosition != UNSET) {
186 | double lastPosition = mSpring.getFinalPosition();
187 | // Approximate by considering half of the time spring position stayed at the old
188 | // position, half of the time it's at the new position.
189 | SpringForce.MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT / 2);
190 | mSpring.setFinalPosition(mPendingPosition);
191 | mPendingPosition = UNSET;
192 |
193 | massState = mSpring.updateValues(massState.mValue, massState.mVelocity, deltaT / 2);
194 | mValue = massState.mValue;
195 | mVelocity = massState.mVelocity;
196 | } else {
197 | SpringForce.MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT);
198 | mValue = massState.mValue;
199 | mVelocity = massState.mVelocity;
200 | }
201 |
202 | mValue = Math.max(mValue, mMinValue);
203 | mValue = Math.min(mValue, mMaxValue);
204 |
205 | if (isAtEquilibrium(mValue, mVelocity)) {
206 | mValue = mSpring.getFinalPosition();
207 | mVelocity = 0f;
208 | return true;
209 | }
210 | return false;
211 | }
212 |
213 | @Override
214 | float getAcceleration(float value, float velocity) {
215 | return mSpring.getAcceleration(value, velocity);
216 | }
217 |
218 | @Override
219 | boolean isAtEquilibrium(float value, float velocity) {
220 | return mSpring.isAtEquilibrium(value, velocity);
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/circualreveal/src/main/java/io/codetail/animation/ViewRevealManager.java:
--------------------------------------------------------------------------------
1 | package io.codetail.animation;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.ObjectAnimator;
6 | import android.graphics.Canvas;
7 | import android.graphics.Color;
8 | import android.graphics.Paint;
9 | import android.graphics.Path;
10 | import android.graphics.Region;
11 | import android.os.Build;
12 | import android.util.Property;
13 | import android.view.View;
14 | import java.util.HashMap;
15 | import java.util.Map;
16 |
17 | @SuppressWarnings("WeakerAccess")
18 | public class ViewRevealManager {
19 | public static final ClipRadiusProperty REVEAL = new ClipRadiusProperty();
20 |
21 | private final ViewTransformation viewTransformation;
22 | private final Map targets = new HashMap<>();
23 | private final Map animators = new HashMap<>();
24 |
25 | private final AnimatorListenerAdapter animatorCallback = new AnimatorListenerAdapter() {
26 | @Override public void onAnimationStart(Animator animation) {
27 | final RevealValues values = getValues(animation);
28 | values.clip(true);
29 | }
30 |
31 | @Override public void onAnimationCancel(Animator animation) {
32 | endAnimation(animation);
33 | }
34 |
35 | @Override public void onAnimationEnd(Animator animation) {
36 | endAnimation(animation);
37 | }
38 |
39 | private void endAnimation(Animator animation) {
40 | final RevealValues values = getValues(animation);
41 | values.clip(false);
42 |
43 | // Clean up after animation is done
44 | targets.remove(values.target);
45 | animators.remove(animation);
46 | }
47 | };
48 |
49 | public ViewRevealManager() {
50 | this(new PathTransformation());
51 | }
52 |
53 | public ViewRevealManager(ViewTransformation transformation) {
54 | this.viewTransformation = transformation;
55 | }
56 |
57 | Animator dispatchCreateAnimator(RevealValues data) {
58 | final Animator animator = createAnimator(data);
59 |
60 | // Before animation is started keep them
61 | targets.put(data.target(), data);
62 | animators.put(animator, data);
63 | return animator;
64 | }
65 |
66 | /**
67 | * Create custom animator of circular reveal
68 | *
69 | * @param data RevealValues contains information of starting & ending points, animation target and
70 | * current animation values
71 | * @return Animator to manage reveal animation
72 | */
73 | protected Animator createAnimator(RevealValues data) {
74 | final ObjectAnimator animator =
75 | ObjectAnimator.ofFloat(data, REVEAL, data.startRadius, data.endRadius);
76 |
77 | animator.addListener(getAnimatorCallback());
78 | return animator;
79 | }
80 |
81 | protected final AnimatorListenerAdapter getAnimatorCallback() {
82 | return animatorCallback;
83 | }
84 |
85 | /**
86 | * @return Retruns Animator
87 | */
88 | protected final RevealValues getValues(Animator animator) {
89 | return animators.get(animator);
90 | }
91 |
92 | /**
93 | * @return Map of started animators
94 | */
95 | protected final RevealValues getValues(View view) {
96 | return targets.get(view);
97 | }
98 |
99 | /**
100 | * @return True if you don't want use Android native reveal animator in order to use your own
101 | * custom one
102 | */
103 | protected boolean overrideNativeAnimator() {
104 | return false;
105 | }
106 |
107 | /**
108 | * @return True if animation was started and it is still running, otherwise returns False
109 | */
110 | public boolean isClipped(View child) {
111 | final RevealValues data = getValues(child);
112 | return data != null && data.isClipping();
113 | }
114 |
115 | /**
116 | * Applies path clipping on a canvas before drawing child,
117 | * you should save canvas state before viewTransformation and
118 | * restore it afterwards
119 | *
120 | * @param canvas Canvas to apply clipping before drawing
121 | * @param child Reveal animation target
122 | * @return True if viewTransformation was successfully applied on referenced child, otherwise
123 | * child be not the target and therefore animation was skipped
124 | */
125 | public final boolean transform(Canvas canvas, View child) {
126 | final RevealValues revealData = targets.get(child);
127 |
128 | // Target doesn't has animation values
129 | if (revealData == null) {
130 | return false;
131 | }
132 | // Check whether target consistency
133 | else if (revealData.target != child) {
134 | throw new IllegalStateException("Inconsistency detected, contains incorrect target view");
135 | }
136 | // View doesn't wants to be clipped therefore transformation is useless
137 | else if (!revealData.clipping) {
138 | return false;
139 | }
140 |
141 | return viewTransformation.transform(canvas, child, revealData);
142 | }
143 |
144 | public static final class RevealValues {
145 | private static final Paint debugPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
146 |
147 | static {
148 | debugPaint.setColor(Color.GREEN);
149 | debugPaint.setStyle(Paint.Style.FILL);
150 | debugPaint.setStrokeWidth(2);
151 | }
152 |
153 | final int centerX;
154 | final int centerY;
155 |
156 | final float startRadius;
157 | final float endRadius;
158 |
159 | // Flag that indicates whether view is clipping now, mutable
160 | boolean clipping;
161 |
162 | // Revealed radius
163 | float radius;
164 |
165 | // Animation target
166 | View target;
167 |
168 | public RevealValues(View target, int centerX, int centerY, float startRadius, float endRadius) {
169 | this.target = target;
170 | this.centerX = centerX;
171 | this.centerY = centerY;
172 | this.startRadius = startRadius;
173 | this.endRadius = endRadius;
174 | }
175 |
176 | public void radius(float radius) {
177 | this.radius = radius;
178 | }
179 |
180 | /** @return current clipping radius */
181 | public float radius() {
182 | return radius;
183 | }
184 |
185 | /** @return Animating view */
186 | public View target() {
187 | return target;
188 | }
189 |
190 | public void clip(boolean clipping) {
191 | this.clipping = clipping;
192 | }
193 |
194 | /** @return View clip status */
195 | public boolean isClipping() {
196 | return clipping;
197 | }
198 | }
199 |
200 | /**
201 | * Custom View viewTransformation extension used for applying different reveal
202 | * techniques
203 | */
204 | interface ViewTransformation {
205 |
206 | /**
207 | * Apply view viewTransformation
208 | *
209 | * @param canvas Main canvas
210 | * @param child Target to be clipped & revealed
211 | * @return True if viewTransformation is applied, otherwise return fAlse
212 | */
213 | boolean transform(Canvas canvas, View child, RevealValues values);
214 | }
215 |
216 | public static class PathTransformation implements ViewTransformation {
217 |
218 | // Android Canvas is tricky, we cannot clip circles directly with Canvas API
219 | // but it is allowed using Path, therefore we use it :|
220 | private final Path path = new Path();
221 |
222 | private Region.Op op = Region.Op.REPLACE;
223 |
224 | /** @see Canvas#clipPath(Path, Region.Op) */
225 | public Region.Op op() {
226 | return op;
227 | }
228 |
229 | /** @see Canvas#clipPath(Path, Region.Op) */
230 | public void op(Region.Op op) {
231 | this.op = op;
232 | }
233 |
234 | @Override public boolean transform(Canvas canvas, View child, RevealValues values) {
235 | path.reset();
236 | // trick to applyTransformation animation, when even x & y translations are running
237 | path.addCircle(child.getX() + values.centerX, child.getY() + values.centerY, values.radius,
238 | Path.Direction.CW);
239 |
240 | canvas.clipPath(path, op);
241 |
242 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
243 | child.invalidateOutline();
244 | }
245 | return false;
246 | }
247 | }
248 |
249 | /**
250 | * Property animator. For performance improvements better to use
251 | * directly variable member (but it's little enhancement that always
252 | * caught as dangerous, let's see)
253 | */
254 | private static final class ClipRadiusProperty extends Property {
255 |
256 | ClipRadiusProperty() {
257 | super(Float.class, "supportCircularReveal");
258 | }
259 |
260 | @Override public void set(RevealValues data, Float value) {
261 | data.radius = value;
262 | data.target.invalidate();
263 | }
264 |
265 | @Override public Float get(RevealValues v) {
266 | return v.radius();
267 | }
268 | }
269 |
270 | /**
271 | * As class name cue's it changes layer type of {@link View} on animation createAnimator
272 | * in order to improve animation smooth & performance and returns original value
273 | * on animation end
274 | */
275 | static class ChangeViewLayerTypeAdapter extends AnimatorListenerAdapter {
276 | private RevealValues viewData;
277 | private int featuredLayerType;
278 | private int originalLayerType;
279 |
280 | ChangeViewLayerTypeAdapter(RevealValues viewData, int layerType) {
281 | this.viewData = viewData;
282 | this.featuredLayerType = layerType;
283 | this.originalLayerType = viewData.target.getLayerType();
284 | }
285 |
286 | @Override public void onAnimationStart(Animator animation) {
287 | viewData.target().setLayerType(featuredLayerType, null);
288 | }
289 |
290 | @Override public void onAnimationCancel(Animator animation) {
291 | viewData.target().setLayerType(originalLayerType, null);
292 | }
293 |
294 | @Override public void onAnimationEnd(Animator animation) {
295 | viewData.target().setLayerType(originalLayerType, null);
296 | }
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/app/src/main/java/io/codetail/circualrevealsample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package io.codetail.circualrevealsample;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.AnimatorSet;
6 | import android.animation.ObjectAnimator;
7 | import android.content.Intent;
8 | import android.graphics.Rect;
9 | import android.os.Build;
10 | import android.os.Bundle;
11 | import android.support.animation.SpringForce;
12 | import android.support.annotation.Nullable;
13 | import android.support.design.widget.BottomSheetBehavior;
14 | import android.support.v4.view.animation.FastOutLinearInInterpolator;
15 | import android.support.v4.view.animation.FastOutSlowInInterpolator;
16 | import android.support.v7.app.AppCompatActivity;
17 | import android.support.v7.widget.CardView;
18 | import android.util.Property;
19 | import android.view.View;
20 | import android.view.ViewGroup;
21 | import android.widget.CompoundButton;
22 | import butterknife.BindView;
23 | import butterknife.ButterKnife;
24 | import butterknife.OnClick;
25 | import io.codetail.animation.RevealViewGroup;
26 | import io.codetail.animation.SpringViewAnimatorManager;
27 | import io.codetail.animation.ViewAnimationUtils;
28 | import io.codetail.animation.ViewRevealManager;
29 | import io.codetail.widget.RevealFrameLayout;
30 |
31 | /**
32 | * Aware section https://www.google.com/design/spec/motion/material-motion.html#material-motion-how-does-material-move
33 | */
34 | public class MainActivity extends AppCompatActivity {
35 | final static int SLOW_DURATION = 400;
36 | final static int FAST_DURATION = 200;
37 |
38 | @BindView(R.id.parent) RevealFrameLayout parent;
39 | @BindView(R.id.circlesLine) ViewGroup circlesLine;
40 | @BindView(R.id.cardsLine) ViewGroup cardsLine;
41 | @BindView(R.id.activator_mask) CardView activatorMask;
42 | @BindView(R.id.springSettings) SpringSettingsBottomDialog settingsView;
43 |
44 | private float maskElevation;
45 |
46 | @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
47 | super.onCreate(savedInstanceState);
48 | setContentView(R.layout.activity_main);
49 | ButterKnife.bind(this);
50 |
51 | final ViewRevealManager revealManager = new ViewRevealManager();
52 | final SpringViewAnimatorManager springManager = new SpringViewAnimatorManager();
53 | springManager.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
54 | springManager.setStiffness(SpringForce.STIFFNESS_LOW);
55 |
56 | parent.setViewRevealManager(revealManager);
57 |
58 | settingsView.addSwitch("Enable Spring", false, new CompoundButton.OnCheckedChangeListener() {
59 | @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
60 | parent.setViewRevealManager(isChecked ? springManager : revealManager);
61 | }
62 | });
63 | settingsView.setAnimatorManager(springManager);
64 |
65 | final BottomSheetBehavior behavior = BottomSheetBehavior.from(settingsView);
66 | behavior.setPeekHeight(getResources().getDimensionPixelSize(R.dimen.bottom_peek_height));
67 | behavior.setSkipCollapsed(false);
68 | behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
69 | }
70 |
71 | @OnClick(R.id.activator) void activateAwareMotion(View target) {
72 | // Cancel all concurrent events on view
73 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
74 | target.cancelPendingInputEvents();
75 | }
76 | target.setEnabled(false);
77 |
78 | // Coordinates of circle initial point
79 | final ViewGroup parent = (ViewGroup) activatorMask.getParent();
80 | final Rect bounds = new Rect();
81 | final Rect maskBounds = new Rect();
82 |
83 | target.getDrawingRect(bounds);
84 | activatorMask.getDrawingRect(maskBounds);
85 | parent.offsetDescendantRectToMyCoords(target, bounds);
86 | parent.offsetDescendantRectToMyCoords(activatorMask, maskBounds);
87 |
88 | // Put Mask view at circle 8initial points
89 | maskElevation = activatorMask.getCardElevation();
90 | activatorMask.setCardElevation(0);
91 | activatorMask.setVisibility(View.VISIBLE);
92 | activatorMask.setX(bounds.left - maskBounds.centerX());
93 | activatorMask.setY(bounds.top - maskBounds.centerY());
94 |
95 | circlesLine.setVisibility(View.INVISIBLE);
96 |
97 | final int cX = maskBounds.centerX();
98 | final int cY = maskBounds.centerY();
99 |
100 | final float endRadius = (float) Math.hypot(maskBounds.width() * .5f, maskBounds.height() * .5f);
101 |
102 | Animator circularReveal =
103 | ViewAnimationUtils.createCircularReveal(activatorMask, cX, cY, target.getWidth() / 2,
104 | endRadius, View.LAYER_TYPE_HARDWARE);
105 |
106 | final float c0X = bounds.centerX() - maskBounds.centerX();
107 | final float c0Y = bounds.centerY() - maskBounds.centerY();
108 |
109 | AnimatorPath path = new AnimatorPath();
110 | path.moveTo(c0X, c0Y);
111 | path.curveTo(c0X, c0Y, 0, c0Y, 0, 0);
112 |
113 | ObjectAnimator pathAnimator = ObjectAnimator.ofObject(this, "maskLocation", new PathEvaluator(),
114 | path.getPoints().toArray());
115 |
116 | AnimatorSet set = new AnimatorSet();
117 | set.playTogether(circularReveal, pathAnimator);
118 | set.setInterpolator(new FastOutSlowInInterpolator());
119 | set.setDuration(SLOW_DURATION);
120 | set.addListener(new AnimatorListenerAdapter() {
121 | @Override public void onAnimationEnd(Animator animation) {
122 | executeCardsSequentialAnimation();
123 | activatorMask.setCardElevation(maskElevation);
124 | }
125 | });
126 | set.start();
127 | }
128 |
129 | private void executeCardsSequentialAnimation() {
130 | final int length = cardsLine.getChildCount();
131 | cardsLine.setVisibility(View.VISIBLE);
132 |
133 | final Animator[] animators = new Animator[length];
134 | for (int i = 0; i < length; i++) {
135 | View target = cardsLine.getChildAt(i);
136 | final float x0 = 0;// i == 0 ? 0 : -10 * (1 + i * 0.2f);
137 | final float y0 = 10 * i;
138 |
139 | target.setTranslationX(x0);
140 | target.setTranslationY(y0);
141 |
142 | AnimatorPath path = new AnimatorPath();
143 | path.moveTo(x0, y0);
144 | path.lineTo(0, 0);
145 |
146 | PathPoint[] points = new PathPoint[path.getPoints().size()];
147 | path.getPoints().toArray(points);
148 |
149 | AnimatorSet set = new AnimatorSet();
150 | set.play(ObjectAnimator.ofObject(target, PATH_POINT, new PathEvaluator(), points))
151 | .with(ObjectAnimator.ofFloat(target, View.ALPHA, 0.8f, 1f));
152 |
153 | animators[i] = set;
154 | animators[i].setStartDelay(15 * i);
155 | }
156 |
157 | final AnimatorSet sequential = new AnimatorSet();
158 | sequential.playTogether(animators);
159 | sequential.setInterpolator(new FastOutLinearInInterpolator());
160 | sequential.setDuration(FAST_DURATION);
161 | sequential.start();
162 | }
163 |
164 | @OnClick(R.id.reset) void resetUi(View resetCard) {
165 | cardsLine.setVisibility(View.INVISIBLE);
166 |
167 | final View target = ButterKnife.findById(this, R.id.activator);
168 |
169 | // Coordinates of circle initial point
170 | final ViewGroup parent = (ViewGroup) activatorMask.getParent();
171 | final Rect bounds = new Rect();
172 | final Rect maskBounds = new Rect();
173 |
174 | target.getDrawingRect(bounds);
175 | activatorMask.getDrawingRect(maskBounds);
176 | parent.offsetDescendantRectToMyCoords(target, bounds);
177 | parent.offsetDescendantRectToMyCoords(activatorMask, maskBounds);
178 |
179 | maskElevation = activatorMask.getCardElevation();
180 | activatorMask.setCardElevation(0);
181 |
182 | final int cX = maskBounds.centerX();
183 | final int cY = maskBounds.centerY();
184 |
185 | final Animator circularReveal = ViewAnimationUtils.createCircularReveal(activatorMask, cX, cY,
186 | (float) Math.hypot(maskBounds.width() * .5f, maskBounds.height() * .5f),
187 | target.getWidth() / 2f, View.LAYER_TYPE_HARDWARE);
188 |
189 | final float c0X = bounds.centerX() - maskBounds.centerX();
190 | final float c0Y = bounds.centerY() - maskBounds.centerY();
191 |
192 | AnimatorPath path = new AnimatorPath();
193 | path.moveTo(0, 0);
194 | path.curveTo(0, 0, 0, c0Y, c0X, c0Y);
195 |
196 | ObjectAnimator pathAnimator = ObjectAnimator.ofObject(this, "maskLocation", new PathEvaluator(),
197 | path.getPoints().toArray());
198 |
199 | AnimatorSet set = new AnimatorSet();
200 | set.playTogether(circularReveal, pathAnimator);
201 | set.setInterpolator(new FastOutSlowInInterpolator());
202 | set.setDuration(SLOW_DURATION);
203 | set.addListener(new AnimatorListenerAdapter() {
204 | @Override public void onAnimationEnd(Animator animation) {
205 | activatorMask.setCardElevation(maskElevation);
206 | activatorMask.setVisibility(View.INVISIBLE);
207 |
208 | circlesLine.setVisibility(View.VISIBLE);
209 | executeCirclesDropDown();
210 | target.setEnabled(true);
211 | }
212 | });
213 | set.start();
214 | }
215 |
216 | private void executeCirclesDropDown() {
217 | final int length = circlesLine.getChildCount();
218 | Animator[] animators = new Animator[length];
219 | for (int i = 0; i < length; i++) {
220 | View target = circlesLine.getChildAt(i);
221 | final float x0 = -10 * i;
222 | final float y0 = -10 * i;
223 |
224 | target.setTranslationX(x0);
225 | target.setTranslationY(y0);
226 |
227 | AnimatorPath path = new AnimatorPath();
228 | path.moveTo(x0, y0);
229 | path.curveTo(x0, y0, 0, y0, 0, 0);
230 |
231 | PathPoint[] points = new PathPoint[path.getPoints().size()];
232 | path.getPoints().toArray(points);
233 |
234 | AnimatorSet set = new AnimatorSet();
235 | set.play(ObjectAnimator.ofObject(target, PATH_POINT, new PathEvaluator(), points))
236 | .with(ObjectAnimator.ofFloat(target, View.ALPHA, (length - i) * 0.1f + 0.6f, 1f));
237 |
238 | animators[i] = set;
239 | animators[i].setStartDelay(15 * i);
240 | }
241 |
242 | AnimatorSet set = new AnimatorSet();
243 | set.playTogether(animators);
244 | set.setInterpolator(new FastOutSlowInInterpolator());
245 | set.setDuration(FAST_DURATION);
246 | set.start();
247 | }
248 |
249 | private final static Property PATH_POINT =
250 | new Property(PathPoint.class, "PATH_POINT") {
251 | PathPoint point;
252 |
253 | @Override public PathPoint get(View object) {
254 | return point;
255 | }
256 |
257 | @Override public void set(View object, PathPoint value) {
258 | point = value;
259 |
260 | object.setTranslationX(value.mX);
261 | object.setTranslationY(value.mY);
262 | }
263 | };
264 |
265 | public void setMaskLocation(PathPoint location) {
266 | activatorMask.setX(location.mX);
267 | activatorMask.setY(location.mY);
268 | }
269 |
270 | @OnClick(R.id.open_radial_transformation) void open2Example() {
271 | Intent intent = new Intent(this, RadialTransformationActivity.class);
272 | startActivity(intent);
273 | }
274 | }
275 |
--------------------------------------------------------------------------------
/spring-revealmanager/src/main/java/io/codetail/animation/SpringForce.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.codetail.animation;
18 |
19 | import android.support.annotation.FloatRange;
20 |
21 | /**
22 | * Spring Force defines the characteristics of the spring being used in the animation.
23 | *
24 | * By configuring the stiffness and damping ratio, callers can create a spring with the look and
25 | * feel suits their use case. Stiffness corresponds to the spring constant. The stiffer the spring
26 | * is, the harder it is to stretch it, the faster it undergoes dampening.
27 | *
28 | * Spring damping ratio describes how oscillations in a system decay after a disturbance.
29 | * When damping ratio > 1* (i.e. over-damped), the object will quickly return to the rest position
30 | * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
31 | * return to equilibrium within the shortest amount of time. When damping ratio is less than 1
32 | * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without any
33 | * damping (i.e. damping ratio = 0), the mass will oscillate forever.
34 | */
35 | final class SpringForce implements Force {
36 | /**
37 | * Stiffness constant for extremely stiff spring.
38 | */
39 | public static final float STIFFNESS_HIGH = 10_000f;
40 | /**
41 | * Stiffness constant for medium stiff spring. This is the default stiffness for spring force.
42 | */
43 | public static final float STIFFNESS_MEDIUM = 1500f;
44 | /**
45 | * Stiffness constant for a spring with low stiffness.
46 | */
47 | public static final float STIFFNESS_LOW = 200f;
48 | /**
49 | * Stiffness constant for a spring with very low stiffness.
50 | */
51 | public static final float STIFFNESS_VERY_LOW = 50f;
52 |
53 | /**
54 | * Damping ratio for a very bouncy spring. Note for under-damped springs
55 | * (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring.
56 | */
57 | public static final float DAMPING_RATIO_HIGH_BOUNCY = 0.2f;
58 | /**
59 | * Damping ratio for a medium bouncy spring. This is also the default damping ratio for spring
60 | * force. Note for under-damped springs (i.e. damping ratio < 1), the lower the damping ratio,
61 | * the more bouncy the spring.
62 | */
63 | public static final float DAMPING_RATIO_MEDIUM_BOUNCY = 0.5f;
64 | /**
65 | * Damping ratio for a spring with low bounciness. Note for under-damped springs
66 | * (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness.
67 | */
68 | public static final float DAMPING_RATIO_LOW_BOUNCY = 0.75f;
69 | /**
70 | * Damping ratio for a spring with no bounciness. This damping ratio will create a critically
71 | * damped spring that returns to equilibrium within the shortest amount of time without
72 | * oscillating.
73 | */
74 | public static final float DAMPING_RATIO_NO_BOUNCY = 1f;
75 |
76 | // This multiplier is used to calculate the velocity threshold given a certain value threshold.
77 | // The idea is that if it takes >= 1 frame to move the value threshold amount, then the velocity
78 | // is a reasonable threshold.
79 | private static final double VELOCITY_THRESHOLD_MULTIPLIER = 1000.0 / 16.0;
80 |
81 | // Default threshold for different properties.
82 | static final double VALUE_THRESHOLD_IN_PIXEL = 0.75;
83 | static final double VALUE_THRESHOLD_ALPHA = VALUE_THRESHOLD_IN_PIXEL / 255.0;
84 | static final double VALUE_THRESHOLD_SCALE = VALUE_THRESHOLD_IN_PIXEL / 500.0;
85 | static final double VALUE_THRESHOLD_ROTATION = VALUE_THRESHOLD_IN_PIXEL / 360.0;
86 |
87 | // Natural frequency
88 | double mNaturalFreq = Math.sqrt(STIFFNESS_MEDIUM);
89 | // Damping ratio.
90 | double mDampingRatio = DAMPING_RATIO_MEDIUM_BOUNCY;
91 |
92 | // Value to indicate an unset state.
93 | private static final double UNSET = Double.MAX_VALUE;
94 |
95 | // Indicates whether the spring has been initialized
96 | private boolean mInitialized = false;
97 |
98 | // Threshold for velocity and value to determine when it's reasonable to assume that the spring
99 | // is approximately at rest.
100 | private double mValueThreshold = VALUE_THRESHOLD_IN_PIXEL;
101 | private double mVelocityThreshold = VALUE_THRESHOLD_IN_PIXEL * VELOCITY_THRESHOLD_MULTIPLIER;
102 |
103 | // Intermediate values to simplify the spring function calculation per frame.
104 | private double mGammaPlus;
105 | private double mGammaMinus;
106 | private double mDampedFreq;
107 |
108 | // Final position of the spring. This must be set before the start of the animation.
109 | private double mFinalPosition = UNSET;
110 |
111 | // Internal state to hold a value/velocity pair.
112 | private final MassState mMassState = new MassState();
113 |
114 | // Internal state for value/velocity pair.
115 | static class MassState {
116 | float mValue;
117 | float mVelocity;
118 | }
119 |
120 | /**
121 | * Creates a spring force. Note that final position of the spring must be set through
122 | * {@link #setFinalPosition(float)} before the spring animation starts.
123 | */
124 | public SpringForce() {
125 | // No op.
126 | }
127 |
128 | /**
129 | * Creates a spring with a given final rest position.
130 | *
131 | * @param finalPosition final position of the spring when it reaches equilibrium
132 | */
133 | public SpringForce(float finalPosition) {
134 | mFinalPosition = finalPosition;
135 | }
136 |
137 | /**
138 | * Sets the stiffness of a spring. The more stiff a spring is, the more force it applies to
139 | * the object attached when the spring is not at the final position. Default stiffness is
140 | * {@link #STIFFNESS_MEDIUM}.
141 | *
142 | * @param stiffness non-negative stiffness constant of a spring
143 | * @return the spring force that the given stiffness is set on
144 | * @throws IllegalArgumentException if the given spring stiffness is negative.
145 | */
146 | public SpringForce setStiffness(@FloatRange(from = 0.0) float stiffness) {
147 | if (stiffness < 0) {
148 | throw new IllegalArgumentException("Spring stiffness constant cannot be negative");
149 | }
150 | mNaturalFreq = Math.sqrt(stiffness);
151 | // All the intermediate values need to be recalculated.
152 | mInitialized = false;
153 | return this;
154 | }
155 |
156 | /**
157 | * Gets the stiffness of the spring.
158 | *
159 | * @return the stiffness of the spring
160 | */
161 | public float getStiffness() {
162 | return (float) (mNaturalFreq * mNaturalFreq);
163 | }
164 |
165 | /**
166 | * Spring damping ratio describes how oscillations in a system decay after a disturbance.
167 | *
168 | * When damping ratio > 1 (over-damped), the object will quickly return to the rest position
169 | * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
170 | * return to equilibrium within the shortest amount of time. When damping ratio is less than 1
171 | * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without
172 | * any damping (i.e. damping ratio = 0), the mass will oscillate forever.
173 | *
174 | * Default damping ratio is {@link #DAMPING_RATIO_MEDIUM_BOUNCY}.
175 | *
176 | * @param dampingRatio damping ratio of the spring, it should be non-negative
177 | * @return the spring force that the given damping ratio is set on
178 | * @throws IllegalArgumentException if the {@param dampingRatio} is negative.
179 | */
180 | public SpringForce setDampingRatio(@FloatRange(from = 0.0) float dampingRatio) {
181 | if (dampingRatio < 0) {
182 | throw new IllegalArgumentException("Damping ratio must be non-negative");
183 | }
184 | mDampingRatio = dampingRatio;
185 | // All the intermediate values need to be recalculated.
186 | mInitialized = false;
187 | return this;
188 | }
189 |
190 | /**
191 | * Returns the damping ratio of the spring.
192 | *
193 | * @return damping ratio of the spring
194 | */
195 | public float getDampingRatio() {
196 | return (float) mDampingRatio;
197 | }
198 |
199 | /**
200 | * Sets the rest position of the spring.
201 | *
202 | * @param finalPosition rest position of the spring
203 | * @return the spring force that the given final position is set on
204 | */
205 | public SpringForce setFinalPosition(float finalPosition) {
206 | mFinalPosition = finalPosition;
207 | return this;
208 | }
209 |
210 | /**
211 | * Returns the rest position of the spring.
212 | *
213 | * @return rest position of the spring
214 | */
215 | public float getFinalPosition() {
216 | return (float) mFinalPosition;
217 | }
218 |
219 | /*********************** Below are private APIs *********************/
220 |
221 | /**
222 | * @hide
223 | */
224 | @Override
225 | public float getAcceleration(float lastDisplacement, float lastVelocity) {
226 |
227 | lastDisplacement -= getFinalPosition();
228 |
229 | double k = mNaturalFreq * mNaturalFreq;
230 | double c = 2 * mNaturalFreq * mDampingRatio;
231 |
232 | return (float) (-k * lastDisplacement - c * lastVelocity);
233 | }
234 |
235 | /**
236 | * @hide
237 | */
238 | @Override
239 | public boolean isAtEquilibrium(float value, float velocity) {
240 | return Math.abs(velocity) < mVelocityThreshold
241 | && Math.abs(value - getFinalPosition()) < mValueThreshold;
242 | }
243 |
244 | /**
245 | * Initialize the string by doing the necessary pre-calculation as well as some sanity check
246 | * on the setup.
247 | *
248 | * @throws IllegalStateException if the final position is not yet set by the time the spring
249 | * animation has started
250 | */
251 | private void init() {
252 | if (mInitialized) {
253 | return;
254 | }
255 |
256 | if (mFinalPosition == UNSET) {
257 | throw new IllegalStateException("Error: Final position of the spring must be"
258 | + " set before the animation starts");
259 | }
260 |
261 | if (mDampingRatio > 1) {
262 | // Over damping
263 | mGammaPlus = -mDampingRatio * mNaturalFreq
264 | + mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
265 | mGammaMinus = -mDampingRatio * mNaturalFreq
266 | - mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
267 | } else if (mDampingRatio >= 0 && mDampingRatio < 1) {
268 | // Under damping
269 | mDampedFreq = mNaturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
270 | }
271 |
272 | mInitialized = true;
273 | }
274 |
275 | /**
276 | * Internal only call for Spring to calculate the spring position/velocity using
277 | * an analytical approach.
278 | */
279 | MassState updateValues(double lastDisplacement, double lastVelocity, long timeElapsed) {
280 | init();
281 |
282 | double deltaT = timeElapsed / 1000d; // unit: seconds
283 | lastDisplacement -= mFinalPosition;
284 | double displacement;
285 | double currentVelocity;
286 | if (mDampingRatio > 1) {
287 | // Overdamped
288 | double coeffA = lastDisplacement - (mGammaMinus * lastDisplacement - lastVelocity)
289 | / (mGammaMinus - mGammaPlus);
290 | double coeffB = (mGammaMinus * lastDisplacement - lastVelocity)
291 | / (mGammaMinus - mGammaPlus);
292 | displacement = coeffA * Math.pow(Math.E, mGammaMinus * deltaT)
293 | + coeffB * Math.pow(Math.E, mGammaPlus * deltaT);
294 | currentVelocity = coeffA * mGammaMinus * Math.pow(Math.E, mGammaMinus * deltaT)
295 | + coeffB * mGammaPlus * Math.pow(Math.E, mGammaPlus * deltaT);
296 | } else if (mDampingRatio == 1) {
297 | // Critically damped
298 | double coeffA = lastDisplacement;
299 | double coeffB = lastVelocity + mNaturalFreq * lastDisplacement;
300 | displacement = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT);
301 | currentVelocity = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT)
302 | * (-mNaturalFreq) + coeffB * Math.pow(Math.E, -mNaturalFreq * deltaT);
303 | } else {
304 | // Underdamped
305 | double cosCoeff = lastDisplacement;
306 | double sinCoeff = (1 / mDampedFreq) * (mDampingRatio * mNaturalFreq
307 | * lastDisplacement + lastVelocity);
308 | displacement = Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
309 | * (cosCoeff * Math.cos(mDampedFreq * deltaT)
310 | + sinCoeff * Math.sin(mDampedFreq * deltaT));
311 | currentVelocity = displacement * (-mNaturalFreq) * mDampingRatio
312 | + Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
313 | * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT)
314 | + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT));
315 | }
316 |
317 | mMassState.mValue = (float) (displacement + mFinalPosition);
318 | mMassState.mVelocity = (float) currentVelocity;
319 | return mMassState;
320 | }
321 |
322 | /**
323 | * This threshold defines how close the animation value needs to be before the animation can
324 | * finish. This default value is based on the property being animated, e.g. animations on alpha,
325 | * scale, translation or rotation would have different thresholds. This value should be small
326 | * enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that
327 | * animations take seconds to finish.
328 | *
329 | * @param threshold the difference between the animation value and final spring position that is
330 | * allowed to end the animation when velocity is very low
331 | */
332 | void setDefaultThreshold(double threshold) {
333 | mValueThreshold = Math.abs(threshold);
334 | mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER;
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/spring-revealmanager/src/main/java/io/codetail/animation/DynamicAnimation.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.codetail.animation;
18 |
19 | import android.os.Build;
20 | import android.os.Looper;
21 | import android.util.AndroidRuntimeException;
22 | import android.view.View;
23 |
24 | import java.util.ArrayList;
25 |
26 | /**
27 | * This class is the base class of physics-based animations. It manages the animation's
28 | * lifecycle such as {@link #start()} and {@link #cancel()}. This base class also handles the common
29 | * setup for all the subclass animations. For example, DynamicAnimation supports adding
30 | * {@link OnAnimationEndListener} and {@link OnAnimationUpdateListener} so that the important
31 | * animation events can be observed through the callbacks. The start conditions for any subclass of
32 | * DynamicAnimation can be set using {@link #setStartValue(float)} and
33 | * {@link #setStartVelocity(float)}.
34 | *
35 | * @param subclass of DynamicAnimation
36 | */
37 | abstract class DynamicAnimation>
38 | implements AnimationHandler.AnimationFrameCallback {
39 |
40 | /**
41 | * ViewProperty holds the access of a property of a {@link View}. When an animation is
42 | * created with a {@link ViewProperty} instance, the corresponding property value of the view
43 | * will be updated through this ViewProperty instance.
44 | */
45 | public abstract static class Property {
46 | private final String mPropertyName;
47 |
48 | public Property(String name) {
49 | mPropertyName = name;
50 | }
51 |
52 | public abstract void setValue(T view, float value);
53 |
54 | public abstract float getValue(T view);
55 | }
56 |
57 | public abstract static class ViewProperty extends Property {
58 | public ViewProperty(String name) {
59 | super(name);
60 | }
61 | }
62 |
63 | /**
64 | * View's translationX property.
65 | */
66 | public static final ViewProperty TRANSLATION_X = new ViewProperty("translationX") {
67 | @Override
68 | public void setValue(View view, float value) {
69 | view.setTranslationX(value);
70 | }
71 |
72 | @Override
73 | public float getValue(View view) {
74 | return view.getTranslationX();
75 | }
76 | };
77 |
78 | /**
79 | * View's translationY property.
80 | */
81 | public static final ViewProperty TRANSLATION_Y = new ViewProperty("translationY") {
82 | @Override
83 | public void setValue(View view, float value) {
84 | view.setTranslationY(value);
85 | }
86 |
87 | @Override
88 | public float getValue(View view) {
89 | return view.getTranslationY();
90 | }
91 | };
92 |
93 | /**
94 | * View's translationZ property.
95 | */
96 | public static final ViewProperty TRANSLATION_Z = new ViewProperty("translationZ") {
97 | @Override
98 | public void setValue(View view, float value) {
99 | if (isZSupported()) {
100 | view.setTranslationZ(value);
101 | }
102 | }
103 |
104 | @Override
105 | public float getValue(View view) {
106 | if (isZSupported()) {
107 | return view.getTranslationZ();
108 | } else {
109 | return 0;
110 | }
111 | }
112 | };
113 |
114 | /**
115 | * View's scaleX property.
116 | */
117 | public static final ViewProperty SCALE_X = new ViewProperty("scaleX") {
118 | @Override
119 | public void setValue(View view, float value) {
120 | view.setScaleX(value);
121 | }
122 |
123 | @Override
124 | public float getValue(View view) {
125 | return view.getScaleX();
126 | }
127 | };
128 |
129 | /**
130 | * View's scaleY property.
131 | */
132 | public static final ViewProperty SCALE_Y = new ViewProperty("scaleY") {
133 | @Override
134 | public void setValue(View view, float value) {
135 | view.setScaleY(value);
136 | }
137 |
138 | @Override
139 | public float getValue(View view) {
140 | return view.getScaleY();
141 | }
142 | };
143 |
144 | /**
145 | * View's rotation property.
146 | */
147 | public static final ViewProperty ROTATION = new ViewProperty("rotation") {
148 | @Override
149 | public void setValue(View view, float value) {
150 | view.setRotation(value);
151 | }
152 |
153 | @Override
154 | public float getValue(View view) {
155 | return view.getRotation();
156 | }
157 | };
158 |
159 | /**
160 | * View's rotationX property.
161 | */
162 | public static final ViewProperty ROTATION_X = new ViewProperty("rotationX") {
163 | @Override
164 | public void setValue(View view, float value) {
165 | view.setRotationX(value);
166 | }
167 |
168 | @Override
169 | public float getValue(View view) {
170 | return view.getRotationX();
171 | }
172 | };
173 |
174 | /**
175 | * View's rotationY property.
176 | */
177 | public static final ViewProperty ROTATION_Y = new ViewProperty("rotationY") {
178 | @Override
179 | public void setValue(View view, float value) {
180 | view.setRotationY(value);
181 | }
182 |
183 | @Override
184 | public float getValue(View view) {
185 | return view.getRotationY();
186 | }
187 | };
188 |
189 | /**
190 | * View's x property.
191 | */
192 | public static final ViewProperty X = new ViewProperty("x") {
193 | @Override
194 | public void setValue(View view, float value) {
195 | view.setX(value);
196 | }
197 |
198 | @Override
199 | public float getValue(View view) {
200 | return view.getX();
201 | }
202 | };
203 |
204 | /**
205 | * View's y property.
206 | */
207 | public static final ViewProperty Y = new ViewProperty("y") {
208 | @Override
209 | public void setValue(View view, float value) {
210 | view.setY(value);
211 | }
212 |
213 | @Override
214 | public float getValue(View view) {
215 | return view.getY();
216 | }
217 | };
218 |
219 | /**
220 | * View's z property.
221 | */
222 | public static final ViewProperty Z = new ViewProperty("z") {
223 | @Override
224 | public void setValue(View view, float value) {
225 | if (isZSupported()) {
226 | view.setZ(value);
227 | }
228 | }
229 |
230 | @Override
231 | public float getValue(View view) {
232 | if (isZSupported()) {
233 | return view.getZ();
234 | } else {
235 | return 0;
236 | }
237 | }
238 | };
239 |
240 | /**
241 | * View's alpha property.
242 | */
243 | public static final ViewProperty ALPHA = new ViewProperty("alpha") {
244 | @Override
245 | public void setValue(View view, float value) {
246 | view.setAlpha(value);
247 | }
248 |
249 | @Override
250 | public float getValue(View view) {
251 | return view.getAlpha();
252 | }
253 | };
254 |
255 | // Properties below are not RenderThread compatible
256 | /**
257 | * View's scrollX property.
258 | */
259 | public static final ViewProperty SCROLL_X = new ViewProperty("scrollX") {
260 | @Override
261 | public void setValue(View view, float value) {
262 | view.setScrollX((int) value);
263 | }
264 |
265 | @Override
266 | public float getValue(View view) {
267 | return view.getScrollX();
268 | }
269 | };
270 |
271 | /**
272 | * View's scrollY property.
273 | */
274 | public static final ViewProperty SCROLL_Y = new ViewProperty("scrollY") {
275 | @Override
276 | public void setValue(View view, float value) {
277 | view.setScrollY((int) value);
278 | }
279 |
280 | @Override
281 | public float getValue(View view) {
282 | return view.getScrollY();
283 | }
284 | };
285 |
286 | // Use the max value of float to indicate an unset state.
287 | private static final float UNSET = Float.MAX_VALUE;
288 |
289 | // Internal tracking for velocity.
290 | float mVelocity = 0;
291 |
292 | // Internal tracking for value.
293 | float mValue = UNSET;
294 |
295 | // Tracks whether start value is set. If not, the animation will obtain the value at the time
296 | // of starting through the getter and use that as the starting value of the animation.
297 | boolean mStartValueIsSet = false;
298 |
299 | // View target to be animated.
300 | final Object mTarget;
301 |
302 | // View property id.
303 | final Property mViewProperty;
304 |
305 | // Package private tracking of animation lifecycle state. Visible to subclass animations.
306 | boolean mRunning = false;
307 |
308 | // Min and max values that defines the range of the animation values.
309 | float mMaxValue = Float.MAX_VALUE;
310 | float mMinValue = -mMaxValue;
311 |
312 | // Last frame time. Always gets reset to -1 at the end of the animation.
313 | private long mLastFrameTime = 0;
314 |
315 | // List of end listeners
316 | private final ArrayList mEndListeners = new ArrayList<>();
317 |
318 | // List of update listeners
319 | private final ArrayList mUpdateListeners = new ArrayList<>();
320 |
321 | /**
322 | * Creates a dynamic animation to animate the given property for the given {@link View}
323 | *
324 | * @param view the View whose property is to be animated
325 | * @param property the property to be animated
326 | */
327 | DynamicAnimation(T view, Property property) {
328 | mTarget = view;
329 | mViewProperty = property;
330 | }
331 |
332 | /**
333 | * Sets the start value of the animation. If start value is not set, the animation will get
334 | * the current value for the view's property, and use that as the start value.
335 | *
336 | * @param startValue start value for the animation
337 | * @return the Animation whose start value is being set
338 | */
339 | public T setStartValue(float startValue) {
340 | mValue = startValue;
341 | mStartValueIsSet = true;
342 | return (T) this;
343 | }
344 |
345 | /**
346 | * Start velocity of the animation. Default velocity is 0. Unit: pixel/second
347 | *
348 | * Note when using a fixed value as the start velocity (as opposed to getting the velocity
349 | * through touch events), it is recommended to define such a value in dp/second and convert it
350 | * to pixel/second based on the density of the screen to achieve a consistent look across
351 | * different screens.
352 | *
353 | * @param startVelocity start velocity of the animation in pixel/second
354 | * @return the Animation whose start velocity is being set
355 | */
356 | public T setStartVelocity(float startVelocity) {
357 | mVelocity = startVelocity;
358 | return (T) this;
359 | }
360 |
361 | /**
362 | * Sets the max value of the animation. Animations will not animate beyond their max value.
363 | * Whether or not animation will come to an end when max value is reached is dependent on the
364 | * child animation's implementation.
365 | *
366 | * @param max maximum value of the property to be animated
367 | * @return the Animation whose max value is being set
368 | */
369 | public T setMaxValue(float max) {
370 | // This max value should be checked and handled in the subclass animations, instead of
371 | // assuming the end of the animations when the max/min value is hit in the base class.
372 | // The reason is that hitting max/min value may just be a transient state, such as during
373 | // the spring oscillation.
374 | mMaxValue = max;
375 | return (T) this;
376 | }
377 |
378 | /**
379 | * Sets the min value of the animation. Animations will not animate beyond their min value.
380 | * Whether or not animation will come to an end when min value is reached is dependent on the
381 | * child animation's implementation.
382 | *
383 | * @param min minimum value of the property to be animated
384 | * @return the Animation whose min value is being set
385 | */
386 | public T setMinValue(float min) {
387 | mMinValue = min;
388 | return (T) this;
389 | }
390 |
391 | /**
392 | * Adds an end listener to the animation for receiving onAnimationEnd callbacks. If the listener
393 | * is {@code null} or has already been added to the list of listeners for the animation, no op.
394 | *
395 | * @param listener the listener to be added
396 | * @return the animation to which the listener is added
397 | */
398 | public T addEndListener(OnAnimationEndListener listener) {
399 | if (!mEndListeners.contains(listener)) {
400 | mEndListeners.add(listener);
401 | }
402 | return (T) this;
403 | }
404 |
405 | /**
406 | * Removes the end listener from the animation, so as to stop receiving animation end callbacks.
407 | *
408 | * @param listener the listener to be removed
409 | */
410 | public void removeEndListener(OnAnimationEndListener listener) {
411 | removeEntry(mEndListeners, listener);
412 | }
413 |
414 | /**
415 | * Adds an update listener to the animation for receiving per-frame animation update callbacks.
416 | * If the listener is {@code null} or has already been added to the list of listeners for the
417 | * animation, no op.
418 | *
419 | *
Note that update listener should only be added before the start of the animation.
420 | *
421 | * @param listener the listener to be added
422 | * @return the animation to which the listener is added
423 | * @throws UnsupportedOperationException if the update listener is added after the animation has
424 | * started
425 | */
426 | public T addUpdateListener(OnAnimationUpdateListener listener) {
427 | if (isRunning()) {
428 | // Require update listener to be added before the animation, such as when we start
429 | // the animation, we know whether the animation is RenderThread compatible.
430 | throw new UnsupportedOperationException("Error: Update listeners must be added before"
431 | + "the animation.");
432 | }
433 | if (!mUpdateListeners.contains(listener)) {
434 | mUpdateListeners.add(listener);
435 | }
436 | return (T) this;
437 | }
438 |
439 | /**
440 | * Removes the update listener from the animation, so as to stop receiving animation update
441 | * callbacks.
442 | *
443 | * @param listener the listener to be removed
444 | */
445 | public void removeUpdateListener(OnAnimationUpdateListener listener) {
446 | removeEntry(mUpdateListeners, listener);
447 | }
448 |
449 | /**
450 | * Remove {@code null} entries from the list.
451 | */
452 | private static void removeNullEntries(ArrayList list) {
453 | // Clean up null entries
454 | for (int i = list.size() - 1; i >= 0; i--) {
455 | if (list.get(i) == null) {
456 | list.remove(i);
457 | }
458 | }
459 | }
460 |
461 | /**
462 | * Remove an entry from the list by marking it {@code null} and clean up later.
463 | */
464 | private static void removeEntry(ArrayList list, T entry) {
465 | int id = list.indexOf(entry);
466 | if (id >= 0) {
467 | list.set(id, null);
468 | }
469 | }
470 |
471 | /****************Animation Lifecycle Management***************/
472 |
473 | /**
474 | * Starts an animation. If the animation has already been started, no op. Note that calling
475 | * {@link #start()} will not immediately set the property value to start value of the animation.
476 | * The property values will be changed at each animation pulse, which happens before the draw
477 | * pass. As a result, the changes will be reflected in the next frame, the same as if the values
478 | * were set immediately. This method should only be called on main thread.
479 | *
480 | * @throws AndroidRuntimeException if this method is not called on the main thread
481 | */
482 | public void start() {
483 | if (Looper.myLooper() != Looper.getMainLooper()) {
484 | throw new AndroidRuntimeException("Animations may only be started on the main thread");
485 | }
486 | if (!mRunning) {
487 | startAnimationInternal();
488 | }
489 | }
490 |
491 | /**
492 | * Cancels the on-going animation. If the animation hasn't started, no op. Note that this method
493 | * should only be called on main thread.
494 | *
495 | * @throws AndroidRuntimeException if this method is not called on the main thread
496 | */
497 | public void cancel() {
498 | if (Looper.myLooper() != Looper.getMainLooper()) {
499 | throw new AndroidRuntimeException("Animations may only be canceled on the main thread");
500 | }
501 | if (mRunning) {
502 | endAnimationInternal(true);
503 | }
504 | }
505 |
506 | /**
507 | * Returns whether the animation is currently running.
508 | *
509 | * @return {@code true} if the animation is currently running, {@code false} otherwise
510 | */
511 | public boolean isRunning() {
512 | return mRunning;
513 | }
514 |
515 | /************************** Private APIs below ********************************/
516 |
517 | // This gets called when the animation is started, to finish the setup of the animation
518 | // before the animation pulsing starts.
519 | private void startAnimationInternal() {
520 | if (!mRunning) {
521 | mRunning = true;
522 | if (!mStartValueIsSet) {
523 | mValue = getPropertyValue();
524 | }
525 | // Sanity check:
526 | if (mValue > mMaxValue || mValue < mMinValue) {
527 | throw new IllegalArgumentException("Starting value need to be in between min"
528 | + " value and max value");
529 | }
530 | AnimationHandler.getInstance().addAnimationFrameCallback(this, 0);
531 | }
532 | }
533 |
534 | /**
535 | * This gets call on each frame of the animation. Animation value and velocity are updated
536 | * in this method based on the new frame time. The property value of the view being animated
537 | * is then updated. The animation's ending conditions are also checked in this method. Once
538 | * the animation reaches equilibrium, the animation will come to its end, and end listeners
539 | * will be notified, if any.
540 | *
541 | * @hide
542 | */
543 | @Override
544 | public boolean doAnimationFrame(long frameTime) {
545 | if (mLastFrameTime == 0) {
546 | // First frame.
547 | mLastFrameTime = frameTime;
548 | if (mStartValueIsSet) {
549 | setPropertyValue(mValue);
550 | }
551 | return false;
552 | }
553 | long deltaT = frameTime - mLastFrameTime;
554 | mLastFrameTime = frameTime;
555 | boolean finished = updateValueAndVelocity(deltaT);
556 | // Clamp value & velocity.
557 | mValue = Math.min(mValue, mMaxValue);
558 | mValue = Math.max(mValue, mMinValue);
559 |
560 | setPropertyValue(mValue);
561 |
562 | if (finished) {
563 | endAnimationInternal(false);
564 | }
565 | return finished;
566 | }
567 |
568 | /**
569 | * Updates the animation state (i.e. value and velocity). This method is package private, so
570 | * subclasses can override this method to calculate the new value and velocity in their custom
571 | * way.
572 | *
573 | * @param deltaT time elapsed in millisecond since last frame
574 | * @return whether the animation has finished
575 | */
576 | boolean updateValueAndVelocity(long deltaT) {
577 | if (deltaT < 0) {
578 | throw new UnsupportedOperationException("Cannot play animation backwards");
579 | }
580 | if (deltaT == 0) {
581 | return false;
582 | }
583 |
584 | // Break down the deltaT into 4ms intervals.
585 | long increment = Math.min(4, deltaT);
586 |
587 | int totalT = (int) deltaT;
588 | int i = 0;
589 | float velocity = mVelocity;
590 | float value = mValue;
591 | for (i = 0; i <= totalT; i += increment) {
592 | float acceleration = getAcceleration(value, velocity);
593 | float newVelocity = acceleration * increment / 1000 + velocity;
594 | value += (velocity + newVelocity) / 2 * increment / 1000;
595 | velocity = newVelocity;
596 | if (i == totalT) {
597 | break;
598 | } else if (i + increment > deltaT) {
599 | increment = totalT - i;
600 | }
601 | }
602 |
603 | mVelocity = velocity;
604 | mValue = value;
605 |
606 | // TODO: need to update values to end value if true, otherwise there'll be precision loss.
607 | return isAtEquilibrium(mValue, mVelocity);
608 | }
609 |
610 | /**
611 | * Internal method to reset the animation states when animation is finished/canceled.
612 | */
613 | private void endAnimationInternal(boolean canceled) {
614 | mRunning = false;
615 | AnimationHandler.getInstance().removeCallback(this);
616 | mLastFrameTime = 0;
617 | mStartValueIsSet = false;
618 | for (int i = 0; i < mEndListeners.size(); i++) {
619 | if (mEndListeners.get(i) != null) {
620 | mEndListeners.get(i).onAnimationEnd(this, canceled, mValue, mVelocity);
621 | }
622 | }
623 | removeNullEntries(mEndListeners);
624 | }
625 |
626 | /**
627 | * Returns whether z and translationZ are supported on the current build version.
628 | */
629 | private static boolean isZSupported() {
630 | return Build.VERSION.SDK_INT >= 21;
631 | }
632 |
633 | /**
634 | * Updates the property value through the corresponding setter.
635 | */
636 | void setPropertyValue(float value) {
637 | mViewProperty.setValue(mTarget, value);
638 | for (int i = 0; i < mUpdateListeners.size(); i++) {
639 | if (mUpdateListeners.get(i) != null) {
640 | mUpdateListeners.get(i).onAnimationUpdate(this, mValue, mVelocity);
641 | }
642 | }
643 | removeNullEntries(mUpdateListeners);
644 | }
645 |
646 | /**
647 | * Obtain the property value through the corresponding getter.
648 | */
649 | private float getPropertyValue() {
650 | return mViewProperty.getValue(mTarget);
651 | }
652 |
653 | /****************Sub class animations**************/
654 | /**
655 | * Returns the acceleration at the given value with the given velocity.
656 | **/
657 | abstract float getAcceleration(float value, float velocity);
658 |
659 | /**
660 | * Returns whether the animation has reached equilibrium.
661 | */
662 | abstract boolean isAtEquilibrium(float value, float velocity);
663 |
664 | /**
665 | * An animation listener that receives end notifications from an animation.
666 | */
667 | public interface OnAnimationEndListener {
668 | /**
669 | * Notifies the end of an animation. Note that this callback will be invoked not only when
670 | * an animation reach equilibrium, but also when the animation is canceled.
671 | *
672 | * @param animation animation that has ended or was canceled
673 | * @param canceled whether the animation has been canceled
674 | * @param value the final value when the animation stopped
675 | * @param velocity the final velocity when the animation stopped
676 | */
677 | void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
678 | float velocity);
679 | }
680 |
681 | /**
682 | * Implementors of this interface can add themselves as update listeners
683 | * to an DynamicAnimation instance to receive callbacks on every animation
684 | * frame, after the current frame's values have been calculated for that
685 | * DynamicAnimation.
686 | */
687 | public interface OnAnimationUpdateListener {
688 |
689 | /**
690 | * Notifies the occurrence of another frame of the animation.
691 | *
692 | * @param animation animation that the update listener is added to
693 | * @param value the current value of the animation
694 | * @param velocity the current velocity of the animation
695 | */
696 | void onAnimationUpdate(DynamicAnimation animation, float value, float velocity);
697 | }
698 | }
699 |
--------------------------------------------------------------------------------