├── app ├── .gitignore ├── src │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── res │ │ │ ├── drawable-hdpi │ │ │ │ └── ic_magic.png │ │ │ ├── drawable-xhdpi │ │ │ │ └── ic_magic.png │ │ │ ├── drawable-xxhdpi │ │ │ │ └── ic_magic.png │ │ │ ├── drawable-xxxhdpi │ │ │ │ └── ic_magic.png │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── drawable-nodpi │ │ │ │ └── illustration.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── menu │ │ │ │ └── activity_main.xml │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── drawable │ │ │ │ ├── ic_check_black_16dp.xml │ │ │ │ ├── ic_mode_edit_black_16dp.xml │ │ │ │ └── il_comments.xml │ │ │ └── layout │ │ │ │ ├── activity_adjustment.xml │ │ │ │ ├── layout_toolbar.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── item_simple.xml │ │ │ │ ├── fragment_square.xml │ │ │ │ └── combo_slider.xml │ │ ├── java │ │ │ └── me │ │ │ │ └── cyandev │ │ │ │ └── springanimatordemo │ │ │ │ ├── util │ │ │ │ └── SimpleAnimatorListener.java │ │ │ │ ├── preview │ │ │ │ ├── BasePreviewFragment.java │ │ │ │ ├── RecyclerViewPreviewFragment.java │ │ │ │ └── TranslationPreviewFragment.java │ │ │ │ ├── adjustment │ │ │ │ ├── Rk4AdjustmentFragment.java │ │ │ │ ├── DhoAdjustmentFragment.java │ │ │ │ ├── BaseAdjustmentFragment.java │ │ │ │ └── view │ │ │ │ │ └── ComboSliderView.java │ │ │ │ ├── ConfigurationResolver.java │ │ │ │ └── MainActivity.java │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── me │ │ │ └── cyandev │ │ │ └── springanimator │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── me │ │ └── cyandev │ │ └── springanimator │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle.kts ├── library ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── me │ │ └── cyandev │ │ └── springanimator │ │ ├── DhoSpringAnimator.java │ │ ├── Rk4SpringAnimator.java │ │ ├── internal │ │ └── AnimationHandler.java │ │ └── AbsSpringAnimator.java ├── proguard-rules.pro └── build.gradle.kts ├── .gitattributes ├── settings.gradle.kts ├── art ├── measure │ ├── links │ │ ├── page-1-active.html │ │ ├── page-1-input.html │ │ └── page-1-main.html │ └── preview │ │ ├── page-1-input.png │ │ ├── page-1-main.png │ │ └── page-1-active.png ├── screencast.gif └── spring_animator.sketch ├── .idea ├── copyright │ ├── profiles_settings.xml │ └── Apache.xml ├── markdown-navigator │ └── profiles_settings.xml ├── encodings.xml ├── statistic.xml ├── modules.xml ├── runConfigurations.xml ├── compiler.xml ├── gradle.xml ├── misc.xml └── markdown-navigator.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── .travis.yml ├── gradle.properties ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE.txt /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-language=Java 2 | 3 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":app") 2 | include(":library") 3 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /art/measure/links/page-1-active.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /art/measure/links/page-1-input.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /art/measure/links/page-1-main.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /art/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/art/screencast.gif -------------------------------------------------------------------------------- /art/spring_animator.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/art/spring_animator.sketch -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /art/measure/preview/page-1-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/art/measure/preview/page-1-input.png -------------------------------------------------------------------------------- /art/measure/preview/page-1-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/art/measure/preview/page-1-main.png -------------------------------------------------------------------------------- /art/measure/preview/page-1-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/art/measure/preview/page-1-active.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/drawable-hdpi/ic_magic.png -------------------------------------------------------------------------------- /.idea/markdown-navigator/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/drawable-xhdpi/ic_magic.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/drawable-xxhdpi/ic_magic.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/drawable-xxxhdpi/ic_magic.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/drawable-nodpi/illustration.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/android-SpringAnimator/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 18 09:36:29 CST 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-7.4.2-bin.zip -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | sudo: required 3 | dist: precise 4 | 5 | android: 6 | components: 7 | - tools 8 | - platform-tools 9 | - build-tools-26.0.0 10 | - android-25 11 | - extra-android-m2repository 12 | 13 | jdk: 14 | - oraclejdk8 15 | 16 | notifications: 17 | email: false 18 | 19 | script: 20 | - ./gradlew assemble check 21 | -------------------------------------------------------------------------------- /.idea/statistic.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/test/java/me/cyandev/springanimator/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package me.cyandev.springanimator; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/Apache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/menu/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | android.enableJetifier=true 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/androidTest/java/me/cyandev/springanimator/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package me.cyandev.springanimator; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("me.cyandev.springanimator", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | #FF212121 20 | #FF000000 21 | #FF009688 22 | 23 | #FFBDBDBD 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | SpringPlayground 19 | Adjustments 20 | You can open this activity in multi-window mode 21 | Drag me! 22 | 23 | -------------------------------------------------------------------------------- /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/cyandev/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle.kts. 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 | -------------------------------------------------------------------------------- /library/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/cyandev/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle.kts. 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 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_black_16dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mode_edit_black_16dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/me/cyandev/springanimatordemo/util/SimpleAnimatorListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimatordemo.util; 18 | 19 | import android.animation.Animator; 20 | 21 | public abstract class SimpleAnimatorListener implements Animator.AnimatorListener { 22 | 23 | @Override 24 | public void onAnimationStart(Animator animation) { 25 | } 26 | 27 | @Override 28 | public void onAnimationEnd(Animator animation) { 29 | } 30 | 31 | @Override 32 | public void onAnimationCancel(Animator animation) { 33 | } 34 | 35 | @Override 36 | public void onAnimationRepeat(Animator animation) { 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_adjustment.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 23 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 | plugins { 18 | id("com.android.application") 19 | } 20 | 21 | android { 22 | compileSdk = 33 23 | defaultConfig { 24 | applicationId = "me.cyandev.springanimatordemo" 25 | minSdk = 16 26 | targetSdk = 33 27 | versionCode = 1 28 | versionName = "1.0" 29 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 30 | } 31 | 32 | buildTypes { 33 | getByName("release") { 34 | isMinifyEnabled = true 35 | proguardFiles("proguard-android.txt", "proguard-rules.pro") 36 | } 37 | } 38 | } 39 | 40 | dependencies { 41 | implementation(project(mapOf("path" to ":library"))) 42 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 43 | implementation("androidx.appcompat:appcompat:1.6.1") 44 | implementation("com.google.android.material:material:1.9.0") 45 | implementation("androidx.cardview:cardview:1.0.0") 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.ByteArrayOutputStream 2 | plugins { 3 | id("com.android.library") 4 | id("maven-publish") 5 | } 6 | 7 | fun String.runCommand(currentWorkingDir: File = file("./")): String { 8 | val byteOut = ByteArrayOutputStream() 9 | project.exec { 10 | workingDir = currentWorkingDir 11 | commandLine = this@runCommand.split("\\s".toRegex()) 12 | standardOutput = byteOut 13 | } 14 | return String(byteOut.toByteArray()).trim() 15 | } 16 | 17 | var gitVersionCode = 1 18 | try { 19 | gitVersionCode = Integer.parseInt("git rev-list HEAD --count".runCommand()) 20 | } catch (ignored: NumberFormatException) { 21 | println("WARN: no git commits yet") 22 | } 23 | 24 | var gitVersionName = "git tag --list".runCommand().split('\n').first() 25 | if (gitVersionName.isEmpty()) { 26 | gitVersionName = "1.0.0" 27 | } 28 | 29 | android { 30 | compileSdk = 33 31 | 32 | defaultConfig { 33 | minSdk = 16 34 | targetSdk = 33 35 | } 36 | buildTypes { 37 | getByName("release") { 38 | isMinifyEnabled = false 39 | proguardFiles("proguard-android.txt", "proguard-rules.pro") 40 | } 41 | } 42 | 43 | publishing { 44 | singleVariant("release") { 45 | withSourcesJar() 46 | withJavadocJar() 47 | } 48 | } 49 | } 50 | 51 | dependencies { 52 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 53 | implementation("androidx.appcompat:appcompat:1.6.1") 54 | } 55 | 56 | publishing { 57 | publications { 58 | register("release") { 59 | groupId = "me.cyandev" 60 | artifactId = "springanimator" 61 | version = gitVersionName 62 | 63 | afterEvaluate { 64 | from(components["release"]) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/me/cyandev/springanimatordemo/preview/BasePreviewFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimatordemo.preview; 18 | 19 | import android.content.Context; 20 | import android.os.Bundle; 21 | import androidx.fragment.app.Fragment; 22 | import android.view.LayoutInflater; 23 | import android.view.View; 24 | import android.view.ViewGroup; 25 | 26 | import me.cyandev.springanimator.AbsSpringAnimator; 27 | 28 | public abstract class BasePreviewFragment extends Fragment { 29 | 30 | private SpringAnimatorProvider mProvider; 31 | 32 | public abstract void onResetView(); 33 | 34 | public abstract void onStartAnimation(); 35 | 36 | @Override 37 | public void onAttach(Context context) { 38 | super.onAttach(context); 39 | 40 | if (getActivity() instanceof SpringAnimatorProvider) { 41 | mProvider = (SpringAnimatorProvider) getActivity(); 42 | } 43 | } 44 | 45 | @Override 46 | public abstract View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState); 47 | 48 | protected final AbsSpringAnimator createNewAnimator() { 49 | if (mProvider == null) { 50 | throw new IllegalStateException("Provider is not presented"); 51 | } 52 | 53 | return mProvider.provideAnimator(); 54 | } 55 | 56 | public interface SpringAnimatorProvider { 57 | AbsSpringAnimator provideAnimator(); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 16 | 17 | 21 | 22 | 29 | 30 | 40 | 41 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpringAnimator 2 | [![](https://travis-ci.org/unixzii/android-SpringAnimator.svg?branch=master)](https://travis-ci.org/unixzii/android-SpringAnimator) 3 |
A framer.js DHO and RK4 spring animation port for Android. 4 | 5 | ### Features 6 | * Extends from `Animator`, providing a familiar API to use 7 | * Provides **DHO** and **RK4** algorithm from Framer.js 8 | * Bundled playground app, fine-tuning made easy 9 | 10 | ## Demo 11 | Checkout the playground here: [playground.apk](https://github.com/unixzii/android-SpringAnimator/releases/download/0.1.0-alpha1/playground.apk) 12 | 13 | Screencast 14 | 15 | ## Requirements 16 | SpringAnimator requires API 16 or higher. 17 | 18 | ## Download 19 | Gradle: 20 | ```groovy 21 | repositories { 22 | jcenter() 23 | } 24 | 25 | dependencies { 26 | compile 'me.cyandev:springanimator:0.1.0-alpha1' 27 | } 28 | ``` 29 | 30 | Find out more releases [here](https://github.com/unixzii/android-SpringAnimator/releases). 31 | 32 | ## Get Started 33 | ```java 34 | DhoSpringAnimator animator = new DhoSpringAnimator(); 35 | animator.setStiffness(200); 36 | animator.setDamping(10); 37 | animator.setMass(1); 38 | animator.setVelocity(0); 39 | animator.addUpdateListener(new AbsSpringAnimator.AnimatorUpdateListener() { 40 | @Override 41 | public void onAnimationUpdate(AbsSpringAnimator animation) { 42 | // Do something cool here... 43 | } 44 | }); 45 | animator.start(); 46 | ``` 47 | 48 | Parameters are fully matched with Frame.js. 49 | 50 | ## Acknowledgement 51 | Thanks [koenbok](https://github.com/koenbok)/[Framer](https://github.com/koenbok/Framer) for providing original algorithms. 52 | 53 | Thanks [MartinRGB](https://github.com/MartinRGB) for designing user interface of the playground app and providing graphical resources. 54 | 55 | ## License 56 | ``` 57 | Copyright 2017 Cyandev 58 | 59 | Licensed under the Apache License, Version 2.0 (the "License"); 60 | you may not use this file except in compliance with the License. 61 | You may obtain a copy of the License at 62 | 63 | http://www.apache.org/licenses/LICENSE-2.0 64 | 65 | Unless required by applicable law or agreed to in writing, software 66 | distributed under the License is distributed on an "AS IS" BASIS, 67 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 68 | See the License for the specific language governing permissions and 69 | limitations under the License. 70 | ``` 71 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_simple.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 26 | 27 | 34 | 35 | 39 | 40 | 47 | 48 | 54 | 55 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/java/me/cyandev/springanimatordemo/adjustment/Rk4AdjustmentFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimatordemo.adjustment; 18 | 19 | import android.os.Bundle; 20 | 21 | import java.util.ArrayList; 22 | 23 | import me.cyandev.springanimator.AbsSpringAnimator; 24 | import me.cyandev.springanimator.Rk4SpringAnimator; 25 | import me.cyandev.springanimatordemo.ConfigurationResolver; 26 | 27 | public class Rk4AdjustmentFragment extends BaseAdjustmentFragment { 28 | 29 | @Override 30 | protected String getTypeName() { 31 | return "RK4"; 32 | } 33 | 34 | @Override 35 | protected ArrayList onCreateAdjustmentInfo() { 36 | int tension = 200; 37 | int friction = 10; 38 | int velocity = 0; 39 | 40 | Bundle args = getArguments(); 41 | if (args != null) { 42 | AbsSpringAnimator animator = ConfigurationResolver.resolveConfiguration(args); 43 | if (animator != null && animator instanceof Rk4SpringAnimator) { 44 | Rk4SpringAnimator rk4SpringAnimator = (Rk4SpringAnimator) animator; 45 | tension = (int) rk4SpringAnimator.getTension(); 46 | friction = (int) rk4SpringAnimator.getFriction(); 47 | velocity = (int) rk4SpringAnimator.getVelocity(); 48 | } 49 | } 50 | 51 | ArrayList result = new ArrayList<>(); 52 | 53 | result.add(new AdjustmentInfo("Tension", tension, 0, 1000)); 54 | result.add(new AdjustmentInfo("Friction", friction, 0, 100)); 55 | result.add(new AdjustmentInfo("Velocity", velocity, 0, 100)); 56 | 57 | return result; 58 | } 59 | 60 | @Override 61 | protected Bundle onCreateConfigurationBundle() { 62 | int tension = getAdjustmentValue(0); 63 | int friction = getAdjustmentValue(1); 64 | int velocity = getAdjustmentValue(2); 65 | 66 | return ConfigurationResolver.buildRk4Configuration(tension, friction, velocity); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /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/me/cyandev/springanimatordemo/adjustment/DhoAdjustmentFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimatordemo.adjustment; 18 | 19 | import android.os.Bundle; 20 | 21 | import java.util.ArrayList; 22 | 23 | import me.cyandev.springanimator.AbsSpringAnimator; 24 | import me.cyandev.springanimator.DhoSpringAnimator; 25 | import me.cyandev.springanimatordemo.ConfigurationResolver; 26 | 27 | public class DhoAdjustmentFragment extends BaseAdjustmentFragment { 28 | 29 | @Override 30 | protected String getTypeName() { 31 | return "DHO"; 32 | } 33 | 34 | @Override 35 | protected ArrayList onCreateAdjustmentInfo() { 36 | int stiffness = 200; 37 | int damping = 10; 38 | int mass = 1; 39 | int velocity = 0; 40 | 41 | Bundle args = getArguments(); 42 | if (args != null) { 43 | AbsSpringAnimator animator = ConfigurationResolver.resolveConfiguration(args); 44 | if (animator != null && animator instanceof DhoSpringAnimator) { 45 | DhoSpringAnimator dhoSpringAnimator = (DhoSpringAnimator) animator; 46 | stiffness = (int) dhoSpringAnimator.getStiffness(); 47 | damping = (int) dhoSpringAnimator.getDamping(); 48 | mass = (int) dhoSpringAnimator.getMass(); 49 | velocity = (int) dhoSpringAnimator.getVelocity(); 50 | } 51 | } 52 | 53 | ArrayList result = new ArrayList<>(); 54 | 55 | result.add(new AdjustmentInfo("Stiffness", stiffness, 0, 1000)); 56 | result.add(new AdjustmentInfo("Damping", damping, 0, 100)); 57 | result.add(new AdjustmentInfo("Mass", mass, 1, 20)); 58 | result.add(new AdjustmentInfo("Velocity", velocity, 0, 100)); 59 | 60 | return result; 61 | } 62 | 63 | @Override 64 | protected Bundle onCreateConfigurationBundle() { 65 | int stiffness = getAdjustmentValue(0); 66 | int damping = getAdjustmentValue(1); 67 | int mass = getAdjustmentValue(2); 68 | int velocity = getAdjustmentValue(3); 69 | 70 | return ConfigurationResolver.buildDhoConfiguration(stiffness, damping, mass, velocity); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_square.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 24 | 25 | 30 | 31 | 35 | 36 | 39 | 40 | 45 | 46 | 50 | 51 | 61 | 62 | 63 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/java/me/cyandev/springanimatordemo/ConfigurationResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimatordemo; 18 | 19 | import android.os.Bundle; 20 | 21 | import me.cyandev.springanimator.AbsSpringAnimator; 22 | import me.cyandev.springanimator.DhoSpringAnimator; 23 | import me.cyandev.springanimator.Rk4SpringAnimator; 24 | 25 | public final class ConfigurationResolver { 26 | 27 | private static final String KEY_TYPE = "type"; 28 | private static final String KEY_STIFFNESS = "stiffness"; 29 | private static final String KEY_DAMPING = "damping"; 30 | private static final String KEY_MASS = "mass"; 31 | private static final String KEY_VELOCITY = "velocity"; 32 | private static final String KEY_TENSION = "tension"; 33 | private static final String KEY_FRICTION = "friction"; 34 | 35 | private static final String TYPE_DHO = "DHO"; 36 | private static final String TYPE_RK4 = "RK4"; 37 | 38 | public static Bundle buildDhoConfiguration(int stiffness, int damping, int mass, int velocity) { 39 | Bundle bundle = new Bundle(); 40 | bundle.putString(KEY_TYPE, TYPE_DHO); 41 | bundle.putInt(KEY_STIFFNESS, stiffness); 42 | bundle.putInt(KEY_DAMPING, damping); 43 | bundle.putInt(KEY_MASS, mass); 44 | bundle.putInt(KEY_VELOCITY, velocity); 45 | 46 | return bundle; 47 | } 48 | 49 | public static Bundle buildRk4Configuration(int tension, int friction, int velocity) { 50 | Bundle bundle = new Bundle(); 51 | bundle.putString(KEY_TYPE, TYPE_RK4); 52 | bundle.putInt(KEY_TENSION, tension); 53 | bundle.putInt(KEY_FRICTION, friction); 54 | bundle.putInt(KEY_VELOCITY, velocity); 55 | 56 | return bundle; 57 | } 58 | 59 | public static AbsSpringAnimator resolveConfiguration(Bundle confBundle) { 60 | String type = confBundle.getString(KEY_TYPE, ""); 61 | 62 | if (TYPE_DHO.equals(type)) { 63 | DhoSpringAnimator animator = new DhoSpringAnimator(); 64 | animator.setStiffness(confBundle.getInt(KEY_STIFFNESS)); 65 | animator.setDamping(confBundle.getInt(KEY_DAMPING)); 66 | animator.setMass(confBundle.getInt(KEY_MASS)); 67 | animator.setVelocity(confBundle.getInt(KEY_VELOCITY)); 68 | 69 | return animator; 70 | } 71 | 72 | if (TYPE_RK4.equals(type)) { 73 | Rk4SpringAnimator animator = new Rk4SpringAnimator(); 74 | animator.setTension(confBundle.getInt(KEY_TENSION)); 75 | animator.setFriction(confBundle.getInt(KEY_FRICTION)); 76 | animator.setVelocity(confBundle.getInt(KEY_VELOCITY)); 77 | 78 | return animator; 79 | } 80 | 81 | return null; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/il_comments.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 23 | 24 | 31 | 38 | 41 | 48 | 55 | 56 | 59 | 66 | 73 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /library/src/main/java/me/cyandev/springanimator/DhoSpringAnimator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimator; 18 | 19 | /** 20 | * A spring animator using Dho algorithm. 21 | *
22 | * Reference: 23 | * See 24 | * https://github.com/koenbok/Framer/blob/master/framer/Animators/SpringDHOAnimator.coffee 25 | */ 26 | public class DhoSpringAnimator extends AbsSpringAnimator { 27 | 28 | private float mVelocity = 0; 29 | private float mTolerance = 1 / 10000.f; 30 | private float mStiffness = 120; 31 | private float mDamping = 5; 32 | private float mMass = 1; 33 | 34 | private boolean mIsFirstFrame = true; 35 | private float mValue = 0; 36 | private double mCurrentVelocity = 0; 37 | 38 | public float getVelocity() { 39 | return mVelocity; 40 | } 41 | 42 | public void setVelocity(float velocity) { 43 | mVelocity = velocity; 44 | } 45 | 46 | public float getTolerance() { 47 | return mTolerance; 48 | } 49 | 50 | public void setTolerance(float tolerance) { 51 | mTolerance = tolerance; 52 | } 53 | 54 | public float getStiffness() { 55 | return mStiffness; 56 | } 57 | 58 | public void setStiffness(float stiffness) { 59 | mStiffness = stiffness; 60 | } 61 | 62 | public float getDamping() { 63 | return mDamping; 64 | } 65 | 66 | public void setDamping(float damping) { 67 | mDamping = damping; 68 | } 69 | 70 | public float getMass() { 71 | return mMass; 72 | } 73 | 74 | public void setMass(float mass) { 75 | mMass = mass; 76 | } 77 | 78 | @Override 79 | protected long computeSettleDuration() { 80 | // TODO: Not implemented. 81 | return 0; 82 | } 83 | 84 | @Override 85 | protected void resetState() { 86 | mIsFirstFrame = true; 87 | mValue = 0; 88 | mCurrentVelocity = mVelocity; 89 | } 90 | 91 | @Override 92 | protected float enterFrame(long frameTime) { 93 | final float delta = Math.max(frameTime / 1000.f, 0.016f); 94 | 95 | // A trick to avoid jitter when frames dropped. 96 | // FIXME: Still encounter jitter sometimes... 97 | if (delta >= 0.024) { 98 | float last = 0; 99 | for (int i = 0, j = (int) Math.floor(delta / 0.016); i < j; i++) { 100 | last = enterFrame(16); 101 | } 102 | return last; 103 | } 104 | 105 | if (isFinished()) { 106 | return 1.f; 107 | } 108 | 109 | double k = 0 - mStiffness; 110 | double b = 0 - mDamping; 111 | 112 | double fSpring = k * (mValue - 1); 113 | double fDamper = b * mCurrentVelocity; 114 | 115 | mCurrentVelocity += ((fSpring + fDamper) / mMass) * delta; 116 | mValue += mCurrentVelocity * delta; 117 | 118 | mIsFirstFrame = false; 119 | 120 | return mValue; 121 | } 122 | 123 | protected boolean isFinished() { 124 | return (!mIsFirstFrame && Math.abs(mCurrentVelocity) < mTolerance); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/res/layout/combo_slider.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 26 | 27 | 34 | 35 | 42 | 43 | 51 | 52 | 61 | 62 | 63 | 64 | 69 | 70 | 71 | 72 | 81 | 82 | 87 | 88 | 89 | 90 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /app/src/main/java/me/cyandev/springanimatordemo/adjustment/BaseAdjustmentFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimatordemo.adjustment; 18 | 19 | import android.content.Context; 20 | import android.os.Bundle; 21 | import androidx.annotation.Nullable; 22 | import androidx.fragment.app.Fragment; 23 | import android.view.LayoutInflater; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import android.widget.LinearLayout; 27 | 28 | import java.util.ArrayList; 29 | 30 | import me.cyandev.springanimatordemo.adjustment.view.ComboSliderView; 31 | 32 | public abstract class BaseAdjustmentFragment extends Fragment implements ComboSliderView.Interactor { 33 | 34 | private static final String TAG = "BaseAdjustmentFragment"; 35 | private static final String KEY_VALUES = TAG + "_values"; 36 | 37 | private ArrayList mAdjustmentViews = new ArrayList<>(); 38 | private OnAdjustmentCommitListener mListener; 39 | 40 | @Override 41 | public void onAttach(Context context) { 42 | super.onAttach(context); 43 | 44 | if (getActivity() instanceof OnAdjustmentCommitListener) { 45 | mListener = (OnAdjustmentCommitListener) getActivity(); 46 | } 47 | } 48 | 49 | @Override 50 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 51 | LinearLayout layout = new LinearLayout(container.getContext()); 52 | layout.setOrientation(LinearLayout.VERTICAL); 53 | 54 | ArrayList adjustmentInfoList = onCreateAdjustmentInfo(); 55 | int id = 0; 56 | for (AdjustmentInfo info : adjustmentInfoList) { 57 | ComboSliderView view = ComboSliderView.create(layout, false); 58 | view.bindAdjustmentInfo(info); 59 | view.setOnChangeListener(this); 60 | view.setId(id++); 61 | 62 | layout.addView(view, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 63 | ViewGroup.LayoutParams.WRAP_CONTENT)); 64 | mAdjustmentViews.add(view); 65 | } 66 | 67 | return layout; 68 | } 69 | 70 | @Override 71 | public void onSaveInstanceState(Bundle outState) { 72 | super.onSaveInstanceState(outState); 73 | 74 | int[] values = new int[mAdjustmentViews.size()]; 75 | for (int i = 0, l = values.length; i < l; i++) { 76 | values[i] = mAdjustmentViews.get(i).getValue(); 77 | } 78 | 79 | outState.putIntArray(KEY_VALUES, values); 80 | } 81 | 82 | @Override 83 | public void onViewStateRestored(@Nullable Bundle savedInstanceState) { 84 | super.onViewStateRestored(savedInstanceState); 85 | 86 | if (savedInstanceState != null) { 87 | int[] values = savedInstanceState.getIntArray(KEY_VALUES); 88 | if (values != null) { 89 | for (int i = 0, l = values.length; i < l; i++) { 90 | mAdjustmentViews.get(i).setValue(values[i]); 91 | } 92 | } 93 | } 94 | } 95 | 96 | @Override 97 | public void onValueChange(int value) { 98 | Bundle confBundle = onCreateConfigurationBundle(); 99 | 100 | if (mListener != null) { 101 | mListener.onAdjustmentCommit(confBundle); 102 | } 103 | } 104 | 105 | @Override 106 | public void onFocusModeChange(View view, boolean focused) { 107 | for (ComboSliderView comboView : mAdjustmentViews) { 108 | if (focused) { 109 | if (view != comboView) { 110 | comboView.animate().alpha(0.4f).setDuration(250).start(); 111 | } 112 | } else { 113 | comboView.animate().alpha(1).setDuration(300).start(); 114 | } 115 | } 116 | } 117 | 118 | protected int getAdjustmentValue(int index) { 119 | return mAdjustmentViews.get(index).getValue(); 120 | } 121 | 122 | protected abstract String getTypeName(); 123 | 124 | protected abstract ArrayList onCreateAdjustmentInfo(); 125 | 126 | protected abstract Bundle onCreateConfigurationBundle(); 127 | 128 | public static class AdjustmentInfo { 129 | public String propertyName; 130 | public int defaultValue; 131 | public int minValue; 132 | public int maxValue; 133 | 134 | AdjustmentInfo(String propertyName, int defaultValue, int minValue, int maxValue) { 135 | this.propertyName = propertyName; 136 | this.defaultValue = defaultValue; 137 | this.minValue = minValue; 138 | this.maxValue = maxValue; 139 | } 140 | } 141 | 142 | public interface OnAdjustmentCommitListener { 143 | void onAdjustmentCommit(Bundle bundle); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /app/src/main/java/me/cyandev/springanimatordemo/preview/RecyclerViewPreviewFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimatordemo.preview; 18 | 19 | import android.animation.Animator; 20 | import android.os.Bundle; 21 | import androidx.recyclerview.widget.LinearLayoutManager; 22 | import androidx.recyclerview.widget.RecyclerView; 23 | import android.util.TypedValue; 24 | import android.view.LayoutInflater; 25 | import android.view.View; 26 | import android.view.ViewGroup; 27 | 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | 31 | import me.cyandev.springanimator.AbsSpringAnimator; 32 | import me.cyandev.springanimatordemo.R; 33 | import me.cyandev.springanimatordemo.util.SimpleAnimatorListener; 34 | 35 | public class RecyclerViewPreviewFragment extends BasePreviewFragment { 36 | 37 | private RecyclerView mRecyclerView; 38 | 39 | private List mAnimators = new ArrayList<>(); 40 | private int mLastScrollOffsetY = 0; 41 | 42 | @Override 43 | public void onResetView() { 44 | stopAnimators(); 45 | 46 | for (int i = 0, count = mRecyclerView.getChildCount(); i < count; i++) { 47 | View child = mRecyclerView.getChildAt(i); 48 | child.setTranslationY(0); 49 | } 50 | } 51 | 52 | @Override 53 | public void onStartAnimation() { 54 | stopAnimators(); 55 | 56 | for (int i = 0, count = mRecyclerView.getChildCount(); i < count; i++) { 57 | final View child = mRecyclerView.getChildAt(i); 58 | performAnimation(child, i); 59 | } 60 | } 61 | 62 | @Override 63 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 64 | mRecyclerView = new RecyclerView(getContext()); 65 | mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); 66 | mRecyclerView.setAdapter(new SimpleAdapter()); 67 | mRecyclerView.setClipToPadding(false); 68 | mRecyclerView.setPadding(0, 0, 0, 69 | (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics())); 70 | mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 71 | @Override 72 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 73 | // track for determining animation direction. 74 | mLastScrollOffsetY = dy; 75 | } 76 | }); 77 | 78 | return mRecyclerView; 79 | } 80 | 81 | private AbsSpringAnimator performAnimation(View v, long staggeringIndex) { 82 | return performAnimation(v, staggeringIndex, false); 83 | } 84 | 85 | private AbsSpringAnimator performAnimation(final View v, boolean slideDown) { 86 | return performAnimation(v, 0, slideDown); 87 | } 88 | 89 | private AbsSpringAnimator performAnimation(final View v, long staggeringIndex, boolean slideDown) { 90 | final float dy = (slideDown ? -1 : 1) * mRecyclerView.getHeight(); 91 | v.setTranslationY(dy); 92 | 93 | AbsSpringAnimator animator = createNewAnimator(); 94 | animator.setStartValue(dy); 95 | animator.setEndValue(0); 96 | animator.setStartDelay(50 * staggeringIndex); 97 | animator.addUpdateListener(new AbsSpringAnimator.AnimatorUpdateListener() { 98 | @Override 99 | public void onAnimationUpdate(AbsSpringAnimator animation) { 100 | v.setTranslationY(animation.getAnimatedValue()); 101 | } 102 | }); 103 | animator.addListener(new SimpleAnimatorListener() { 104 | @Override 105 | public void onAnimationEnd(Animator animation) { 106 | mAnimators.remove(animation); 107 | } 108 | }); 109 | animator.start(); 110 | 111 | mAnimators.add(animator); 112 | 113 | return animator; 114 | } 115 | 116 | private void stopAnimators() { 117 | ArrayList tmpAnimators = (ArrayList) ((ArrayList) mAnimators).clone(); 118 | for (AbsSpringAnimator animator : tmpAnimators) { 119 | animator.cancel(); 120 | } 121 | 122 | mAnimators.clear(); 123 | } 124 | 125 | private class SimpleAdapter extends RecyclerView.Adapter { 126 | 127 | @Override 128 | public SimpleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 129 | View view = LayoutInflater.from(getContext()).inflate(R.layout.item_simple, parent, false); 130 | return new SimpleViewHolder(view); 131 | } 132 | 133 | @Override 134 | public void onBindViewHolder(SimpleViewHolder holder, int position) { 135 | // No-op 136 | } 137 | 138 | @Override 139 | public int getItemCount() { 140 | return 20; 141 | } 142 | 143 | @Override 144 | public void onViewAttachedToWindow(SimpleViewHolder holder) { 145 | if (holder.attachedAnimator != null && holder.attachedAnimator.isRunning()) { 146 | holder.attachedAnimator.cancel(); 147 | } 148 | holder.attachedAnimator = performAnimation(holder.itemView, mLastScrollOffsetY < 0); 149 | } 150 | 151 | } 152 | 153 | private class SimpleViewHolder extends RecyclerView.ViewHolder { 154 | 155 | AbsSpringAnimator attachedAnimator; 156 | 157 | public SimpleViewHolder(View itemView) { 158 | super(itemView); 159 | itemView.setOnClickListener(new View.OnClickListener() { 160 | @Override 161 | public void onClick(View v) { 162 | onStartAnimation(); 163 | } 164 | }); 165 | } 166 | 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /library/src/main/java/me/cyandev/springanimator/Rk4SpringAnimator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimator; 18 | 19 | import androidx.annotation.NonNull; 20 | import android.util.SparseArray; 21 | 22 | /** 23 | * A spring animator using Rk4 algorithm. 24 | *
25 | * Reference: 26 | * See 27 | * https://github.com/koenbok/Framer/blob/master/framer/Animators/SpringRK4Animator.coffee 28 | */ 29 | public class Rk4SpringAnimator extends AbsSpringAnimator { 30 | 31 | private static SparseArray sStatePool; 32 | 33 | private float mTension = 250; 34 | private float mFriction = 25; 35 | private float mVelocity = 0; 36 | private float mTolerance = 1 / 10000.f; 37 | 38 | private float mValue = 0; 39 | private float mCurrentVelocity = 0; 40 | private boolean mStopSpring = false; 41 | 42 | private Integrator mIntegrator = new Integrator(new Integrator.AccelerationForStateEvaluator() { 43 | @Override 44 | public float evaluate(float[] state) { 45 | return - mTension * state[0] - mFriction * state[1]; 46 | } 47 | }); 48 | 49 | public float getTension() { 50 | return mTension; 51 | } 52 | 53 | public void setTension(float tension) { 54 | mTension = tension; 55 | } 56 | 57 | public float getFriction() { 58 | return mFriction; 59 | } 60 | 61 | public void setFriction(float friction) { 62 | mFriction = friction; 63 | } 64 | 65 | public float getVelocity() { 66 | return mVelocity; 67 | } 68 | 69 | public void setVelocity(float velocity) { 70 | mVelocity = velocity; 71 | } 72 | 73 | public float getTolerance() { 74 | return mTolerance; 75 | } 76 | 77 | public void setTolerance(float tolerance) { 78 | mTolerance = tolerance; 79 | } 80 | 81 | @Override 82 | protected long computeSettleDuration() { 83 | // TODO: Not implemented. 84 | return 0; 85 | } 86 | 87 | @Override 88 | protected void resetState() { 89 | mValue = 0; 90 | mCurrentVelocity = mVelocity; 91 | mStopSpring = false; 92 | } 93 | 94 | @Override 95 | protected float enterFrame(long frameTime) { 96 | final float delta = Math.max(frameTime / 1000.f, 0.016f); 97 | 98 | if (isFinished()) { 99 | return 1.f; 100 | } 101 | 102 | float[] stateBefore = getState(5); 103 | float[] stateAfter = getState(6); 104 | 105 | stateBefore[0] = mValue - 1; 106 | stateBefore[1] = mCurrentVelocity; 107 | 108 | mIntegrator.evaluateIntegrateState(stateBefore, delta); 109 | stateAfter[0] = stateBefore[0]; 110 | stateAfter[1] = stateBefore[1]; 111 | 112 | mValue = 1 + stateAfter[0]; 113 | 114 | final float finalVelocity = stateAfter[1]; 115 | final float netFloat = stateAfter[0]; 116 | final float net1DVelocity = stateAfter[1]; 117 | 118 | final boolean netValueIsLow = Math.abs(netFloat) < mTolerance; 119 | final boolean netVelocityIsLow = Math.abs(net1DVelocity) < mTolerance; 120 | 121 | mStopSpring = netValueIsLow && netVelocityIsLow; 122 | mCurrentVelocity = finalVelocity; 123 | 124 | return mValue; 125 | } 126 | 127 | @Override 128 | protected boolean isFinished() { 129 | return mStopSpring; 130 | } 131 | 132 | @NonNull 133 | private static float[] getState(int key) { 134 | if (sStatePool == null) { 135 | return new float[2]; 136 | } 137 | 138 | float[] state = sStatePool.get(key); 139 | if (state == null) { 140 | state = new float[2]; 141 | sStatePool.put(key, state); 142 | } 143 | 144 | return state; 145 | } 146 | 147 | private static class Integrator { 148 | 149 | private AccelerationForStateEvaluator mAccelerationForStateEvaluator; 150 | 151 | Integrator(AccelerationForStateEvaluator evaluator) { 152 | mAccelerationForStateEvaluator = evaluator; 153 | 154 | if (sStatePool == null) { 155 | sStatePool = new SparseArray<>(); 156 | } 157 | } 158 | 159 | void evaluateIntegrateState(float[] state, float dt) { 160 | float[] a = getState(1); 161 | float[] b = getState(2); 162 | float[] c = getState(3); 163 | float[] d = getState(4); 164 | 165 | evaluateState(state, a); 166 | evaluateStateWithDerivative(state, dt * 0.5f, a, b); 167 | evaluateStateWithDerivative(state, dt * 0.5f, b, c); 168 | evaluateStateWithDerivative(state, dt, c, d); 169 | 170 | final float dxdt = 1.f / 6.f * (a[0] + 2.f * (b[0] + c[0]) + d[0]); 171 | final float dvdt = 1.f / 6.f * (a[1] + 2.f * (b[1] + c[1]) + d[1]); 172 | 173 | state[0] += dxdt * dt; 174 | state[1] += dvdt * dt; 175 | } 176 | 177 | private void evaluateState(float[] initialState, float[] output) { 178 | output[0] = initialState[1]; 179 | output[1] = mAccelerationForStateEvaluator.evaluate(initialState); 180 | } 181 | 182 | private void evaluateStateWithDerivative(float[] initialState, float dt, float[] derivative, float[] output) { 183 | float[] state = getState(7); 184 | state[0] = initialState[0] + derivative[0] * dt; 185 | state[1] = initialState[1] + derivative[1] * dt; 186 | 187 | output[0] = state[1]; 188 | output[1] = mAccelerationForStateEvaluator.evaluate(state); 189 | } 190 | 191 | interface AccelerationForStateEvaluator { 192 | float evaluate(float[] state); 193 | } 194 | 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /app/src/main/java/me/cyandev/springanimatordemo/preview/TranslationPreviewFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimatordemo.preview; 18 | 19 | import android.os.Build; 20 | import android.os.Bundle; 21 | import androidx.core.view.ViewCompat; 22 | import androidx.customview.widget.ViewDragHelper; 23 | import androidx.cardview.widget.CardView; 24 | import android.util.TypedValue; 25 | import android.view.LayoutInflater; 26 | import android.view.MotionEvent; 27 | import android.view.View; 28 | import android.view.ViewGroup; 29 | import android.view.ViewTreeObserver; 30 | 31 | import me.cyandev.springanimator.AbsSpringAnimator; 32 | import me.cyandev.springanimatordemo.R; 33 | 34 | public class TranslationPreviewFragment extends BasePreviewFragment { 35 | 36 | private CardView mCardView; 37 | 38 | private AbsSpringAnimator mXAnimator; 39 | private AbsSpringAnimator mYAnimator; 40 | 41 | private boolean mOriLocationRecorded = false; 42 | private int mOriX = 0; 43 | private int mOriY = 0; 44 | private float mCardZ = 0; 45 | 46 | private ViewDragHelper.Callback mViewDragHelperCallback = new ViewDragHelper.Callback() { 47 | 48 | @Override 49 | public boolean tryCaptureView(View child, int pointerId) { 50 | return child == mCardView; 51 | } 52 | 53 | @Override 54 | public void onViewCaptured(View capturedChild, int activePointerId) { 55 | capturedChild.getParent().requestDisallowInterceptTouchEvent(true); 56 | 57 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 58 | capturedChild.animate().z(mCardZ * 5).setDuration(150).start(); 59 | } 60 | stopAnimators(); 61 | } 62 | 63 | @Override 64 | public int clampViewPositionHorizontal(View child, int left, int dx) { 65 | return left; 66 | } 67 | 68 | @Override 69 | public int clampViewPositionVertical(View child, int top, int dy) { 70 | return top; 71 | } 72 | 73 | @Override 74 | public void onViewReleased(View releasedChild, float xvel, float yvel) { 75 | releasedChild.getParent().requestDisallowInterceptTouchEvent(false); 76 | 77 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 78 | releasedChild.animate().z(mCardZ).start(); 79 | } 80 | onStartAnimation(); 81 | } 82 | 83 | }; 84 | 85 | @Override 86 | public void onResetView() { 87 | stopAnimators(); 88 | 89 | mCardView.getParent().requestLayout(); 90 | recordOriginalLocation(mCardView); 91 | } 92 | 93 | @Override 94 | public void onStartAnimation() { 95 | stopAnimators(); 96 | 97 | if (!mOriLocationRecorded) { 98 | onResetView(); 99 | return; 100 | } 101 | 102 | AbsSpringAnimator animator = createNewAnimator(); 103 | animator.setStartValue(mCardView.getLeft()); 104 | animator.setEndValue(mOriX); 105 | animator.addUpdateListener(new AbsSpringAnimator.AnimatorUpdateListener() { 106 | @Override 107 | public void onAnimationUpdate(AbsSpringAnimator animation) { 108 | ViewCompat.offsetLeftAndRight(mCardView, -mCardView.getLeft() + (int) animation.getAnimatedValue()); 109 | } 110 | }); 111 | animator.start(); 112 | mXAnimator = animator; 113 | 114 | animator = createNewAnimator(); 115 | animator.setStartValue(mCardView.getTop()); 116 | animator.setEndValue(mOriY); 117 | animator.addUpdateListener(new AbsSpringAnimator.AnimatorUpdateListener() { 118 | @Override 119 | public void onAnimationUpdate(AbsSpringAnimator animation) { 120 | ViewCompat.offsetTopAndBottom(mCardView, -mCardView.getTop() + (int) animation.getAnimatedValue()); 121 | } 122 | }); 123 | animator.start(); 124 | mYAnimator = animator; 125 | } 126 | 127 | @Override 128 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 129 | ViewGroup view = (ViewGroup) inflater.inflate(R.layout.fragment_square, container, false); 130 | mCardView = (CardView) view.findViewById(R.id.card_view); 131 | 132 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 133 | mCardZ = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2.f, getResources().getDisplayMetrics()); 134 | mCardView.setZ(mCardZ); 135 | } 136 | 137 | final ViewDragHelper viewDragHelper = ViewDragHelper.create(view, mViewDragHelperCallback); 138 | view.setOnTouchListener(new View.OnTouchListener() { 139 | @Override 140 | public boolean onTouch(View v, MotionEvent event) { 141 | viewDragHelper.processTouchEvent(event); 142 | return false; 143 | } 144 | }); 145 | 146 | recordOriginalLocation(mCardView); 147 | 148 | return view; 149 | } 150 | 151 | private void stopAnimators() { 152 | if (mXAnimator != null) { 153 | mXAnimator.cancel(); 154 | mXAnimator = null; 155 | } 156 | 157 | if (mYAnimator != null) { 158 | mYAnimator.cancel(); 159 | mYAnimator = null; 160 | } 161 | } 162 | 163 | private void recordOriginalLocation(final View target) { 164 | mOriLocationRecorded = false; 165 | target.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 166 | @Override 167 | public void onGlobalLayout() { 168 | ViewTreeObserver observer = target.getViewTreeObserver(); 169 | if (observer.isAlive()) { 170 | observer.removeOnGlobalLayoutListener(this); 171 | } 172 | 173 | mOriX = target.getLeft(); 174 | mOriY = target.getTop(); 175 | mOriLocationRecorded = true; 176 | } 177 | }); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /library/src/main/java/me/cyandev/springanimator/internal/AnimationHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * Copyright (C) 2017 Cyandev 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package me.cyandev.springanimator.internal; 19 | 20 | import android.os.SystemClock; 21 | import android.view.Choreographer; 22 | 23 | import java.util.ArrayList; 24 | import java.util.HashMap; 25 | 26 | /** 27 | * A modified version of stock {@code AnimationHandler}, removing a bunch of usage of private API. 28 | * 29 | * This custom, static handler handles the timing pulse that is shared by all active 30 | * ValueAnimators. This approach ensures that the setting of animation values will happen on the 31 | * same thread that animations start on, and that all animations will share the same times for 32 | * calculating their values, which makes synchronizing animations possible. 33 | * 34 | * The handler uses the Choreographer by default for doing periodic callbacks. A custom 35 | * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that 36 | * may be independent of UI frame update. This could be useful in testing. 37 | */ 38 | public class AnimationHandler { 39 | /** 40 | * Internal per-thread collections used to avoid set collisions as animations start and end 41 | * while being processed. 42 | */ 43 | private final HashMap mDelayedCallbackStartTime = 44 | new HashMap<>(); 45 | private final ArrayList mAnimationCallbacks = 46 | new ArrayList<>(); 47 | private AnimationFrameCallbackProvider mProvider; 48 | 49 | private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() { 50 | @Override 51 | public void doFrame(long frameTimeNanos) { 52 | doAnimationFrame(getProvider().getFrameTime()); 53 | if (mAnimationCallbacks.size() > 0) { 54 | getProvider().postFrameCallback(this); 55 | } 56 | } 57 | }; 58 | 59 | private final static ThreadLocal sAnimatorHandler = new ThreadLocal<>(); 60 | private boolean mListDirty = false; 61 | 62 | public static AnimationHandler getInstance() { 63 | if (sAnimatorHandler.get() == null) { 64 | sAnimatorHandler.set(new AnimationHandler()); 65 | } 66 | return sAnimatorHandler.get(); 67 | } 68 | 69 | /** 70 | * By default, the Choreographer is used to provide timing for frame callbacks. A custom 71 | * provider can be used here to provide different timing pulse. 72 | */ 73 | public void setProvider(AnimationFrameCallbackProvider provider) { 74 | if (provider == null) { 75 | mProvider = new MyFrameCallbackProvider(); 76 | } else { 77 | mProvider = provider; 78 | } 79 | } 80 | 81 | private AnimationFrameCallbackProvider getProvider() { 82 | if (mProvider == null) { 83 | mProvider = new MyFrameCallbackProvider(); 84 | } 85 | return mProvider; 86 | } 87 | 88 | /** 89 | * Register to get a callback on the next frame after the delay. 90 | */ 91 | public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) { 92 | if (mAnimationCallbacks.size() == 0) { 93 | getProvider().postFrameCallback(mFrameCallback); 94 | } 95 | if (!mAnimationCallbacks.contains(callback)) { 96 | mAnimationCallbacks.add(callback); 97 | } 98 | 99 | if (delay > 0) { 100 | mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay)); 101 | } 102 | } 103 | 104 | /** 105 | * Removes the given callback from the list, so it will no longer be called for frame related 106 | * timing. 107 | */ 108 | public void removeCallback(AnimationFrameCallback callback) { 109 | mDelayedCallbackStartTime.remove(callback); 110 | int id = mAnimationCallbacks.indexOf(callback); 111 | if (id >= 0) { 112 | mAnimationCallbacks.set(id, null); 113 | mListDirty = true; 114 | } 115 | } 116 | 117 | private void doAnimationFrame(long frameTime) { 118 | int size = mAnimationCallbacks.size(); 119 | long currentTime = SystemClock.uptimeMillis(); 120 | for (int i = 0; i < size; i++) { 121 | final AnimationFrameCallback callback = mAnimationCallbacks.get(i); 122 | if (callback == null) { 123 | continue; 124 | } 125 | if (isCallbackDue(callback, currentTime)) { 126 | callback.doAnimationFrame(frameTime); 127 | } 128 | } 129 | cleanUpList(); 130 | } 131 | 132 | /** 133 | * Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay 134 | * so that they can start getting frame callbacks. 135 | * 136 | * @return true if they have passed the initial delay or have no delay, false otherwise. 137 | */ 138 | private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) { 139 | Long startTime = mDelayedCallbackStartTime.get(callback); 140 | if (startTime == null) { 141 | return true; 142 | } 143 | if (startTime < currentTime) { 144 | mDelayedCallbackStartTime.remove(callback); 145 | return true; 146 | } 147 | return false; 148 | } 149 | 150 | /** 151 | * Return the number of callbacks that have registered for frame callbacks. 152 | */ 153 | public static int getAnimationCount() { 154 | AnimationHandler handler = sAnimatorHandler.get(); 155 | if (handler == null) { 156 | return 0; 157 | } 158 | return handler.getCallbackSize(); 159 | } 160 | 161 | private void cleanUpList() { 162 | if (mListDirty) { 163 | for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) { 164 | if (mAnimationCallbacks.get(i) == null) { 165 | mAnimationCallbacks.remove(i); 166 | } 167 | } 168 | mListDirty = false; 169 | } 170 | } 171 | 172 | private int getCallbackSize() { 173 | int count = 0; 174 | int size = mAnimationCallbacks.size(); 175 | for (int i = size - 1; i >= 0; i--) { 176 | if (mAnimationCallbacks.get(i) != null) { 177 | count++; 178 | } 179 | } 180 | return count; 181 | } 182 | 183 | /** 184 | * Default provider of timing pulse that uses Choreographer for frame callbacks. 185 | */ 186 | private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider { 187 | 188 | final Choreographer mChoreographer = Choreographer.getInstance(); 189 | 190 | @Override 191 | public void postFrameCallback(Choreographer.FrameCallback callback) { 192 | mChoreographer.postFrameCallback(callback); 193 | } 194 | 195 | @Override 196 | public long getFrameTime() { 197 | return System.nanoTime(); 198 | } 199 | 200 | } 201 | 202 | /** 203 | * Callbacks that receives notifications for animation timing and frame commit timing. 204 | */ 205 | public interface AnimationFrameCallback { 206 | /** 207 | * Run animation based on the frame time. 208 | * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time 209 | * base. 210 | */ 211 | void doAnimationFrame(long frameTime); 212 | } 213 | 214 | /** 215 | * The intention for having this interface is to increase the testability of ValueAnimator. 216 | * Specifically, we can have a custom implementation of the interface below and provide 217 | * timing pulse without using Choreographer. That way we could use any arbitrary interval for 218 | * our timing pulse in the tests. 219 | */ 220 | public interface AnimationFrameCallbackProvider { 221 | void postFrameCallback(Choreographer.FrameCallback callback); 222 | long getFrameTime(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /app/src/main/java/me/cyandev/springanimatordemo/adjustment/view/ComboSliderView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimatordemo.adjustment.view; 18 | 19 | import android.animation.Animator; 20 | import android.content.Context; 21 | import android.content.res.ColorStateList; 22 | import android.content.res.TypedArray; 23 | import android.os.Build; 24 | import androidx.annotation.Nullable; 25 | import com.google.android.material.textfield.TextInputEditText; 26 | import com.google.android.material.textfield.TextInputLayout; 27 | import androidx.appcompat.widget.AppCompatImageButton; 28 | import androidx.appcompat.widget.AppCompatSeekBar; 29 | import android.util.AttributeSet; 30 | import android.view.KeyEvent; 31 | import android.view.LayoutInflater; 32 | import android.view.View; 33 | import android.view.ViewGroup; 34 | import android.view.inputmethod.InputMethodManager; 35 | import android.widget.FrameLayout; 36 | import android.widget.SeekBar; 37 | import android.widget.TextView; 38 | 39 | import me.cyandev.springanimatordemo.R; 40 | import me.cyandev.springanimatordemo.adjustment.BaseAdjustmentFragment; 41 | import me.cyandev.springanimatordemo.util.SimpleAnimatorListener; 42 | 43 | public class ComboSliderView extends FrameLayout { 44 | 45 | private ViewGroup mSliderLayout; 46 | private ViewGroup mInputViewLayout; 47 | private TextView mLabelTextView; 48 | private TextView mValueTextView; 49 | private AppCompatSeekBar mSeekBar; 50 | private TextInputEditText mEditText; 51 | private AppCompatImageButton mEditButton; 52 | 53 | private BaseAdjustmentFragment.AdjustmentInfo mAdjustmentInfo; 54 | private boolean mIsInputViewVisible = false; 55 | 56 | private InputMethodManager mImm; 57 | 58 | private Interactor mInteractor; 59 | 60 | public static ComboSliderView create(ViewGroup root, boolean attachToRoot) { 61 | return (ComboSliderView) LayoutInflater.from(root.getContext()) 62 | .inflate(R.layout.combo_slider, root, attachToRoot); 63 | } 64 | 65 | public ComboSliderView(Context context) { 66 | this(context, null); 67 | } 68 | 69 | public ComboSliderView(Context context, @Nullable AttributeSet attrs) { 70 | this(context, attrs, 0); 71 | } 72 | 73 | public ComboSliderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 74 | super(context, attrs, defStyleAttr); 75 | 76 | mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); 77 | } 78 | 79 | public void setOnChangeListener(Interactor listener) { 80 | mInteractor = listener; 81 | } 82 | 83 | public void setValue(int value) { 84 | mSeekBar.setProgress(value - mAdjustmentInfo.minValue); 85 | mValueTextView.setText(String.valueOf(value)); 86 | mEditText.setText(mValueTextView.getText()); 87 | } 88 | 89 | public int getValue() { 90 | return mSeekBar.getProgress() + mAdjustmentInfo.minValue; 91 | } 92 | 93 | public void bindAdjustmentInfo(BaseAdjustmentFragment.AdjustmentInfo info) { 94 | mAdjustmentInfo = info; 95 | 96 | mLabelTextView.setText(info.propertyName); 97 | ((TextInputLayout) mInputViewLayout).setHint(info.propertyName); 98 | mSeekBar.setMax(info.maxValue - info.minValue); 99 | mSeekBar.setProgress(info.defaultValue - info.minValue); 100 | mValueTextView.setText(String.valueOf(info.defaultValue)); 101 | mEditText.setText(mValueTextView.getText()); 102 | } 103 | 104 | @Override 105 | protected void onFinishInflate() { 106 | super.onFinishInflate(); 107 | 108 | mSliderLayout = (ViewGroup) findViewById(R.id.slider_layout); 109 | mInputViewLayout = (ViewGroup) findViewById(R.id.input_view_layout); 110 | mLabelTextView = (TextView) findViewById(R.id.text); 111 | mValueTextView = (TextView) findViewById(R.id.text_value); 112 | mSeekBar = (AppCompatSeekBar) findViewById(R.id.seek_bar); 113 | mEditText = (TextInputEditText) findViewById(R.id.edit); 114 | mEditButton = (AppCompatImageButton) findViewById(R.id.btn_edit); 115 | 116 | mInputViewLayout.setVisibility(INVISIBLE); 117 | 118 | mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 119 | @Override 120 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 121 | mValueTextView.setText(String.valueOf(progress + mAdjustmentInfo.minValue)); 122 | } 123 | 124 | @Override 125 | public void onStartTrackingTouch(SeekBar seekBar) { 126 | notifyFocusChange(true); 127 | } 128 | 129 | @Override 130 | public void onStopTrackingTouch(SeekBar seekBar) { 131 | notifyFocusChange(false); 132 | commitValue(getValue()); 133 | } 134 | }); 135 | 136 | mEditText.setFocusable(true); 137 | mEditText.setFocusableInTouchMode(true); 138 | mEditText.setOnKeyListener(new OnKeyListener() { 139 | @Override 140 | public boolean onKey(View v, int keyCode, KeyEvent event) { 141 | if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_BACK) { 142 | try { 143 | commitValue(Integer.parseInt(mEditText.getText().toString())); 144 | } catch (NumberFormatException ignored) {} 145 | 146 | // If the input view is still visible, press back key to hide it. 147 | if (keyCode == KeyEvent.KEYCODE_BACK && mInputViewLayout.getVisibility() == VISIBLE) { 148 | return true; 149 | } 150 | } 151 | return false; 152 | } 153 | }); 154 | mEditText.setOnFocusChangeListener(new OnFocusChangeListener() { 155 | @Override 156 | public void onFocusChange(View v, boolean hasFocus) { 157 | if (hasFocus) { 158 | return; 159 | } 160 | 161 | try { 162 | commitValue(Integer.parseInt(mEditText.getText().toString())); 163 | } catch (NumberFormatException ignored) {} 164 | } 165 | }); 166 | 167 | mEditButton.setOnClickListener(new OnClickListener() { 168 | @Override 169 | public void onClick(View v) { 170 | if (mIsInputViewVisible) { 171 | commitValue(Integer.parseInt(mEditText.getText().toString())); 172 | } else { 173 | showInputView(); 174 | } 175 | } 176 | }); 177 | } 178 | 179 | private void commitValue(int value) { 180 | // Clamp the value. 181 | value = Math.min(Math.max(value, mAdjustmentInfo.minValue), mAdjustmentInfo.maxValue); 182 | 183 | mSeekBar.setProgress(value - mAdjustmentInfo.minValue); 184 | mValueTextView.setText(String.valueOf(value)); 185 | mEditText.setText(mValueTextView.getText()); 186 | 187 | if (mInteractor != null) { 188 | mInteractor.onValueChange(value); 189 | } 190 | 191 | if (mIsInputViewVisible) { 192 | hideInputView(); 193 | } 194 | } 195 | 196 | private void notifyFocusChange(boolean focus) { 197 | if (mInteractor != null) { 198 | mInteractor.onFocusModeChange(this, focus); 199 | } 200 | } 201 | 202 | private void showInputView() { 203 | if (mIsInputViewVisible) { 204 | return; 205 | } 206 | 207 | mIsInputViewVisible = true; 208 | 209 | mEditButton.setImageResource(R.drawable.ic_check_black_16dp); 210 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 211 | TypedArray a = getContext().getTheme().obtainStyledAttributes(new int[] { R.attr.colorAccent }); 212 | mEditButton.setImageTintList(ColorStateList.valueOf(a.getColor(0, 0))); 213 | a.recycle(); 214 | } 215 | 216 | mSliderLayout 217 | .animate() 218 | .alpha(0) 219 | .setListener(new SimpleAnimatorListener() { 220 | @Override 221 | public void onAnimationEnd(Animator animation) { 222 | mSliderLayout.setVisibility(INVISIBLE); 223 | } 224 | }) 225 | .setDuration(200) 226 | .start(); 227 | 228 | mInputViewLayout.setVisibility(VISIBLE); 229 | mInputViewLayout.setAlpha(0); 230 | mInputViewLayout 231 | .animate() 232 | .alpha(1) 233 | .setListener(new SimpleAnimatorListener() { 234 | @Override 235 | public void onAnimationEnd(Animator animation) { 236 | mEditText.requestFocus(); 237 | mEditText.setSelection(mEditText.getText().length()); 238 | mImm.showSoftInput(mEditText, 0, null); 239 | notifyFocusChange(true); 240 | } 241 | }) 242 | .setDuration(200) 243 | .start(); 244 | } 245 | 246 | @SuppressWarnings("deprecation") 247 | private void hideInputView() { 248 | if (!mIsInputViewVisible) { 249 | return; 250 | } 251 | 252 | mIsInputViewVisible = false; 253 | 254 | mEditButton.setImageResource(R.drawable.ic_mode_edit_black_16dp); 255 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 256 | mEditButton.setImageTintList(ColorStateList.valueOf(getResources().getColor(R.color.colorSecondaryGray))); 257 | } 258 | 259 | mSliderLayout.setVisibility(VISIBLE); 260 | mSliderLayout 261 | .animate() 262 | .alpha(1) 263 | .setListener(null) 264 | .setDuration(200) 265 | .start(); 266 | 267 | mInputViewLayout 268 | .animate() 269 | .alpha(0) 270 | .setListener(new SimpleAnimatorListener() { 271 | @Override 272 | public void onAnimationEnd(Animator animation) { 273 | mInputViewLayout.setVisibility(INVISIBLE); 274 | } 275 | }) 276 | .setDuration(200) 277 | .start(); 278 | 279 | mImm.hideSoftInputFromWindow(getWindowToken(), 0, null); 280 | notifyFocusChange(false); 281 | } 282 | 283 | public interface Interactor { 284 | void onValueChange(int value); 285 | void onFocusModeChange(View view, boolean focused); 286 | } 287 | 288 | } 289 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2017 Cyandev 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /library/src/main/java/me/cyandev/springanimator/AbsSpringAnimator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Cyandev 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 me.cyandev.springanimator; 18 | 19 | import android.animation.Animator; 20 | import android.animation.TimeInterpolator; 21 | import android.os.Build; 22 | import android.os.Looper; 23 | import androidx.annotation.RequiresApi; 24 | import android.util.AndroidRuntimeException; 25 | import android.util.Log; 26 | 27 | import java.util.ArrayList; 28 | 29 | import me.cyandev.springanimator.internal.AnimationHandler; 30 | 31 | /** 32 | * This abstract class provides the basic mechanism for each spring animator. 33 | */ 34 | public abstract class AbsSpringAnimator extends Animator implements AnimationHandler.AnimationFrameCallback { 35 | 36 | private static final String TAG = "AbsSpringAnimator"; 37 | private static final boolean DEBUG = false; 38 | 39 | private ArrayList mUpdateListeners = new ArrayList<>(); 40 | 41 | private long mLastFrameTime = 0; 42 | private long mStartDelay = 0; 43 | private boolean mStarted = false; 44 | private boolean mRunning = false; 45 | private boolean mStartListenersCalled = false; 46 | private float mProgress = 0; 47 | private float mStartValue = 0; 48 | private float mEndValue = 0; 49 | 50 | /** 51 | * Sets the start value. 52 | */ 53 | public void setStartValue(float startValue) { 54 | if (mRunning || mStarted) { 55 | throw new IllegalStateException("Animators that has been started cannot be changed"); 56 | } 57 | 58 | mStartValue = startValue; 59 | } 60 | 61 | /** 62 | * Sets the end value. 63 | */ 64 | public void setEndValue(float endValue) { 65 | if (mRunning || mStarted) { 66 | throw new IllegalStateException("Animators that has been started cannot be changed"); 67 | } 68 | 69 | mEndValue = endValue; 70 | } 71 | 72 | /** 73 | * Gets current progress fraction of the animator. 74 | * 75 | * @return the progress 76 | */ 77 | public float getProgress() { 78 | return mProgress; 79 | } 80 | 81 | /** 82 | * Gets current animated value of the animator. 83 | * 84 | * @return the value 85 | */ 86 | public float getAnimatedValue() { 87 | return mStartValue + (mEndValue - mStartValue) * mProgress; 88 | } 89 | 90 | /** 91 | * Adds a listener to the set of listeners that are sent update events through the life of 92 | * an animation. This method is called on all listeners for every frame of the animation, 93 | * after the values for the animation have been calculated. 94 | * 95 | * @param listener the listener to be added to the current set of listeners for this animation 96 | */ 97 | public void addUpdateListener(AnimatorUpdateListener listener) { 98 | if (!mUpdateListeners.contains(listener)) { 99 | mUpdateListeners.add(listener); 100 | } 101 | } 102 | 103 | /** 104 | * Removes a listener from the set listening to frame updates for this animation. 105 | * 106 | * @param listener the listener to be removed from the current set of update listeners 107 | * for this animation 108 | */ 109 | public void removeUpdateListener(AnimatorUpdateListener listener) { 110 | mUpdateListeners.remove(listener); 111 | } 112 | 113 | /** 114 | * Removes all listeners from the set listening to frame updates for this animation. 115 | */ 116 | public void removeAllUpdateListeners() { 117 | if (mUpdateListeners == null) { 118 | return; 119 | } 120 | mUpdateListeners.clear(); 121 | } 122 | 123 | /** {@inheritDoc} */ 124 | @Override 125 | public long getStartDelay() { 126 | return mStartDelay; 127 | } 128 | 129 | /** {@inheritDoc} */ 130 | @Override 131 | public void setStartDelay(long startDelay) { 132 | // Clamp start delay to non-negative range. 133 | if (startDelay < 0) { 134 | Log.w(TAG, "Start delay should always be non-negative"); 135 | startDelay = 0; 136 | } 137 | mStartDelay = startDelay; 138 | } 139 | 140 | @Override 141 | public Animator setDuration(long duration) { 142 | throw new UnsupportedOperationException("Duration should not be set manually"); 143 | } 144 | 145 | /** 146 | * Gets a estimated duration based on current properties. 147 | * 148 | * @return the duration in milliseconds 149 | */ 150 | @Override 151 | public long getDuration() { 152 | return computeSettleDuration(); 153 | } 154 | 155 | @Override 156 | public void setInterpolator(TimeInterpolator value) { 157 | throw new UnsupportedOperationException("SpringAnimator does not support time interpolator"); 158 | } 159 | 160 | /** {@inheritDoc} */ 161 | @Override 162 | public boolean isRunning() { 163 | return mRunning; 164 | } 165 | 166 | /** {@inheritDoc} */ 167 | @Override 168 | public void start() { 169 | if (Looper.myLooper() == null) { 170 | throw new AndroidRuntimeException("Animators may only be run on Looper threads"); 171 | } 172 | 173 | mStarted = true; 174 | mRunning = false; 175 | mProgress = 0; 176 | 177 | mLastFrameTime = 0; 178 | AnimationHandler handler = AnimationHandler.getInstance(); 179 | handler.addAnimationFrameCallback(this, mStartDelay); 180 | 181 | if (mStartDelay == 0) { 182 | startAnimation(); 183 | } 184 | } 185 | 186 | /** {@inheritDoc} */ 187 | @SuppressWarnings("unchecked") 188 | @Override 189 | public void cancel() { 190 | if (Looper.myLooper() == null) { 191 | throw new AndroidRuntimeException("Animators may only be run on Looper threads"); 192 | } 193 | 194 | if ((mStarted || mRunning) && getListeners() != null) { 195 | if (!mRunning) { 196 | // If it's not yet running, then start listeners weren't called. Call them now. 197 | notifyStartListeners(); 198 | } 199 | ArrayList tmpListeners = 200 | (ArrayList) getListeners().clone(); 201 | for (AnimatorListener listener : tmpListeners) { 202 | listener.onAnimationCancel(this); 203 | } 204 | } 205 | endAnimation(); 206 | } 207 | 208 | /** {@inheritDoc} */ 209 | @Override 210 | public void end() { 211 | if (Looper.myLooper() == null) { 212 | throw new AndroidRuntimeException("Animators may only be run on Looper threads"); 213 | } 214 | if (mProgress != 1.f) { 215 | mProgress = 1.f; 216 | notifyUpdateListeners(); 217 | } 218 | endAnimation(); 219 | } 220 | 221 | /** {@inheritDoc} */ 222 | @RequiresApi(api = Build.VERSION_CODES.KITKAT) 223 | @Override 224 | public void resume() { 225 | if (Looper.myLooper() == null) { 226 | throw new AndroidRuntimeException("Animators may only be resumed from the same " + 227 | "thread that the animator was started on"); 228 | } 229 | if (isPaused()) { 230 | if (!mRunning) { 231 | AnimationHandler handler = AnimationHandler.getInstance(); 232 | handler.addAnimationFrameCallback(this, 0); 233 | } 234 | } 235 | super.resume(); 236 | } 237 | 238 | /** 239 | * Override point for subclasses to compute an estimated settle duration. 240 | * 241 | * @return the duration 242 | */ 243 | protected abstract long computeSettleDuration(); 244 | 245 | /** 246 | * Override point for subclasses to reset internal state. 247 | */ 248 | protected abstract void resetState(); 249 | 250 | /** 251 | * Override point for subclasses to compute values for next frames. 252 | * 253 | * @param frameTime how long has been skipped since last call 254 | */ 255 | protected abstract float enterFrame(long frameTime); 256 | 257 | /** 258 | * Override point for subclasses to report whether the animation is finished. 259 | * 260 | * @return whether the animation is finished 261 | */ 262 | protected abstract boolean isFinished(); 263 | 264 | private void startAnimation() { 265 | mRunning = true; 266 | resetState(); 267 | notifyStartListeners(); 268 | } 269 | 270 | @SuppressWarnings("unchecked") 271 | private void endAnimation() { 272 | AnimationHandler handler = AnimationHandler.getInstance(); 273 | handler.removeCallback(this); 274 | 275 | if ((mStarted || mRunning) && getListeners() != null) { 276 | if (!mRunning) { 277 | // If it's not yet running, then start listeners weren't called. Call them now. 278 | notifyStartListeners(); 279 | } 280 | ArrayList tmpListeners = 281 | (ArrayList) getListeners().clone(); 282 | int numListeners = tmpListeners.size(); 283 | for (int i = 0; i < numListeners; ++i) { 284 | tmpListeners.get(i).onAnimationEnd(this); 285 | } 286 | } 287 | mRunning = false; 288 | mStarted = false; 289 | mStartListenersCalled = false; 290 | mLastFrameTime = 0; 291 | } 292 | 293 | @SuppressWarnings("unchecked") 294 | private void notifyStartListeners() { 295 | if (getListeners() != null && !mStartListenersCalled) { 296 | ArrayList tmpListeners = 297 | (ArrayList) getListeners().clone(); 298 | int numListeners = tmpListeners.size(); 299 | for (int i = 0; i < numListeners; ++i) { 300 | tmpListeners.get(i).onAnimationStart(this); 301 | } 302 | } 303 | mStartListenersCalled = true; 304 | } 305 | 306 | @SuppressWarnings("unchecked") 307 | private void notifyUpdateListeners() { 308 | if (mUpdateListeners.size() > 0) { 309 | ArrayList tmpListeners = 310 | (ArrayList) mUpdateListeners.clone(); 311 | int numListeners = tmpListeners.size(); 312 | for (int i = 0; i < numListeners; ++i) { 313 | tmpListeners.get(i).onAnimationUpdate(this); 314 | } 315 | } 316 | } 317 | 318 | // ###################### AnimationFrameCallback ###################### 319 | 320 | @Override 321 | public void doAnimationFrame(long frameTime) { 322 | AnimationHandler handler = AnimationHandler.getInstance(); 323 | long skipped = 0; 324 | if (mLastFrameTime == 0) { 325 | if (getStartDelay() > 0) { 326 | startAnimation(); 327 | } 328 | } else { 329 | skipped = frameTime - mLastFrameTime; 330 | } 331 | mLastFrameTime = frameTime; 332 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 333 | if (isPaused()) { 334 | handler.removeCallback(this); 335 | mLastFrameTime = 0; 336 | mRunning = false; 337 | } 338 | } 339 | 340 | mProgress = enterFrame(skipped / 1000000L); 341 | notifyUpdateListeners(); 342 | if (isFinished()) { 343 | endAnimation(); 344 | } 345 | } 346 | 347 | public interface AnimatorUpdateListener { 348 | /** 349 | *

Notifies the occurrence of another frame of the animation.

350 | * 351 | * @param animation The animation which was repeated. 352 | */ 353 | void onAnimationUpdate(AbsSpringAnimator animation); 354 | 355 | } 356 | 357 | } 358 | 359 | 360 | -------------------------------------------------------------------------------- /app/src/main/java/me/cyandev/springanimatordemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package me.cyandev.springanimatordemo; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ValueAnimator; 5 | import android.os.Build; 6 | import android.os.Bundle; 7 | 8 | import com.google.android.material.floatingactionbutton.FloatingActionButton; 9 | import androidx.fragment.app.Fragment; 10 | import androidx.fragment.app.FragmentManager; 11 | import androidx.fragment.app.FragmentPagerAdapter; 12 | import androidx.viewpager.widget.ViewPager; 13 | import androidx.interpolator.view.animation.FastOutLinearInInterpolator; 14 | import androidx.interpolator.view.animation.FastOutSlowInInterpolator; 15 | import androidx.appcompat.app.AppCompatActivity; 16 | import androidx.appcompat.widget.AppCompatSpinner; 17 | import androidx.appcompat.widget.Toolbar; 18 | import android.transition.Slide; 19 | import android.util.SparseArray; 20 | import android.util.TypedValue; 21 | import android.view.Gravity; 22 | import android.view.Menu; 23 | import android.view.MenuItem; 24 | import android.view.MotionEvent; 25 | import android.view.View; 26 | import android.view.ViewGroup; 27 | import android.view.ViewTreeObserver; 28 | import android.widget.AdapterView; 29 | import android.widget.ArrayAdapter; 30 | import android.widget.FrameLayout; 31 | 32 | import java.util.ArrayList; 33 | 34 | import me.cyandev.springanimator.AbsSpringAnimator; 35 | import me.cyandev.springanimatordemo.adjustment.BaseAdjustmentFragment; 36 | import me.cyandev.springanimatordemo.adjustment.DhoAdjustmentFragment; 37 | import me.cyandev.springanimatordemo.adjustment.Rk4AdjustmentFragment; 38 | import me.cyandev.springanimatordemo.preview.BasePreviewFragment; 39 | import me.cyandev.springanimatordemo.preview.RecyclerViewPreviewFragment; 40 | import me.cyandev.springanimatordemo.preview.TranslationPreviewFragment; 41 | import me.cyandev.springanimatordemo.util.SimpleAnimatorListener; 42 | 43 | public class MainActivity extends AppCompatActivity 44 | implements BasePreviewFragment.SpringAnimatorProvider, BaseAdjustmentFragment.OnAdjustmentCommitListener { 45 | 46 | private static final String TAG = "MainActivity"; 47 | private static final String KEY_CURRENT_CONFIGURATION = TAG + "_currentConfiguration"; 48 | private static final String KEY_SELECTED_ADJUSTMENT = TAG + "_selectedAdjustment"; 49 | 50 | private static final Class[] PREVIEW_FRAGMENT_CLASSES = { 51 | TranslationPreviewFragment.class, 52 | RecyclerViewPreviewFragment.class 53 | }; 54 | 55 | private static final String[][] ADJUSTMENT_METADATA = { 56 | { "Spring - DHO", DhoAdjustmentFragment.class.getName() }, 57 | { "Spring - RK4", Rk4AdjustmentFragment.class.getName() } 58 | }; 59 | 60 | private FloatingActionButton mFab; 61 | private ViewPager mPager; 62 | private ViewGroup mPanelLayout; 63 | private AppCompatSpinner mAdjustmentTypeSpinner; 64 | private FrameLayout mPanelContainer; 65 | 66 | private Bundle mCurrentConfiguration; 67 | 68 | private boolean mPanelClosed = false; 69 | private int mSelectedAdjustment = 0; 70 | 71 | @SuppressWarnings("unchecked") 72 | @Override 73 | protected void onCreate(Bundle savedInstanceState) { 74 | super.onCreate(savedInstanceState); 75 | setContentView(R.layout.activity_main); 76 | 77 | setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); 78 | 79 | mFab = (FloatingActionButton) findViewById(R.id.fab); 80 | mPager = (ViewPager) findViewById(R.id.pager); 81 | mPanelLayout = (ViewGroup) findViewById(R.id.panel_layout); 82 | mAdjustmentTypeSpinner = (AppCompatSpinner) findViewById(R.id.spinner); 83 | mPanelContainer = (FrameLayout) findViewById(R.id.container2); 84 | 85 | mFab.setOnClickListener(new View.OnClickListener() { 86 | @Override 87 | public void onClick(View v) { 88 | openAdjustmentPanel(); 89 | } 90 | }); 91 | 92 | mPager.setAdapter(new PreviewPagerAdapter(getSupportFragmentManager())); 93 | 94 | mPanelLayout.setOnTouchListener(new View.OnTouchListener() { 95 | @Override 96 | public boolean onTouch(View v, MotionEvent event) { 97 | // Eat the event to prevent touching through. 98 | return true; 99 | } 100 | }); 101 | 102 | setupSpinner(); 103 | 104 | if (savedInstanceState == null) { 105 | mCurrentConfiguration = ConfigurationResolver.buildDhoConfiguration(200, 10, 1, 0); 106 | mPager.postDelayed(new Runnable() { 107 | @Override 108 | public void run() { 109 | peekPager(); 110 | } 111 | }, 1500); 112 | } 113 | } 114 | 115 | @Override 116 | protected void onStart() { 117 | super.onStart(); 118 | 119 | // Hide the panel for a while if it's not opened. 120 | if (!isAdjustmentPanelVisible()) { 121 | mPanelLayout.setVisibility(View.INVISIBLE); 122 | } 123 | } 124 | 125 | @Override 126 | public boolean onCreateOptionsMenu(Menu menu) { 127 | getMenuInflater().inflate(R.menu.activity_main, menu); 128 | 129 | return true; 130 | } 131 | 132 | @Override 133 | public boolean onOptionsItemSelected(MenuItem item) { 134 | switch (item.getItemId()) { 135 | case 0 /* placeholder */: 136 | break; 137 | default: 138 | return super.onOptionsItemSelected(item); 139 | } 140 | 141 | return true; 142 | } 143 | 144 | @Override 145 | protected void onSaveInstanceState(Bundle outState) { 146 | super.onSaveInstanceState(outState); 147 | 148 | outState.putBundle(KEY_CURRENT_CONFIGURATION, mCurrentConfiguration); 149 | outState.putInt(KEY_SELECTED_ADJUSTMENT, mSelectedAdjustment); 150 | } 151 | 152 | @Override 153 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 154 | super.onRestoreInstanceState(savedInstanceState); 155 | 156 | mCurrentConfiguration = savedInstanceState.getBundle(KEY_CURRENT_CONFIGURATION); 157 | mSelectedAdjustment = savedInstanceState.getInt(KEY_SELECTED_ADJUSTMENT); 158 | } 159 | 160 | @Override 161 | public void onBackPressed() { 162 | if (closeAdjustmentPanel()) { 163 | return; 164 | } 165 | 166 | super.onBackPressed(); 167 | } 168 | 169 | private void setupSpinner() { 170 | ArrayList types = new ArrayList<>(); 171 | for (String[] metadata : ADJUSTMENT_METADATA) { 172 | types.add(metadata[0]); 173 | } 174 | 175 | mAdjustmentTypeSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, types)); 176 | mAdjustmentTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 177 | @Override 178 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 179 | // Workaround for duplicated selecting when restored. 180 | if (mSelectedAdjustment == position) { 181 | return; 182 | } 183 | 184 | mSelectedAdjustment = position; 185 | if (getSupportFragmentManager().findFragmentById(R.id.container2) != null) { 186 | mAdjustmentTypeSpinner.setEnabled(false); 187 | closeAdjustmentPanel(new Runnable() { 188 | @Override 189 | public void run() { 190 | mAdjustmentTypeSpinner.setEnabled(true); 191 | openAdjustmentPanel(); 192 | } 193 | }); 194 | } 195 | } 196 | 197 | @Override 198 | public void onNothingSelected(AdapterView parent) { 199 | 200 | } 201 | }); 202 | } 203 | 204 | private void peekPager() { 205 | final float peekDistance = 206 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics()); 207 | ValueAnimator peekAnimator = ValueAnimator.ofFloat(0, -peekDistance); 208 | peekAnimator.setDuration(450); 209 | peekAnimator.setInterpolator(new FastOutSlowInInterpolator()); 210 | peekAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 211 | float mLastValue = 0; 212 | 213 | @Override 214 | public void onAnimationUpdate(ValueAnimator animation) { 215 | final float currentValue = (float) animation.getAnimatedValue(); 216 | mPager.fakeDragBy(currentValue - mLastValue); 217 | mLastValue = currentValue; 218 | } 219 | }); 220 | peekAnimator.addListener(new SimpleAnimatorListener() { 221 | @Override 222 | public void onAnimationStart(Animator animation) { 223 | mPager.beginFakeDrag(); 224 | } 225 | 226 | @Override 227 | public void onAnimationEnd(Animator animation) { 228 | mPager.postDelayed(new Runnable() { 229 | @Override 230 | public void run() { 231 | mPager.endFakeDrag(); 232 | } 233 | }, 300); 234 | } 235 | }); 236 | peekAnimator.start(); 237 | } 238 | 239 | private BasePreviewFragment getCurrentPreviewFragment() { 240 | Fragment f = ((PreviewPagerAdapter) mPager.getAdapter()).getItem(mPager.getCurrentItem()); 241 | 242 | if (f instanceof BasePreviewFragment) { 243 | return (BasePreviewFragment) f; 244 | } 245 | 246 | return null; 247 | } 248 | 249 | private void openAdjustmentPanel() { 250 | final Fragment f; 251 | 252 | String adjustmentFragmentClassname = ADJUSTMENT_METADATA[mSelectedAdjustment][1]; 253 | try { 254 | Class adjustmentFragmentClass = Class.forName(adjustmentFragmentClassname); 255 | f = (Fragment) adjustmentFragmentClass.newInstance(); 256 | } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) { 257 | e.printStackTrace(); 258 | return; 259 | } 260 | 261 | f.setArguments(mCurrentConfiguration); 262 | 263 | // Perform a slide up fashion on L or higher versions. 264 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 265 | f.setEnterTransition(new Slide(Gravity.BOTTOM).setDuration(500)); 266 | f.postponeEnterTransition(); 267 | } 268 | 269 | FragmentManager fm = getSupportFragmentManager(); 270 | fm.beginTransaction() 271 | .replace(R.id.container2, f) 272 | .commitNow(); 273 | 274 | mPanelContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 275 | @Override 276 | public boolean onPreDraw() { 277 | if (mPanelContainer.getChildCount() == 0) { 278 | // Fragment view is not installed, ignore the callback one time. 279 | return true; 280 | } 281 | 282 | ViewTreeObserver observer = mPanelLayout.getViewTreeObserver(); 283 | if (observer.isAlive()) { 284 | observer.removeOnPreDrawListener(this); 285 | } 286 | 287 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 288 | f.startPostponedEnterTransition(); 289 | } 290 | 291 | mPanelLayout.setVisibility(View.VISIBLE); 292 | mPanelLayout.setTranslationY(mPanelLayout.getHeight()); 293 | mPanelLayout 294 | .animate() 295 | .translationY(0) 296 | .setDuration(400) 297 | .setListener(null) 298 | .start(); 299 | 300 | return true; 301 | } 302 | }); 303 | 304 | mPanelClosed = false; 305 | mFab.hide(); 306 | } 307 | 308 | private boolean closeAdjustmentPanel() { 309 | return closeAdjustmentPanel(null); 310 | } 311 | 312 | private boolean closeAdjustmentPanel(final Runnable callback) { 313 | final FragmentManager fm = getSupportFragmentManager(); 314 | final Fragment f = fm.findFragmentById(R.id.container2); 315 | if (f != null && !mPanelClosed) { 316 | mPanelClosed = true; 317 | mPanelLayout 318 | .animate() 319 | .translationY(mPanelLayout.getHeight()) 320 | .setDuration(300) 321 | .setListener(new SimpleAnimatorListener() { 322 | @Override 323 | public void onAnimationEnd(Animator animation) { 324 | mPanelClosed = false; 325 | mFab.show(); 326 | 327 | fm.beginTransaction() 328 | .remove(f) 329 | .commit(); 330 | 331 | if (callback != null) { 332 | callback.run(); 333 | } 334 | } 335 | }); 336 | } else { 337 | return false; 338 | } 339 | 340 | return true; 341 | } 342 | 343 | private boolean isAdjustmentPanelVisible() { 344 | return getSupportFragmentManager().findFragmentById(R.id.container2) != null; 345 | } 346 | 347 | @Override 348 | public AbsSpringAnimator provideAnimator() { 349 | return ConfigurationResolver.resolveConfiguration(mCurrentConfiguration); 350 | } 351 | 352 | @Override 353 | public void onAdjustmentCommit(Bundle bundle) { 354 | mCurrentConfiguration = bundle; 355 | BasePreviewFragment f = getCurrentPreviewFragment(); 356 | if (f != null) { 357 | f.onStartAnimation(); 358 | } 359 | } 360 | 361 | private class PreviewPagerAdapter extends FragmentPagerAdapter { 362 | 363 | private SparseArray mCache = new SparseArray<>(); 364 | 365 | public PreviewPagerAdapter(FragmentManager fm) { 366 | super(fm); 367 | } 368 | 369 | @Override 370 | public Fragment getItem(int position) { 371 | Fragment f = mCache.get(position); 372 | if (f == null) { 373 | try { 374 | f = (Fragment) PREVIEW_FRAGMENT_CLASSES[position].newInstance(); 375 | mCache.put(position, f); 376 | } catch (InstantiationException | IllegalAccessException e) { 377 | e.printStackTrace(); 378 | } 379 | } 380 | return f; 381 | } 382 | 383 | @Override 384 | public int getCount() { 385 | return PREVIEW_FRAGMENT_CLASSES.length; 386 | } 387 | 388 | } 389 | 390 | } 391 | --------------------------------------------------------------------------------