├── circualreveal ├── .gitignore ├── gradle.properties ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── io │ │ └── codetail │ │ ├── animation │ │ ├── RevealViewGroup.java │ │ ├── ViewAnimationUtils.java │ │ └── ViewRevealManager.java │ │ └── widget │ │ ├── RevealLinearLayout.java │ │ └── RevealFrameLayout.java ├── proguard-rules.pro ├── build.gradle └── gradle_mvn_push.gradle ├── jitpack.yml ├── spring-revealmanager ├── .gitignore ├── src │ └── main │ │ ├── res │ │ └── values │ │ │ └── strings.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── io │ │ └── codetail │ │ └── animation │ │ ├── Force.java │ │ ├── DynamicAnimator.java │ │ ├── SpringViewAnimatorManager.java │ │ ├── AnimationHandler.java │ │ ├── SpringAnimation.java │ │ ├── SpringForce.java │ │ └── DynamicAnimation.java ├── build.gradle └── proguard-rules.pro ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── app ├── src │ └── main │ │ ├── res │ │ ├── drawable-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_refresh_white_24dp.png │ │ ├── drawable-mdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_refresh_white_24dp.png │ │ │ └── floatingactionbutton_shadow_layer.png │ │ ├── drawable-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_refresh_white_24dp.png │ │ ├── drawable-nodpi │ │ │ └── example_raw_image.jpg │ │ ├── drawable │ │ │ ├── image_borders.xml │ │ │ ├── ic_looks_one_black_24dp.xml │ │ │ └── ic_looks_two_black_24dp.xml │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── dimens.xml │ │ │ └── styles.xml │ │ ├── values-w820dp │ │ │ └── dimens.xml │ │ ├── color │ │ │ └── overlay_color.xml │ │ ├── menu │ │ │ └── menu_main.xml │ │ └── layout │ │ │ ├── card_item.xml │ │ │ ├── activity_sample_3.xml │ │ │ ├── activity_sample_2.xml │ │ │ └── activity_main.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── io │ │ └── codetail │ │ └── circualrevealsample │ │ ├── PathEvaluator.java │ │ ├── AnimatorPath.java │ │ ├── PathPoint.java │ │ ├── SpringSettingsBottomDialog.java │ │ ├── RadialTransformationActivity.java │ │ └── MainActivity.java ├── proguard-rules.pro └── build.gradle ├── .gitignore ├── LICENSE ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /circualreveal/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - oraclejdk8 3 | -------------------------------------------------------------------------------- /spring-revealmanager/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':circualreveal', ':spring-revealmanager' 2 | -------------------------------------------------------------------------------- /circualreveal/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=com.ozodrukh 2 | POM_ARTIFACT_ID=circularreveal 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozodrukh/CircularReveal/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /circualreveal/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /spring-revealmanager/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | spring-revealmanager 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozodrukh/CircularReveal/HEAD/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozodrukh/CircularReveal/HEAD/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozodrukh/CircularReveal/HEAD/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozodrukh/CircularReveal/HEAD/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/example_raw_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozodrukh/CircularReveal/HEAD/app/src/main/res/drawable-nodpi/example_raw_image.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozodrukh/CircularReveal/HEAD/app/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozodrukh/CircularReveal/HEAD/app/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozodrukh/CircularReveal/HEAD/app/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/floatingactionbutton_shadow_layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozodrukh/CircularReveal/HEAD/app/src/main/res/drawable-xhdpi/floatingactionbutton_shadow_layer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/image_borders.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen 3 | .project 4 | .classpath 5 | .settings 6 | .idea 7 | *.iml 8 | *.ipr 9 | *.iws 10 | out 11 | target 12 | release.properties 13 | pom.xml.* 14 | build.xml 15 | local.properties 16 | proguard.cfg 17 | .DS_Store 18 | .gradle 19 | build 20 | app/build -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CircualRevealSample 5 | Hello world! 6 | Settings 7 | 8 | 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 15 10:12:15 UZT 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /spring-revealmanager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_looks_one_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/color/overlay_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 96dp 6 | 96dp 7 | 16dp 8 | 220dp 9 | 220dp 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_looks_two_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /circualreveal/src/main/java/io/codetail/animation/RevealViewGroup.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.view.ViewGroup; 4 | 5 | /** 6 | * Indicator for internal API that {@link ViewGroup} support 7 | * Circular Reveal animation 8 | */ 9 | public interface RevealViewGroup { 10 | 11 | /** 12 | * @return Bridge between view and circular reveal animation 13 | */ 14 | ViewRevealManager getViewRevealManager(); 15 | 16 | /** 17 | * 18 | * @param manager 19 | */ 20 | void setViewRevealManager(ViewRevealManager manager); 21 | } -------------------------------------------------------------------------------- /spring-revealmanager/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion project.compileSDKVersion 5 | buildToolsVersion project.buildToolsVersion 6 | 7 | defaultConfig { 8 | minSdkVersion project.minSDKVersion 9 | targetSdkVersion project.targetSDKVersion 10 | } 11 | 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | } 19 | 20 | dependencies { 21 | compile project(':circualreveal') 22 | compile "com.android.support:support-core-utils:$support_lib_version" 23 | } 24 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/Ozodrukh/Documents/Android/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /circualreveal/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/Ozodrukh/Documents/Android/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/card_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 23 | 24 | -------------------------------------------------------------------------------- /spring-revealmanager/src/main/java/io/codetail/animation/Force.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 | /** 20 | * Hide this for now, in case we want to change the API. 21 | */ 22 | interface Force { 23 | // Acceleration based on position. 24 | float getAcceleration(float position, float velocity); 25 | 26 | boolean isAtEquilibrium(float value, float velocity); 27 | } 28 | -------------------------------------------------------------------------------- /circualreveal/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven' 3 | 4 | group = 'com.github.ozodrukh' 5 | 6 | android { 7 | compileSdkVersion project.compileSDKVersion 8 | buildToolsVersion project.buildToolsVersion 9 | 10 | defaultConfig { 11 | minSdkVersion project.minSDKVersion 12 | targetSdkVersion project.targetSDKVersion 13 | } 14 | } 15 | 16 | // build a jar with source files 17 | task sourcesJar(type: Jar) { 18 | from android.sourceSets.main.java.srcDirs 19 | classifier = 'sources' 20 | } 21 | 22 | task javadoc(type: Javadoc) { 23 | failOnError false 24 | source = android.sourceSets.main.java.sourceFiles 25 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 26 | classpath += configurations.compile 27 | } 28 | 29 | // build a jar with javadoc 30 | task javadocJar(type: Jar, dependsOn: javadoc) { 31 | classifier = 'javadoc' 32 | from javadoc.destinationDir 33 | } 34 | 35 | artifacts { 36 | archives sourcesJar 37 | archives javadocJar 38 | } 39 | -------------------------------------------------------------------------------- /spring-revealmanager/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/Ozodrukh/Documents/Android/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ozodrukh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'com.neenbedankt.android-apt' 3 | 4 | android { 5 | compileSdkVersion project.compileSDKVersion 6 | buildToolsVersion project.buildToolsVersion 7 | 8 | defaultConfig { 9 | minSdkVersion project.minSDKVersion 10 | targetSdkVersion project.targetSDKVersion 11 | versionCode 1 12 | versionName "1.0" 13 | applicationId "io.codetail.circualrevealsample" 14 | 15 | vectorDrawables { 16 | useSupportLibrary = true 17 | } 18 | } 19 | 20 | lintOptions { 21 | abortOnError false 22 | } 23 | } 24 | 25 | dependencies { 26 | compile fileTree(dir: 'libs', include: ['*.jar']) 27 | compile project(':circualreveal') 28 | compile project(':spring-revealmanager') 29 | 30 | compile "com.android.support:appcompat-v7:$support_lib_version" 31 | compile "com.android.support:recyclerview-v7:$support_lib_version" 32 | compile "com.android.support:cardview-v7:$support_lib_version" 33 | compile "com.android.support:design:$support_lib_version" 34 | compile "com.android.support:support-dynamic-animation:$support_lib_version" 35 | 36 | compile 'com.squareup.picasso:picasso:2.5.2' 37 | compile 'com.jakewharton:butterknife:8.0.1' 38 | apt 'com.jakewharton:butterknife-compiler:8.0.1' 39 | } 40 | -------------------------------------------------------------------------------- /spring-revealmanager/src/main/java/io/codetail/animation/DynamicAnimator.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.animation.ValueAnimator; 4 | 5 | /** 6 | * created at 3/15/17 7 | * 8 | * @author Ozodrukh 9 | * @version 1.0 10 | */ 11 | 12 | class DynamicAnimator> 13 | extends ValueAnimator implements DynamicAnimation.OnAnimationEndListener { 14 | private final DynamicAnimation animation; 15 | 16 | public DynamicAnimator(DynamicAnimation animation) { 17 | this.animation = animation; 18 | this.animation.addEndListener(this); 19 | } 20 | 21 | @Override public void start() { 22 | animation.start(); 23 | 24 | for (AnimatorListener listener : getListeners()) { 25 | listener.onAnimationStart(this); 26 | } 27 | } 28 | 29 | @Override public void cancel() { 30 | animation.cancel(); 31 | } 32 | 33 | @Override public boolean isRunning() { 34 | return animation.isRunning(); 35 | } 36 | 37 | @Override public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, 38 | float velocity) { 39 | for (AnimatorListener listener : getListeners()) { 40 | if (canceled) { 41 | listener.onAnimationCancel(this); 42 | } else { 43 | listener.onAnimationEnd(this); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /circualreveal/src/main/java/io/codetail/widget/RevealLinearLayout.java: -------------------------------------------------------------------------------- 1 | package io.codetail.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.util.AttributeSet; 6 | import android.view.View; 7 | import android.widget.LinearLayout; 8 | import io.codetail.animation.RevealViewGroup; 9 | import io.codetail.animation.ViewRevealManager; 10 | 11 | public class RevealLinearLayout extends LinearLayout implements RevealViewGroup { 12 | private ViewRevealManager manager; 13 | 14 | public RevealLinearLayout(Context context) { 15 | this(context, null); 16 | } 17 | 18 | public RevealLinearLayout(Context context, AttributeSet attrs) { 19 | this(context, attrs, 0); 20 | } 21 | 22 | public RevealLinearLayout(Context context, AttributeSet attrs, int defStyle) { 23 | super(context, attrs); 24 | manager = new ViewRevealManager(); 25 | } 26 | 27 | @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 28 | try { 29 | canvas.save(); 30 | 31 | manager.transform(canvas, child); 32 | return super.drawChild(canvas, child, drawingTime); 33 | } finally { 34 | canvas.restore(); 35 | } 36 | } 37 | 38 | public void setViewRevealManager(ViewRevealManager manager) { 39 | if (manager == null) { 40 | throw new NullPointerException("ViewRevealManager is null"); 41 | } 42 | 43 | this.manager = manager; 44 | } 45 | 46 | @Override public ViewRevealManager getViewRevealManager() { 47 | return manager; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /circualreveal/src/main/java/io/codetail/widget/RevealFrameLayout.java: -------------------------------------------------------------------------------- 1 | package io.codetail.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.util.AttributeSet; 6 | import android.view.View; 7 | import android.widget.FrameLayout; 8 | import io.codetail.animation.RevealViewGroup; 9 | import io.codetail.animation.ViewRevealManager; 10 | 11 | public class RevealFrameLayout extends FrameLayout implements RevealViewGroup { 12 | private ViewRevealManager manager; 13 | 14 | public RevealFrameLayout(Context context) { 15 | this(context, null); 16 | } 17 | 18 | public RevealFrameLayout(Context context, AttributeSet attrs) { 19 | this(context, attrs, 0); 20 | } 21 | 22 | public RevealFrameLayout(Context context, AttributeSet attrs, int defStyle) { 23 | super(context, attrs, defStyle); 24 | 25 | manager = new ViewRevealManager(); 26 | } 27 | 28 | @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 29 | try { 30 | canvas.save(); 31 | return manager.transform(canvas, child) 32 | & super.drawChild(canvas, child, drawingTime); 33 | } finally { 34 | canvas.restore(); 35 | } 36 | } 37 | 38 | public void setViewRevealManager(ViewRevealManager manager) { 39 | if (manager == null) { 40 | throw new NullPointerException("ViewRevealManager is null"); 41 | } 42 | 43 | this.manager = manager; 44 | } 45 | 46 | @Override public ViewRevealManager getViewRevealManager() { 47 | return manager; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | org.gradle.parallel=true 19 | org.gradle.daemon=true 20 | #### 21 | # Global Application Enviorenment Properties 22 | ### 23 | # Android SDK Properties 24 | MIN_SDK=21 25 | TARGET_SDK=24 26 | COMPILE_SDK=24 27 | BUILD_TOOLS_VERSION=25.0.2 28 | # Support Library 29 | SUPPORT_LIB_VERSION=25.3.0 30 | # Repository Data 31 | VERSION_NAME=1.3.0 32 | VERSION_CODE=3 33 | 34 | GROUP=com.github.ozodrukh 35 | 36 | POM_DESCRIPTION=Port of Lollipop circular reveal animation 37 | POM_URL=https://github.com/ozodrukh/CircularReveal 38 | POM_SCM_URL=https://github.com/ozodrukh/CircularReveal 39 | POM_SCM_CONNECTION=scm:git@github.com:ozodrukh/CircularReveal.git 40 | POM_SCM_DEV_CONNECTION=scm:git@github.com:ozodrukh/CircularReveal.git 41 | POM_LICENCE_NAME=The MIT License (MIT) 42 | POM_LICENCE_URL=http://opensource.org/licenses/MIT 43 | POM_LICENCE_DIST=repo 44 | POM_DEVELOPER_ID=ozodrukh 45 | POM_DEVELOPER_NAME=Abdullaev Ozodrukh 46 | POM_DEVELOPER_EMAIL=ozik.dev@gmail.com -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_sample_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | 22 | 23 | 29 | 30 | 36 | 37 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/io/codetail/circualrevealsample/PathEvaluator.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 | import android.animation.TypeEvaluator; 19 | 20 | /** 21 | * This evaluator interpolates between two PathPoint values given the value t, the 22 | * proportion traveled between those points. The value of the interpolation depends 23 | * on the operation specified by the endValue (the operation for the interval between 24 | * PathPoints is always specified by the end point of that interval). 25 | */ 26 | public class PathEvaluator implements TypeEvaluator { 27 | @Override public PathPoint evaluate(float t, PathPoint startValue, PathPoint endValue) { 28 | float x, y; 29 | if (endValue.mOperation == PathPoint.CURVE) { 30 | float oneMinusT = 1 - t; 31 | x = oneMinusT * oneMinusT * oneMinusT * startValue.mX + 32 | 3 * oneMinusT * oneMinusT * t * endValue.mControl0X + 33 | 3 * oneMinusT * t * t * endValue.mControl1X + 34 | t * t * t * endValue.mX; 35 | y = oneMinusT * oneMinusT * oneMinusT * startValue.mY + 36 | 3 * oneMinusT * oneMinusT * t * endValue.mControl0Y + 37 | 3 * oneMinusT * t * t * endValue.mControl1Y + 38 | t * t * t * endValue.mY; 39 | } else if (endValue.mOperation == PathPoint.LINE) { 40 | x = startValue.mX + t * (endValue.mX - startValue.mX); 41 | y = startValue.mY + t * (endValue.mY - startValue.mY); 42 | } else { 43 | x = endValue.mX; 44 | y = endValue.mY; 45 | } 46 | return PathPoint.moveTo(x, y); 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_sample_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 22 | 23 | 29 | 30 | 36 | 37 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/java/io/codetail/circualrevealsample/AnimatorPath.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 | import java.util.ArrayList; 19 | import java.util.Collection; 20 | 21 | /** 22 | * A simple Path object that holds information about the points along 23 | * a path. The API allows you to specify a move location (which essentially 24 | * jumps from the previous point in the path to the new one), a line location 25 | * (which creates a line segment from the previous location) and a curve 26 | * location (which creates a cubic B�zier curve from the previous location). 27 | */ 28 | public class AnimatorPath { 29 | 30 | // The points in the path 31 | ArrayList mPoints = new ArrayList(); 32 | 33 | /** 34 | * Move from the current path point to the new one 35 | * specified by x and y. This will create a discontinuity if this point is 36 | * neither the first point in the path nor the same as the previous point 37 | * in the path. 38 | */ 39 | public void moveTo(float x, float y) { 40 | mPoints.add(PathPoint.moveTo(x, y)); 41 | } 42 | 43 | /** 44 | * Create a straight line from the current path point to the new one 45 | * specified by x and y. 46 | */ 47 | public void lineTo(float x, float y) { 48 | mPoints.add(PathPoint.lineTo(x, y)); 49 | } 50 | 51 | /** 52 | * Create a cubic B�zier curve from the current path point to the new one 53 | * specified by x and y. The curve uses the current path location as the first anchor 54 | * point, the control points (c0X, c0Y) and (c1X, c1Y), and (x, y) as the end anchor point. 55 | */ 56 | public void curveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y) { 57 | mPoints.add(PathPoint.curveTo(c0X, c0Y, c1X, c1Y, x, y)); 58 | } 59 | 60 | /** 61 | * Returns a Collection of PathPoint objects that describe all points in the path. 62 | */ 63 | public Collection 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/v/Ozodrukh/CircularReveal.svg)](https://www.jitpack.io/#Ozodrukh/CircularReveal) 2 | 3 | CircularReveal 4 | ============== 5 | 6 | Lollipop ViewAnimationUtils.createCircularReveal for everyone 14+ 7 | 8 | Yotube Video
Circular Reveal
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 | --------------------------------------------------------------------------------