├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── dodola │ │ └── spring │ │ └── layout │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── recyclerview │ │ │ ├── CustomAdapter.java │ │ │ ├── MainActivity.java │ │ │ └── RecyclerViewFragment.java │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_launcher.png │ │ └── tile.9.png │ │ ├── drawable-mdpi │ │ └── ic_launcher.png │ │ ├── drawable-xhdpi │ │ └── ic_launcher.png │ │ ├── drawable-xxhdpi │ │ └── ic_launcher.png │ │ ├── layout-w720dp │ │ └── activity_main.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── recycler_view_frag.xml │ │ └── text_row_item.xml │ │ ├── menu │ │ └── main.xml │ │ ├── mipmap-xxhdpi │ │ └── icon.png │ │ ├── values-sw600dp │ │ ├── template-dimens.xml │ │ └── template-styles.xml │ │ ├── values-v11 │ │ └── template-styles.xml │ │ ├── values-v21 │ │ ├── base-colors.xml │ │ └── base-template-styles.xml │ │ └── values │ │ ├── base-strings.xml │ │ ├── dimens.xml │ │ ├── fragmentview_strings.xml │ │ ├── strings.xml │ │ ├── template-dimens.xml │ │ └── template-styles.xml │ └── test │ └── java │ └── dodola │ └── spring │ └── layout │ └── ExampleUnitTest.java ├── build.gradle ├── demo-min.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── spring ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── dodola │ └── spring │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ ├── com │ │ └── facebook │ │ │ └── rebound │ │ │ ├── AndroidSpringLooperFactory.java │ │ │ ├── AnimationQueue.java │ │ │ ├── BaseSpringSystem.java │ │ │ ├── BouncyConversion.java │ │ │ ├── ChoreographerCompat.java │ │ │ ├── OrigamiValueConverter.java │ │ │ ├── Spring.java │ │ │ ├── SpringChain.java │ │ │ ├── SpringConfig.java │ │ │ ├── SpringConfigRegistry.java │ │ │ ├── SpringListener.java │ │ │ ├── SpringLooper.java │ │ │ ├── SpringSystem.java │ │ │ ├── SpringSystemListener.java │ │ │ ├── SpringUtil.java │ │ │ ├── SteppingLooper.java │ │ │ ├── SynchronousLooper.java │ │ │ └── ui │ │ │ └── Util.java │ └── dodola │ │ └── spring │ │ ├── RecyclerViewWrapper.java │ │ ├── SpringFrameLayout.java │ │ └── SpringListenerExt.java └── res │ └── values │ ├── ids.xml │ └── strings.xml └── test └── java └── dodola └── spring └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/assetWizardSettings.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | .idea/caches 44 | 45 | # Keystore files 46 | # Uncomment the following line if you do not want to check your keystore files in. 47 | #*.jks 48 | 49 | # External native build folder generated in Android Studio 2.2 and later 50 | .externalNativeBuild 51 | 52 | # Google Services (e.g. APIs or Firebase) 53 | google-services.json 54 | 55 | # Freeline 56 | freeline.py 57 | freeline/ 58 | freeline_project_description.json 59 | 60 | # fastlane 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots 64 | fastlane/test_output 65 | fastlane/readme.md 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 dodola 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpringRecyclerView 2 | 3 | 联动列表 4 | 5 | 基于Facebook的 `Rebound` 动画库,为了实现效果,对一部分代码进行了魔改 6 | 7 | 8 | 9 | ![demo-min](demo-min.gif) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android-extensions' 3 | apply plugin: 'kotlin-android' 4 | 5 | android { 6 | compileSdkVersion 28 7 | defaultConfig { 8 | applicationId "dodola.spring.layout" 9 | minSdkVersion 14 10 | targetSdkVersion 28 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation fileTree(include: ['*.jar'], dir: 'libs') 25 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 26 | exclude group: 'com.android.support', module: 'support-annotations' 27 | }) 28 | implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta1' 29 | implementation project(':spring') 30 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 31 | } 32 | repositories { 33 | mavenCentral() 34 | } 35 | -------------------------------------------------------------------------------- /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/baidu/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. 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/androidTest/java/dodola/spring/layout/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package dodola.spring.layout; 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("dodola.spring.layout", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 24 | 25 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/recyclerview/CustomAdapter.java: -------------------------------------------------------------------------------- 1 | 2 | package com.example.android.recyclerview; 3 | 4 | import android.support.v7.widget.RecyclerView; 5 | import android.util.Log; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.TextView; 10 | 11 | import com.facebook.rebound.Spring; 12 | import com.facebook.rebound.SpringChain; 13 | 14 | import dodola.spring.SpringFrameLayout; 15 | 16 | public class CustomAdapter extends RecyclerView.Adapter { 17 | private static final String TAG = "CustomAdapter"; 18 | 19 | private String[] mDataSet; 20 | 21 | public static class ViewHolder extends RecyclerView.ViewHolder { 22 | private final TextView textView; 23 | private final SpringFrameLayout container; 24 | 25 | public ViewHolder(View v) { 26 | super(v); 27 | v.setOnClickListener(new View.OnClickListener() { 28 | @Override 29 | public void onClick(View v) { 30 | Log.d(TAG, "Element " + getAdapterPosition() + " clicked."); 31 | } 32 | }); 33 | textView = (TextView) v.findViewById(R.id.person_name); 34 | container = (SpringFrameLayout) v.findViewById(R.id.container); 35 | } 36 | 37 | public TextView getTextView() { 38 | return textView; 39 | } 40 | } 41 | 42 | private SpringChain mSpringChain; 43 | 44 | public CustomAdapter(String[] dataSet, SpringChain springChain) { 45 | mDataSet = dataSet; 46 | mSpringChain = springChain; 47 | for (int i = 0; i < dataSet.length; i++) { 48 | this.mSpringChain.addSpring(null); 49 | } 50 | } 51 | 52 | @Override 53 | public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { 54 | View v = LayoutInflater.from(viewGroup.getContext()) 55 | .inflate(R.layout.text_row_item, viewGroup, false); 56 | 57 | return new ViewHolder(v); 58 | } 59 | 60 | @Override 61 | public void onBindViewHolder(ViewHolder viewHolder, final int position) { 62 | Log.d(TAG, "Element " + position + " set."); 63 | 64 | viewHolder.getTextView().setText(mDataSet[position]); 65 | viewHolder.container.setPositionInSpringChain(position); 66 | viewHolder.container.setSpringChain(this.mSpringChain); 67 | this.mSpringChain.addSpring(position, viewHolder.container); 68 | Spring sp = this.mSpringChain.getAllSprings().get(position); 69 | viewHolder.container.setTranslationY(0.0f); 70 | viewHolder.container.setLastTranslationY(0.0f); 71 | } 72 | 73 | public void onViewRecycled(ViewHolder holder) { 74 | this.mSpringChain.addSpring(holder.container.getPositionInSpringChain(), null); 75 | holder.container.setTranslationY(0.0f); 76 | holder.container.setLastTranslationY(0.0f); 77 | } 78 | 79 | @Override 80 | public int getItemCount() { 81 | return mDataSet.length; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/recyclerview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.android.recyclerview; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.FragmentActivity; 5 | import android.support.v4.app.FragmentTransaction; 6 | 7 | public class MainActivity extends FragmentActivity { 8 | 9 | public static final String TAG = "MainActivity"; 10 | 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.activity_main); 15 | 16 | if (savedInstanceState == null) { 17 | FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); 18 | RecyclerViewFragment fragment = new RecyclerViewFragment(); 19 | transaction.replace(R.id.sample_content_fragment, fragment); 20 | transaction.commit(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/recyclerview/RecyclerViewFragment.java: -------------------------------------------------------------------------------- 1 | package com.example.android.recyclerview; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.Fragment; 5 | import android.support.v7.widget.GridLayoutManager; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | 12 | import dodola.spring.RecyclerViewWrapper; 13 | 14 | public class RecyclerViewFragment extends Fragment { 15 | 16 | private static final String TAG = "RecyclerViewFragment"; 17 | private static final String KEY_LAYOUT_MANAGER = "layoutManager"; 18 | private static final int SPAN_COUNT = 2; 19 | private static final int DATASET_COUNT = 20; 20 | 21 | private enum LayoutManagerType { 22 | GRID_LAYOUT_MANAGER, 23 | LINEAR_LAYOUT_MANAGER 24 | } 25 | 26 | protected LayoutManagerType mCurrentLayoutManagerType; 27 | 28 | protected RecyclerViewWrapper mRecyclerView; 29 | protected CustomAdapter mAdapter; 30 | protected RecyclerView.LayoutManager mLayoutManager; 31 | protected String[] mDataset; 32 | 33 | @Override 34 | public void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | 37 | initDataset(); 38 | } 39 | 40 | @Override 41 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 42 | Bundle savedInstanceState) { 43 | View rootView = inflater.inflate(R.layout.recycler_view_frag, container, false); 44 | rootView.setTag(TAG); 45 | mRecyclerView = (RecyclerViewWrapper) rootView.findViewById(R.id.recyclerView); 46 | mRecyclerView.setHasFixedSize(true); 47 | 48 | mLayoutManager = new LinearLayoutManager(getActivity()); 49 | 50 | mCurrentLayoutManagerType = LayoutManagerType.LINEAR_LAYOUT_MANAGER; 51 | 52 | setRecyclerViewLayoutManager(mCurrentLayoutManagerType); 53 | mAdapter = new CustomAdapter(mDataset, mRecyclerView.getSpringChain()); 54 | mRecyclerView.setAdapter(mAdapter); 55 | 56 | return rootView; 57 | } 58 | 59 | public void setRecyclerViewLayoutManager(LayoutManagerType layoutManagerType) { 60 | int scrollPosition = 0; 61 | 62 | if (mRecyclerView.getLayoutManager() != null) { 63 | scrollPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager()) 64 | .findFirstCompletelyVisibleItemPosition(); 65 | } 66 | 67 | switch (layoutManagerType) { 68 | case GRID_LAYOUT_MANAGER: 69 | mLayoutManager = new GridLayoutManager(getActivity(), SPAN_COUNT); 70 | mCurrentLayoutManagerType = LayoutManagerType.GRID_LAYOUT_MANAGER; 71 | break; 72 | case LINEAR_LAYOUT_MANAGER: 73 | mLayoutManager = new LinearLayoutManager(getActivity()); 74 | mCurrentLayoutManagerType = LayoutManagerType.LINEAR_LAYOUT_MANAGER; 75 | break; 76 | default: 77 | mLayoutManager = new LinearLayoutManager(getActivity()); 78 | mCurrentLayoutManagerType = LayoutManagerType.LINEAR_LAYOUT_MANAGER; 79 | } 80 | 81 | mRecyclerView.setLayoutManager(mLayoutManager); 82 | mRecyclerView.scrollToPosition(scrollPosition); 83 | } 84 | 85 | @Override 86 | public void onSaveInstanceState(Bundle savedInstanceState) { 87 | savedInstanceState.putSerializable(KEY_LAYOUT_MANAGER, mCurrentLayoutManagerType); 88 | super.onSaveInstanceState(savedInstanceState); 89 | } 90 | 91 | private void initDataset() { 92 | mDataset = new String[DATASET_COUNT]; 93 | for (int i = 0; i < DATASET_COUNT; i++) { 94 | mDataset[i] = "dodola #" + i; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/SpringRecyclerView/36577a3f29e7e7ad5b4c10cc65412e05386774bd/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/tile.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/SpringRecyclerView/36577a3f29e7e7ad5b4c10cc65412e05386774bd/app/src/main/res/drawable-hdpi/tile.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/SpringRecyclerView/36577a3f29e7e7ad5b4c10cc65412e05386774bd/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/SpringRecyclerView/36577a3f29e7e7ad5b4c10cc65412e05386774bd/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/SpringRecyclerView/36577a3f29e7e7ad5b4c10cc65412e05386774bd/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout-w720dp/activity_main.xml: -------------------------------------------------------------------------------- 1 | 16 | 22 | 23 | 29 | 30 | 34 | 35 | 44 | 45 | 46 | 50 | 51 | 57 | 58 | 59 | 60 | 64 | 65 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 16 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/recycler_view_frag.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/text_row_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 13 | 14 | 18 | 19 | 27 | 28 | 35 | 36 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/SpringRecyclerView/36577a3f29e7e7ad5b4c10cc65412e05386774bd/app/src/main/res/mipmap-xxhdpi/icon.png -------------------------------------------------------------------------------- /app/src/main/res/values-sw600dp/template-dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | @dimen/margin_huge 22 | @dimen/margin_medium 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values-sw600dp/template-styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values-v11/template-styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/base-strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | RecyclerView 20 | 21 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 72dp 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/fragmentview_strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | Show Log 18 | Hide Log 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | Element 20 | Grid Layout Manager 21 | Linear Layout Manager 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/template-dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 4dp 22 | 8dp 23 | 16dp 24 | 32dp 25 | 64dp 26 | 27 | 28 | 29 | @dimen/margin_medium 30 | @dimen/margin_medium 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/values/template-styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 34 | 35 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/test/java/dodola/spring/layout/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package dodola.spring.layout; 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 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.31' 5 | repositories { 6 | jcenter() 7 | google() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.5.0-beta03' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | jcenter() 21 | google() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /demo-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/SpringRecyclerView/36577a3f29e7e7ad5b4c10cc65412e05386774bd/demo-min.gif -------------------------------------------------------------------------------- /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 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/SpringRecyclerView/36577a3f29e7e7ad5b4c10cc65412e05386774bd/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jun 01 22:20:09 CST 2019 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-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Baidu, Inc. All Rights Reserved. 3 | */ 4 | include ':app', ':spring' 5 | -------------------------------------------------------------------------------- /spring/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /spring/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android-extensions' 3 | apply plugin: 'kotlin-android' 4 | 5 | android { 6 | compileSdkVersion 28 7 | 8 | defaultConfig { 9 | minSdkVersion 14 10 | targetSdkVersion 28 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | api fileTree(dir: 'libs', include: ['*.jar']) 27 | api 'com.android.support:appcompat-v7:28.0.0' 28 | api 'com.android.support:recyclerview-v7:28.0.0' 29 | api 'com.android.support:cardview-v7:28.0.0' 30 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 31 | 32 | } 33 | repositories { 34 | mavenCentral() 35 | } 36 | -------------------------------------------------------------------------------- /spring/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/baidu/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. 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 | -------------------------------------------------------------------------------- /spring/src/androidTest/java/dodola/spring/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Baidu, Inc. All Rights Reserved. 3 | */ 4 | package dodola.spring; 5 | 6 | import android.content.Context; 7 | import android.support.test.InstrumentationRegistry; 8 | import android.support.test.runner.AndroidJUnit4; 9 | 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | 13 | import static org.junit.Assert.*; 14 | 15 | /** 16 | * Instrumentation test, which will execute on an Android device. 17 | * 18 | * @see Testing documentation 19 | */ 20 | @RunWith(AndroidJUnit4.class) 21 | public class ExampleInstrumentedTest { 22 | @Test 23 | public void useAppContext() throws Exception { 24 | // Context of the app under test. 25 | Context appContext = InstrumentationRegistry.getTargetContext(); 26 | 27 | assertEquals("dodola.spring.test", appContext.getPackageName()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spring/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/AndroidSpringLooperFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | import android.annotation.TargetApi; 14 | import android.os.Build; 15 | import android.os.Handler; 16 | import android.os.SystemClock; 17 | import android.view.Choreographer; 18 | 19 | /** 20 | * Android version of the spring looper that uses the most appropriate frame callback mechanism 21 | * available. It uses Android's {@link Choreographer} when available, otherwise it uses a 22 | * {@link Handler}. 23 | */ 24 | abstract class AndroidSpringLooperFactory { 25 | 26 | /** 27 | * Create an Android {@link SpringLooper} for the detected Android platform. 28 | * @return a SpringLooper 29 | */ 30 | public static SpringLooper createSpringLooper() { 31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 32 | return ChoreographerAndroidSpringLooper.create(); 33 | } else { 34 | return LegacyAndroidSpringLooper.create(); 35 | } 36 | } 37 | 38 | /** 39 | * The base implementation of the Android spring looper, using a {@link Handler} for the 40 | * frame callbacks. 41 | */ 42 | private static class LegacyAndroidSpringLooper extends SpringLooper { 43 | 44 | private final Handler mHandler; 45 | private final Runnable mLooperRunnable; 46 | private boolean mStarted; 47 | private long mLastTime; 48 | 49 | /** 50 | * @return an Android spring looper using a new {@link Handler} instance 51 | */ 52 | public static SpringLooper create() { 53 | return new LegacyAndroidSpringLooper(new Handler()); 54 | } 55 | 56 | public LegacyAndroidSpringLooper(Handler handler) { 57 | mHandler = handler; 58 | mLooperRunnable = new Runnable() { 59 | @Override 60 | public void run() { 61 | if (!mStarted || mSpringSystem == null) { 62 | return; 63 | } 64 | long currentTime = SystemClock.uptimeMillis(); 65 | mSpringSystem.loop(currentTime - mLastTime); 66 | mLastTime = currentTime; 67 | mHandler.post(mLooperRunnable); 68 | } 69 | }; 70 | } 71 | 72 | @Override 73 | public void start() { 74 | if (mStarted) { 75 | return; 76 | } 77 | mStarted = true; 78 | mLastTime = SystemClock.uptimeMillis(); 79 | mHandler.removeCallbacks(mLooperRunnable); 80 | mHandler.post(mLooperRunnable); 81 | } 82 | 83 | @Override 84 | public void stop() { 85 | mStarted = false; 86 | mHandler.removeCallbacks(mLooperRunnable); 87 | } 88 | } 89 | 90 | /** 91 | * The Jelly Bean and up implementation of the spring looper that uses Android's 92 | * {@link Choreographer} instead of a {@link Handler} 93 | */ 94 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 95 | private static class ChoreographerAndroidSpringLooper extends SpringLooper { 96 | 97 | private final Choreographer mChoreographer; 98 | private final Choreographer.FrameCallback mFrameCallback; 99 | private boolean mStarted; 100 | private long mLastTime; 101 | 102 | /** 103 | * @return an Android spring choreographer using the system {@link Choreographer} 104 | */ 105 | public static ChoreographerAndroidSpringLooper create() { 106 | return new ChoreographerAndroidSpringLooper(Choreographer.getInstance()); 107 | } 108 | 109 | public ChoreographerAndroidSpringLooper(Choreographer choreographer) { 110 | mChoreographer = choreographer; 111 | mFrameCallback = new Choreographer.FrameCallback() { 112 | @Override 113 | public void doFrame(long frameTimeNanos) { 114 | if (!mStarted || mSpringSystem == null) { 115 | return; 116 | } 117 | long currentTime = SystemClock.uptimeMillis(); 118 | mSpringSystem.loop(currentTime - mLastTime); 119 | mLastTime = currentTime; 120 | mChoreographer.postFrameCallback(mFrameCallback); 121 | } 122 | }; 123 | } 124 | 125 | @Override 126 | public void start() { 127 | if (mStarted) { 128 | return; 129 | } 130 | mStarted = true; 131 | mLastTime = SystemClock.uptimeMillis(); 132 | mChoreographer.removeFrameCallback(mFrameCallback); 133 | mChoreographer.postFrameCallback(mFrameCallback); 134 | } 135 | 136 | @Override 137 | public void stop() { 138 | mStarted = false; 139 | mChoreographer.removeFrameCallback(mFrameCallback); 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/AnimationQueue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.rebound; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Collection; 14 | import java.util.LinkedList; 15 | import java.util.List; 16 | import java.util.Queue; 17 | 18 | /** 19 | * AnimationQueue provides a way to trigger a delayed stream of animations off of a stream of 20 | * values. Each callback that is added the AnimationQueue will be process the stream delayed by 21 | * the number of animation frames equal to its position in the callback list. This makes it easy 22 | * to build cascading animations. 23 | * 24 | * TODO: Add options for changing the delay after which a callback receives a value from the 25 | * animation queue value stream. 26 | */ 27 | public class AnimationQueue { 28 | 29 | /** 30 | * AnimationQueue.Callback receives the value from the stream that it should use in its onFrame 31 | * method. 32 | */ 33 | public interface Callback { 34 | void onFrame(Double value); 35 | } 36 | 37 | private final ChoreographerCompat mChoreographer; 38 | private final Queue mPendingQueue = new LinkedList(); 39 | private final Queue mAnimationQueue = new LinkedList(); 40 | private final List mCallbacks = new ArrayList(); 41 | private final ArrayList mTempValues = new ArrayList(); 42 | private final ChoreographerCompat.FrameCallback mChoreographerCallback; 43 | private boolean mRunning; 44 | 45 | public AnimationQueue() { 46 | mChoreographer = ChoreographerCompat.getInstance(); 47 | mChoreographerCallback = new ChoreographerCompat.FrameCallback() { 48 | @Override 49 | public void doFrame(long frameTimeNanos) { 50 | onFrame(frameTimeNanos); 51 | } 52 | }; 53 | } 54 | 55 | /* Values */ 56 | 57 | /** 58 | * Add a single value to the pending animation queue. 59 | * @param value the single value to add 60 | */ 61 | public void addValue(Double value) { 62 | mPendingQueue.add(value); 63 | runIfIdle(); 64 | } 65 | 66 | /** 67 | * Add a collection of values to the pending animation value queue 68 | * @param values the collection of values to add 69 | */ 70 | public void addAllValues(Collection values) { 71 | mPendingQueue.addAll(values); 72 | runIfIdle(); 73 | } 74 | 75 | /** 76 | * Clear all pending animation values. 77 | */ 78 | public void clearValues() { 79 | mPendingQueue.clear(); 80 | } 81 | 82 | /* Callbacks */ 83 | 84 | /** 85 | * Add a callback to the AnimationQueue. 86 | * @param callback the callback to add 87 | */ 88 | public void addCallback(Callback callback) { 89 | mCallbacks.add(callback); 90 | } 91 | 92 | /** 93 | * Remove the specified callback from the AnimationQueue. 94 | * @param callback the callback to remove 95 | */ 96 | public void removeCallback(Callback callback) { 97 | mCallbacks.remove(callback); 98 | } 99 | 100 | /** 101 | * Remove any callbacks from the AnimationQueue. 102 | */ 103 | public void clearCallbacks() { 104 | mCallbacks.clear(); 105 | } 106 | 107 | /** 108 | * Start the animation loop if it is not currently running. 109 | */ 110 | private void runIfIdle() { 111 | if (!mRunning) { 112 | mRunning = true; 113 | mChoreographer.postFrameCallback(mChoreographerCallback); 114 | } 115 | } 116 | 117 | /** 118 | * Called every time a new frame is ready to be rendered. 119 | * 120 | * Values are processed FIFO and each callback is given a chance to handle each value when its 121 | * turn comes before a value is poll'd off the AnimationQueue. 122 | * 123 | * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, in the 124 | * nanoTime() timebase. Divide this value by 1000000 to convert it to the 125 | * uptimeMillis() time base. 126 | */ 127 | private void onFrame(long frameTimeNanos) { 128 | Double nextPendingValue = mPendingQueue.poll(); 129 | 130 | int drainingOffset; 131 | if (nextPendingValue != null) { 132 | mAnimationQueue.offer(nextPendingValue); 133 | drainingOffset = 0; 134 | } else { 135 | drainingOffset = Math.max(mCallbacks.size() - mAnimationQueue.size(), 0); 136 | } 137 | 138 | // Copy the values into a temporary ArrayList for processing. 139 | mTempValues.addAll(mAnimationQueue); 140 | for (int i = mTempValues.size() - 1; i > -1; i--) { 141 | Double val = mTempValues.get(i); 142 | int cbIdx = mTempValues.size() - 1 - i + drainingOffset; 143 | if (mCallbacks.size() > cbIdx) { 144 | mCallbacks.get(cbIdx).onFrame(val); 145 | } 146 | } 147 | mTempValues.clear(); 148 | 149 | while (mAnimationQueue.size() + drainingOffset >= mCallbacks.size()) { 150 | mAnimationQueue.poll(); 151 | } 152 | 153 | if (mAnimationQueue.isEmpty() && mPendingQueue.isEmpty()) { 154 | mRunning = false; 155 | } else { 156 | mChoreographer.postFrameCallback(mChoreographerCallback); 157 | } 158 | } 159 | 160 | } 161 | 162 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/BaseSpringSystem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.Collections; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Set; 20 | import java.util.concurrent.CopyOnWriteArraySet; 21 | 22 | /** 23 | * BaseSpringSystem maintains the set of springs within an Application context. It is responsible for 24 | * Running the spring integration loop and maintains a registry of all the Springs it solves for. 25 | * In addition to listening to physics events on the individual Springs in the system, listeners 26 | * can be added to the BaseSpringSystem itself to provide pre and post integration setup. 27 | */ 28 | public class BaseSpringSystem { 29 | 30 | private final Map mSpringRegistry = new HashMap(); 31 | private final Set mActiveSprings = new CopyOnWriteArraySet(); 32 | private final SpringLooper mSpringLooper; 33 | private final CopyOnWriteArraySet mListeners = new CopyOnWriteArraySet(); 34 | private boolean mIdle = true; 35 | 36 | /** 37 | * create a new BaseSpringSystem 38 | * @param springLooper parameterized springLooper to allow testability of the 39 | * physics loop 40 | */ 41 | public BaseSpringSystem(SpringLooper springLooper) { 42 | if (springLooper == null) { 43 | throw new IllegalArgumentException("springLooper is required"); 44 | } 45 | mSpringLooper = springLooper; 46 | mSpringLooper.setSpringSystem(this); 47 | } 48 | 49 | /** 50 | * check if the system is idle 51 | * @return is the system idle 52 | */ 53 | public boolean getIsIdle() { 54 | return mIdle; 55 | } 56 | 57 | /** 58 | * create a spring with a random uuid for its name. 59 | * @return the spring 60 | */ 61 | public Spring createSpring() { 62 | Spring spring = new Spring(this); 63 | registerSpring(spring); 64 | return spring; 65 | } 66 | 67 | /** 68 | * get a spring by name 69 | * @param id id of the spring to retrieve 70 | * @return Spring with the specified key 71 | */ 72 | public Spring getSpringById(String id) { 73 | if (id == null) { 74 | throw new IllegalArgumentException("id is required"); 75 | } 76 | return mSpringRegistry.get(id); 77 | } 78 | 79 | /** 80 | * return all the springs in the simulator 81 | * @return all the springs 82 | */ 83 | public List getAllSprings() { 84 | Collection collection = mSpringRegistry.values(); 85 | List list; 86 | if (collection instanceof List) { 87 | list = (List)collection; 88 | } else { 89 | list = new ArrayList(collection); 90 | } 91 | return Collections.unmodifiableList(list); 92 | } 93 | 94 | /** 95 | * Registers a Spring to this BaseSpringSystem so it can be iterated if active. 96 | * @param spring the Spring to register 97 | */ 98 | void registerSpring(Spring spring) { 99 | if (spring == null) { 100 | throw new IllegalArgumentException("spring is required"); 101 | } 102 | if (mSpringRegistry.containsKey(spring.getId())) { 103 | throw new IllegalArgumentException("spring is already registered"); } 104 | mSpringRegistry.put(spring.getId(), spring); 105 | } 106 | 107 | /** 108 | * Deregisters a Spring from this BaseSpringSystem, so it won't be iterated anymore. The Spring should 109 | * not be used anymore after doing this. 110 | * 111 | * @param spring the Spring to deregister 112 | */ 113 | void deregisterSpring(Spring spring) { 114 | if (spring == null) { 115 | throw new IllegalArgumentException("spring is required"); 116 | } 117 | mActiveSprings.remove(spring); 118 | mSpringRegistry.remove(spring.getId()); 119 | } 120 | 121 | /** 122 | * update the springs in the system 123 | * @param deltaTime delta since last update in millis 124 | */ 125 | void advance(double deltaTime) { 126 | for (Spring spring : mActiveSprings) { 127 | // advance time in seconds 128 | if (spring.systemShouldAdvance()) { 129 | spring.advance(deltaTime / 1000.0); 130 | } else { 131 | mActiveSprings.remove(spring); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * loop the system until idle 138 | */ 139 | public void loop(double ellapsedMillis) { 140 | for (SpringSystemListener listener : mListeners) { 141 | listener.onBeforeIntegrate(this); 142 | } 143 | advance(ellapsedMillis); 144 | if (mActiveSprings.isEmpty()) { 145 | mIdle = true; 146 | } 147 | for (SpringSystemListener listener : mListeners) { 148 | listener.onAfterIntegrate(this); 149 | } 150 | if (mIdle) { 151 | mSpringLooper.stop(); 152 | } 153 | } 154 | 155 | /** 156 | * This is used internally by the {@link Spring}s created by this {@link BaseSpringSystem} to notify 157 | * it has reached a state where it needs to be iterated. This will add the spring to the list of 158 | * active springs on this system and start the iteration if the system was idle before this call. 159 | * @param springId the id of the Spring to be activated 160 | */ 161 | void activateSpring(String springId) { 162 | Spring spring = mSpringRegistry.get(springId); 163 | if (spring == null) { 164 | throw new IllegalArgumentException("springId " + springId + " does not reference a registered spring"); 165 | } 166 | mActiveSprings.add(spring); 167 | if (getIsIdle()) { 168 | mIdle = false; 169 | mSpringLooper.start(); 170 | } 171 | } 172 | 173 | /** listeners **/ 174 | 175 | public void addListener(SpringSystemListener newListener) { 176 | if (newListener == null) { 177 | throw new IllegalArgumentException("newListener is required"); 178 | } 179 | mListeners.add(newListener); 180 | } 181 | 182 | public void removeListener(SpringSystemListener listenerToRemove) { 183 | if (listenerToRemove == null) { 184 | throw new IllegalArgumentException("listenerToRemove is required"); 185 | } 186 | mListeners.remove(listenerToRemove); 187 | } 188 | 189 | public void removeAllListeners() { 190 | mListeners.clear(); 191 | } 192 | } 193 | 194 | 195 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/BouncyConversion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | /** 14 | * This class converts values from the Quartz Composer Bouncy patch into Bouncy QC tension and 15 | * friction values. 16 | */ 17 | public class BouncyConversion { 18 | 19 | private final double mBouncyTension; 20 | private final double mBouncyFriction; 21 | private final double mSpeed; 22 | private final double mBounciness; 23 | 24 | public BouncyConversion(double speed, double bounciness) { 25 | mSpeed = speed; 26 | mBounciness = bounciness; 27 | double b = normalize(bounciness / 1.7, 0, 20.); 28 | b = project_normal(b, 0.0, 0.8); 29 | double s = normalize(speed / 1.7, 0, 20.); 30 | mBouncyTension = project_normal(s, 0.5, 200); 31 | mBouncyFriction = quadratic_out_interpolation(b, b3_nobounce(mBouncyTension), 0.01); 32 | } 33 | 34 | public double getSpeed() { 35 | return mSpeed; 36 | } 37 | 38 | public double getBounciness() { 39 | return mBounciness; 40 | } 41 | 42 | public double getBouncyTension() { 43 | return mBouncyTension; 44 | } 45 | 46 | public double getBouncyFriction() { 47 | return mBouncyFriction; 48 | } 49 | 50 | private double normalize(double value, double startValue, double endValue) { 51 | return (value - startValue) / (endValue - startValue); 52 | } 53 | 54 | private double project_normal(double n, double start, double end) { 55 | return start + (n * (end - start)); 56 | } 57 | 58 | private double linear_interpolation(double t, double start, double end) { 59 | return t * end + (1.f - t) * start; 60 | } 61 | 62 | private double quadratic_out_interpolation(double t, double start, double end) { 63 | return linear_interpolation(2*t - t*t, start, end); 64 | } 65 | 66 | private double b3_friction1(double x) { 67 | return (0.0007 * Math.pow(x, 3)) - (0.031 * Math.pow(x, 2)) + 0.64 * x + 1.28; 68 | } 69 | 70 | private double b3_friction2(double x) { 71 | return (0.000044 * Math.pow(x, 3)) - (0.006 * Math.pow(x, 2)) + 0.36 * x + 2.; 72 | } 73 | 74 | private double b3_friction3(double x) { 75 | return (0.00000045 * Math.pow(x, 3)) - (0.000332 * Math.pow(x, 2)) + 0.1078 * x + 5.84; 76 | } 77 | 78 | private double b3_nobounce(double tension) { 79 | double friction = 0; 80 | if (tension <= 18) { 81 | friction = b3_friction1(tension); 82 | } else if (tension > 18 && tension <= 44) { 83 | friction = b3_friction2(tension); 84 | } else if (tension > 44) { 85 | friction = b3_friction3(tension); 86 | } else { 87 | assert(false); 88 | } 89 | return friction; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/ChoreographerCompat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | import android.annotation.TargetApi; 14 | import android.os.Build; 15 | import android.os.Handler; 16 | import android.os.Looper; 17 | import android.view.Choreographer; 18 | 19 | /** 20 | * Wrapper class for abstracting away availability of the JellyBean Choreographer. If Choreographer 21 | * is unavailable we fallback to using a normal Handler. 22 | */ 23 | public class ChoreographerCompat { 24 | 25 | private static final long ONE_FRAME_MILLIS = 17; 26 | private static final boolean IS_JELLYBEAN_OR_HIGHER = 27 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; 28 | private static ChoreographerCompat __instance = new ChoreographerCompat(); 29 | 30 | private Handler mHandler; 31 | private Choreographer mChoreographer; 32 | 33 | public static ChoreographerCompat getInstance() { 34 | return __instance; 35 | } 36 | 37 | private ChoreographerCompat() { 38 | if (IS_JELLYBEAN_OR_HIGHER) { 39 | mChoreographer = getChoreographer(); 40 | } else { 41 | mHandler = new Handler(Looper.getMainLooper()); 42 | } 43 | } 44 | 45 | public void postFrameCallback(FrameCallback callbackWrapper) { 46 | if (IS_JELLYBEAN_OR_HIGHER) { 47 | choreographerPostFrameCallback(callbackWrapper.getFrameCallback()); 48 | } else { 49 | mHandler.postDelayed(callbackWrapper.getRunnable(), 0); 50 | } 51 | } 52 | 53 | public void postFrameCallbackDelayed(FrameCallback callbackWrapper, long delayMillis) { 54 | if (IS_JELLYBEAN_OR_HIGHER) { 55 | choreographerPostFrameCallbackDelayed(callbackWrapper.getFrameCallback(), delayMillis); 56 | } else { 57 | mHandler.postDelayed(callbackWrapper.getRunnable(), delayMillis + ONE_FRAME_MILLIS); 58 | } 59 | } 60 | 61 | public void removeFrameCallback(FrameCallback callbackWrapper) { 62 | if (IS_JELLYBEAN_OR_HIGHER) { 63 | choreographerRemoveFrameCallback(callbackWrapper.getFrameCallback()); 64 | } else { 65 | mHandler.removeCallbacks(callbackWrapper.getRunnable()); 66 | } 67 | } 68 | 69 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 70 | private Choreographer getChoreographer() { 71 | return Choreographer.getInstance(); 72 | } 73 | 74 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 75 | private void choreographerPostFrameCallback(Choreographer.FrameCallback frameCallback) { 76 | mChoreographer.postFrameCallback(frameCallback); 77 | } 78 | 79 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 80 | private void choreographerPostFrameCallbackDelayed( 81 | Choreographer.FrameCallback frameCallback, 82 | long delayMillis) { 83 | mChoreographer.postFrameCallbackDelayed(frameCallback, delayMillis); 84 | } 85 | 86 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 87 | private void choreographerRemoveFrameCallback(Choreographer.FrameCallback frameCallback) { 88 | mChoreographer.removeFrameCallback(frameCallback); 89 | } 90 | 91 | /** 92 | * This class provides a compatibility wrapper around the JellyBean FrameCallback with methods 93 | * to access cached wrappers for submitting a real FrameCallback to a Choreographer or a Runnable 94 | * to a Handler. 95 | */ 96 | public static abstract class FrameCallback { 97 | 98 | private Runnable mRunnable; 99 | private Choreographer.FrameCallback mFrameCallback; 100 | 101 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 102 | Choreographer.FrameCallback getFrameCallback() { 103 | if (mFrameCallback == null) { 104 | mFrameCallback = new Choreographer.FrameCallback() { 105 | @Override 106 | public void doFrame(long frameTimeNanos) { 107 | FrameCallback.this.doFrame(frameTimeNanos); 108 | } 109 | }; 110 | } 111 | return mFrameCallback; 112 | } 113 | 114 | Runnable getRunnable() { 115 | if (mRunnable == null) { 116 | mRunnable = new Runnable() { 117 | @Override 118 | public void run() { 119 | doFrame(System.nanoTime()); 120 | } 121 | }; 122 | } 123 | return mRunnable; 124 | } 125 | 126 | public abstract void doFrame(long frameTimeNanos); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/OrigamiValueConverter.java: -------------------------------------------------------------------------------- 1 | package com.facebook.rebound; 2 | 3 | /** 4 | * Helper math util to convert tension & friction values from the Origami design tool to values that 5 | * the spring system needs. 6 | */ 7 | public class OrigamiValueConverter { 8 | 9 | public static double tensionFromOrigamiValue(double oValue) { 10 | return oValue == 0 ? 0 : (oValue - 30.0) * 3.62 + 194.0; 11 | } 12 | 13 | public static double origamiValueFromTension(double tension) { 14 | return tension == 0 ? 0 : (tension - 194.0) / 3.62 + 30.0; 15 | } 16 | 17 | public static double frictionFromOrigamiValue(double oValue) { 18 | return oValue == 0 ? 0 : (oValue - 8.0) * 3.0 + 25.0; 19 | } 20 | 21 | public static double origamiValueFromFriction(double friction) { 22 | return friction == 0 ? 0 : (friction - 25.0) / 3.0 + 8.0; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/Spring.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | import java.util.concurrent.CopyOnWriteArraySet; 14 | 15 | /** 16 | * Classical spring implementing Hooke's law with configurable friction and tension. 17 | */ 18 | public class Spring { 19 | 20 | // unique incrementer id for springs 21 | private static int ID = 0; 22 | 23 | // maximum amount of time to simulate per physics iteration in seconds (4 frames at 60 FPS) 24 | private static final double MAX_DELTA_TIME_SEC = 0.064; 25 | // fixed timestep to use in the physics solver in seconds 26 | private static final double SOLVER_TIMESTEP_SEC = 0.001; 27 | private SpringConfig mSpringConfig; 28 | private boolean mOvershootClampingEnabled; 29 | 30 | // storage for the current and prior physics state while integration is occurring 31 | private static class PhysicsState { 32 | double position; 33 | double velocity; 34 | } 35 | 36 | // unique id for the spring in the system 37 | private final String mId; 38 | // all physics simulation objects are final and reused in each processing pass 39 | private final PhysicsState mCurrentState = new PhysicsState(); 40 | private final PhysicsState mPreviousState = new PhysicsState(); 41 | private final PhysicsState mTempState = new PhysicsState(); 42 | private double mStartValue; 43 | private double mEndValue; 44 | private boolean mWasAtRest = true; 45 | // thresholds for determining when the spring is at rest 46 | private double mRestSpeedThreshold = 0.005; 47 | private double mDisplacementFromRestThreshold = 0.005; 48 | private CopyOnWriteArraySet mListeners = new CopyOnWriteArraySet(); 49 | private double mTimeAccumulator = 0; 50 | 51 | private final BaseSpringSystem mSpringSystem; 52 | 53 | /** 54 | * create a new spring 55 | */ 56 | Spring(BaseSpringSystem springSystem) { 57 | if (springSystem == null) { 58 | throw new IllegalArgumentException("Spring cannot be created outside of a BaseSpringSystem"); 59 | } 60 | mSpringSystem = springSystem; 61 | mId = "spring:" + ID++; 62 | setSpringConfig(SpringConfig.defaultConfig); 63 | } 64 | 65 | /** 66 | * Destroys this Spring, meaning that it will be deregistered from its BaseSpringSystem so it won't be 67 | * iterated anymore and will clear its set of listeners. Do not use the Spring after calling this, 68 | * doing so may just cause an exception to be thrown. 69 | */ 70 | public void destroy() { 71 | mListeners.clear(); 72 | mSpringSystem.deregisterSpring(this); 73 | } 74 | 75 | /** 76 | * get the unique id for this spring 77 | * @return the unique id 78 | */ 79 | public String getId() { 80 | return mId; 81 | } 82 | 83 | /** 84 | * set the config class 85 | * @param springConfig config class for the spring 86 | * @return this Spring instance for chaining 87 | */ 88 | public Spring setSpringConfig(SpringConfig springConfig) { 89 | if (springConfig == null) { 90 | throw new IllegalArgumentException("springConfig is required"); 91 | } 92 | mSpringConfig = springConfig; 93 | return this; 94 | } 95 | 96 | /** 97 | * retrieve the spring config for this spring 98 | * @return the SpringConfig applied to this spring 99 | */ 100 | public SpringConfig getSpringConfig() { 101 | return mSpringConfig; 102 | } 103 | 104 | /** 105 | * Set the displaced value to determine the displacement for the spring from the rest value. 106 | * This value is retained and used to calculate the displacement ratio. 107 | * The default signature also sets the Spring at rest to facilitate the common behavior of moving 108 | * a spring to a new position. 109 | * @param currentValue the new start and current value for the spring 110 | * @return the spring for chaining 111 | */ 112 | public Spring setCurrentValue(double currentValue) { 113 | return setCurrentValue(currentValue, true); 114 | } 115 | 116 | /** 117 | * The full signature for setCurrentValue includes the option of not setting the spring at rest 118 | * after updating its currentValue. Passing setAtRest false means that if the endValue of the 119 | * spring is not equal to the currentValue, the physics system will start iterating to resolve 120 | * the spring to the end value. This is almost never the behavior that you want, so the default 121 | * setCurrentValue signature passes true. 122 | * @param currentValue the new start and current value for the spring 123 | * @param setAtRest optionally set the spring at rest after updating its current value. 124 | * see {@link Spring#setAtRest()} 125 | * @return the spring for chaining 126 | */ 127 | public Spring setCurrentValue(double currentValue, boolean setAtRest) { 128 | mStartValue = currentValue; 129 | mCurrentState.position = currentValue; 130 | mSpringSystem.activateSpring(this.getId()); 131 | for (SpringListener listener : mListeners) { 132 | listener.onSpringUpdate(this); 133 | } 134 | if (setAtRest) { 135 | setAtRest(); 136 | } 137 | return this; 138 | } 139 | 140 | /** 141 | * Get the displacement value from the last time setCurrentValue was called. 142 | * @return displacement value 143 | */ 144 | public double getStartValue() { 145 | return mStartValue; 146 | } 147 | 148 | /** 149 | * Get the current 150 | * @return current value 151 | */ 152 | public double getCurrentValue() { 153 | return mCurrentState.position; 154 | } 155 | 156 | /** 157 | * get the displacement of the springs current value from its rest value. 158 | * @return the distance displaced by 159 | */ 160 | public double getCurrentDisplacementDistance() { 161 | return getDisplacementDistanceForState(mCurrentState); 162 | } 163 | 164 | /** 165 | * get the displacement from rest for a given physics state 166 | * @param state the state to measure from 167 | * @return the distance displaced by 168 | */ 169 | private double getDisplacementDistanceForState(PhysicsState state) { 170 | return Math.abs(mEndValue - state.position); 171 | } 172 | 173 | /** 174 | * set the rest value to determine the displacement for the spring 175 | * @param endValue the endValue for the spring 176 | * @return the spring for chaining 177 | */ 178 | public Spring setEndValue(double endValue) { 179 | if (mEndValue == endValue && isAtRest()) { 180 | return this; 181 | } 182 | mStartValue = getCurrentValue(); 183 | mEndValue = endValue; 184 | mSpringSystem.activateSpring(this.getId()); 185 | for (SpringListener listener : mListeners) { 186 | listener.onSpringEndStateChange(this); 187 | } 188 | return this; 189 | } 190 | 191 | /** 192 | * get the rest value used for determining the displacement of the spring 193 | * @return the rest value for the spring 194 | */ 195 | public double getEndValue() { 196 | return mEndValue; 197 | } 198 | 199 | /** 200 | * set the velocity on the spring in pixels per second 201 | * @return the spring for chaining 202 | */ 203 | public Spring setVelocity(double velocity) { 204 | if (velocity == mCurrentState.velocity) { 205 | return this; 206 | } 207 | mCurrentState.velocity = velocity; 208 | mSpringSystem.activateSpring(this.getId()); 209 | return this; 210 | } 211 | 212 | /** 213 | * get the velocity of the spring 214 | * @return the current velocity 215 | */ 216 | public double getVelocity() { 217 | return mCurrentState.velocity; 218 | } 219 | 220 | /** 221 | * Sets the speed at which the spring should be considered at rest. 222 | * @param restSpeedThreshold speed pixels per second 223 | * @return the spring for chaining 224 | */ 225 | public Spring setRestSpeedThreshold(double restSpeedThreshold) { 226 | mRestSpeedThreshold = restSpeedThreshold; 227 | return this; 228 | } 229 | 230 | /** 231 | * Returns the speed at which the spring should be considered at rest in pixels per second 232 | * @return speed in pixels per second 233 | */ 234 | public double getRestSpeedThreshold() { 235 | return mRestSpeedThreshold; 236 | } 237 | 238 | /** 239 | * set the threshold of displacement from rest below which the spring should be considered at rest 240 | * @param displacementFromRestThreshold displacement to consider resting below 241 | * @return the spring for chaining 242 | */ 243 | public Spring setRestDisplacementThreshold(double displacementFromRestThreshold) { 244 | mDisplacementFromRestThreshold = displacementFromRestThreshold; 245 | return this; 246 | } 247 | 248 | /** 249 | * get the threshold of displacement from rest below which the spring should be considered at rest 250 | * @return displacement to consider resting below 251 | */ 252 | public double getRestDisplacementThreshold() { 253 | return mDisplacementFromRestThreshold; 254 | } 255 | 256 | /** 257 | * Force the spring to clamp at its end value to avoid overshooting the target value. 258 | * @param overshootClampingEnabled whether or not to enable overshoot clamping 259 | * @return the spring for chaining 260 | */ 261 | public Spring setOvershootClampingEnabled(boolean overshootClampingEnabled) { 262 | mOvershootClampingEnabled = overshootClampingEnabled; 263 | return this; 264 | } 265 | 266 | /** 267 | * Check if overshoot clamping is enabled. 268 | * @return is overshoot clamping enabled 269 | */ 270 | public boolean isOvershootClampingEnabled() { 271 | return mOvershootClampingEnabled; 272 | } 273 | 274 | /** 275 | * Check if the spring is overshooting beyond its target. 276 | * @return true if the spring is overshooting its target 277 | */ 278 | public boolean isOvershooting() { 279 | return mSpringConfig.tension > 0 && 280 | ((mStartValue < mEndValue && getCurrentValue() > mEndValue) || 281 | (mStartValue > mEndValue && getCurrentValue() < mEndValue)); 282 | } 283 | 284 | /** 285 | * advance the physics simulation in SOLVER_TIMESTEP_SEC sized chunks to fulfill the required 286 | * realTimeDelta. 287 | * The math is inlined inside the loop since it made a huge performance impact when there are 288 | * several springs being advanced. 289 | * @param time clock time 290 | * @param realDeltaTime clock drift 291 | */ 292 | void advance(double realDeltaTime) { 293 | 294 | boolean isAtRest = isAtRest(); 295 | 296 | if (isAtRest && mWasAtRest) { 297 | /* begin debug 298 | Log.d(TAG, "bailing out because we are at rest:" + getName()); 299 | end debug */ 300 | return; 301 | } 302 | 303 | // clamp the amount of realTime to simulate to avoid stuttering in the UI. We should be able 304 | // to catch up in a subsequent advance if necessary. 305 | double adjustedDeltaTime = realDeltaTime; 306 | if (realDeltaTime > MAX_DELTA_TIME_SEC) { 307 | adjustedDeltaTime = MAX_DELTA_TIME_SEC; 308 | } 309 | 310 | /* begin debug 311 | long startTime = System.currentTimeMillis(); 312 | int iterations = 0; 313 | end debug */ 314 | 315 | mTimeAccumulator += adjustedDeltaTime; 316 | 317 | double tension = mSpringConfig.tension; 318 | double friction = mSpringConfig.friction; 319 | 320 | double position = mCurrentState.position; 321 | double velocity = mCurrentState.velocity; 322 | double tempPosition = mTempState.position; 323 | double tempVelocity = mTempState.velocity; 324 | 325 | double aVelocity, aAcceleration; 326 | double bVelocity, bAcceleration; 327 | double cVelocity, cAcceleration; 328 | double dVelocity, dAcceleration; 329 | 330 | double dxdt, dvdt; 331 | 332 | // iterate over the true time 333 | while (mTimeAccumulator >= SOLVER_TIMESTEP_SEC) { 334 | /* begin debug 335 | iterations++; 336 | end debug */ 337 | mTimeAccumulator -= SOLVER_TIMESTEP_SEC; 338 | 339 | if (mTimeAccumulator < SOLVER_TIMESTEP_SEC) { 340 | // This will be the last iteration. Remember the previous state in case we need to 341 | // interpolate 342 | mPreviousState.position = position; 343 | mPreviousState.velocity = velocity; 344 | } 345 | 346 | // Perform an RK4 integration to provide better detection of the acceleration curve via 347 | // sampling of Euler integrations at 4 intervals feeding each derivative into the calculation 348 | // of the next and taking a weighted sum of the 4 derivatives as the final output. 349 | 350 | // This math was inlined since it made for big performance improvements when advancing several 351 | // springs in one pass of the BaseSpringSystem. 352 | 353 | // The initial derivative is based on the current velocity and the calculated acceleration 354 | aVelocity = velocity; 355 | aAcceleration = (tension * (mEndValue - tempPosition)) - friction * velocity; 356 | 357 | // Calculate the next derivatives starting with the last derivative and integrating over the 358 | // timestep 359 | tempPosition = position + aVelocity * SOLVER_TIMESTEP_SEC * 0.5; 360 | tempVelocity = velocity + aAcceleration * SOLVER_TIMESTEP_SEC * 0.5; 361 | bVelocity = tempVelocity; 362 | bAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; 363 | 364 | tempPosition = position + bVelocity * SOLVER_TIMESTEP_SEC * 0.5; 365 | tempVelocity = velocity + bAcceleration * SOLVER_TIMESTEP_SEC * 0.5; 366 | cVelocity = tempVelocity; 367 | cAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; 368 | 369 | tempPosition = position + cVelocity * SOLVER_TIMESTEP_SEC; 370 | tempVelocity = velocity + cAcceleration * SOLVER_TIMESTEP_SEC; 371 | dVelocity = tempVelocity; 372 | dAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; 373 | 374 | // Take the weighted sum of the 4 derivatives as the final output. 375 | dxdt = 1.0/6.0 * (aVelocity + 2.0 * (bVelocity + cVelocity) + dVelocity); 376 | dvdt = 1.0/6.0 * (aAcceleration + 2.0 * (bAcceleration + cAcceleration) + dAcceleration); 377 | 378 | position += dxdt * SOLVER_TIMESTEP_SEC; 379 | velocity += dvdt * SOLVER_TIMESTEP_SEC; 380 | } 381 | 382 | mTempState.position = tempPosition; 383 | mTempState.velocity = tempVelocity; 384 | 385 | mCurrentState.position = position; 386 | mCurrentState.velocity = velocity; 387 | 388 | if (mTimeAccumulator > 0) { 389 | interpolate(mTimeAccumulator / SOLVER_TIMESTEP_SEC); 390 | } 391 | 392 | // End the spring immediately if it is overshooting and overshoot clamping is enabled. 393 | // Also make sure that if the spring was considered within a resting threshold that it's now 394 | // snapped to its end value. 395 | if (isAtRest() || (mOvershootClampingEnabled && isOvershooting())) { 396 | // Don't call setCurrentValue because that forces a call to onSpringUpdate 397 | if (tension > 0) { 398 | mStartValue = mEndValue; 399 | mCurrentState.position = mEndValue; 400 | } else { 401 | mEndValue = mCurrentState.position; 402 | mStartValue = mEndValue; 403 | } 404 | setVelocity(0); 405 | isAtRest = true; 406 | } 407 | 408 | /* begin debug 409 | long endTime = System.currentTimeMillis(); 410 | long elapsedMillis = endTime - startTime; 411 | Log.d(TAG, 412 | "iterations:" + iterations + 413 | " iterationTime:" + elapsedMillis + 414 | " position:" + mCurrentState.position + 415 | " velocity:" + mCurrentState.velocity + 416 | " realDeltaTime:" + realDeltaTime + 417 | " adjustedDeltaTime:" + adjustedDeltaTime + 418 | " isAtRest:" + isAtRest + 419 | " wasAtRest:" + mWasAtRest); 420 | end debug */ 421 | 422 | // NB: do these checks outside the loop so all listeners are properly notified of the state 423 | // transition 424 | boolean notifyActivate = false; 425 | if (mWasAtRest) { 426 | mWasAtRest = false; 427 | notifyActivate = true; 428 | } 429 | boolean notifyAtRest = false; 430 | if (isAtRest) { 431 | mWasAtRest = true; 432 | notifyAtRest = true; 433 | } 434 | for (SpringListener listener : mListeners) { 435 | // starting to move 436 | if (notifyActivate) { 437 | listener.onSpringActivate(this); 438 | } 439 | 440 | // updated 441 | listener.onSpringUpdate(this); 442 | 443 | // coming to rest 444 | if (notifyAtRest) { 445 | listener.onSpringAtRest(this); 446 | } 447 | } 448 | } 449 | 450 | /** 451 | * Check if this spring should be advanced by the system. * The rule is if the spring is 452 | * currently at rest and it was at rest in the previous advance, the system can skip this spring 453 | * @return should the system process this spring 454 | */ 455 | public boolean systemShouldAdvance() { 456 | return !isAtRest() || !wasAtRest(); 457 | } 458 | 459 | /** 460 | * Check if the spring was at rest in the prior iteration. This is used for ensuring the ending 461 | * callbacks are fired as the spring comes to a rest. 462 | * @return true if the spring was at rest in the prior iteration 463 | */ 464 | public boolean wasAtRest() { 465 | return mWasAtRest; 466 | } 467 | 468 | /** 469 | * check if the current state is at rest 470 | * @return is the spring at rest 471 | */ 472 | public boolean isAtRest() { 473 | return Math.abs(mCurrentState.velocity) <= mRestSpeedThreshold && 474 | (getDisplacementDistanceForState(mCurrentState) <= mDisplacementFromRestThreshold || 475 | mSpringConfig.tension == 0); 476 | } 477 | 478 | /** 479 | * Set the spring to be at rest by making its end value equal to its current value and setting 480 | * velocity to 0. 481 | */ 482 | public Spring setAtRest() { 483 | mEndValue = mCurrentState.position; 484 | mTempState.position = mCurrentState.position; 485 | mCurrentState.velocity = 0; 486 | return this; 487 | } 488 | 489 | /** 490 | * linear interpolation between the previous and current physics state based on the amount of 491 | * timestep remaining after processing the rendering delta time in timestep sized chunks. 492 | * @param alpha from 0 to 1, where 0 is the previous state, 1 is the current state 493 | */ 494 | private void interpolate(double alpha) { 495 | mCurrentState.position = mCurrentState.position * alpha + mPreviousState.position *(1-alpha); 496 | mCurrentState.velocity = mCurrentState.velocity * alpha + mPreviousState.velocity *(1-alpha); 497 | } 498 | 499 | /** listeners **/ 500 | 501 | /** 502 | * add a listener 503 | * @param newListener to add 504 | * @return the spring for chaining 505 | */ 506 | public Spring addListener(SpringListener newListener) { 507 | if (newListener == null) { 508 | throw new IllegalArgumentException("newListener is required"); 509 | } 510 | mListeners.add(newListener); 511 | return this; 512 | } 513 | 514 | /** 515 | * remove a listener 516 | * @param listenerToRemove to remove 517 | * @return the spring for chaining 518 | */ 519 | public Spring removeListener(SpringListener listenerToRemove) { 520 | if (listenerToRemove == null) { 521 | throw new IllegalArgumentException("listenerToRemove is required"); 522 | } 523 | mListeners.remove(listenerToRemove); 524 | return this; 525 | } 526 | 527 | /** 528 | * remove all of the listeners 529 | * @return the spring for chaining 530 | */ 531 | public Spring removeAllListeners() { 532 | mListeners.clear(); 533 | return this; 534 | } 535 | 536 | /** 537 | * This method checks to see that the current spring displacement value is equal to the input, 538 | * accounting for the spring's rest displacement threshold. 539 | * @param value The value to compare the spring value to 540 | * @return Whether the displacement value from the spring is within the bounds of the compare 541 | * value, accounting for threshold 542 | */ 543 | public boolean currentValueIsApproximately(double value) { 544 | return Math.abs(getCurrentValue() - value) <= getRestDisplacementThreshold(); 545 | } 546 | 547 | } 548 | 549 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/SpringChain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.rebound; 11 | 12 | import java.util.List; 13 | import java.util.concurrent.CopyOnWriteArrayList; 14 | 15 | /** 16 | * SpringChain is a helper class for creating spring animations with multiple springs in a chain. 17 | * Chains of springs can be used to create cascading animations that maintain individual physics 18 | * state for each member of the chain. One spring in the chain is chosen to be the control spring. 19 | * Springs before and after the control spring in the chain are pulled along by their predecessor. 20 | * You can change which spring is the control spring at any point by calling 21 | * {@link SpringChain#setControlSpringIndex(int)}. 22 | */ 23 | public class SpringChain implements SpringListener { 24 | 25 | /** 26 | * Add these spring configs to the registry to support live tuning through the 27 | * {@link com.facebook.rebound.ui.SpringConfiguratorView} 28 | */ 29 | private static final SpringConfigRegistry registry = SpringConfigRegistry.getInstance(); 30 | private static final int DEFAULT_MAIN_TENSION = 40; 31 | private static final int DEFAULT_MAIN_FRICTION = 6; 32 | private static final int DEFAULT_ATTACHMENT_TENSION = 70; 33 | private static final int DEFAULT_ATTACHMENT_FRICTION = 10; 34 | private static int id = 0; 35 | 36 | 37 | /** 38 | * Factory method for creating a new SpringChain with default SpringConfig. 39 | * 40 | * @return the newly created SpringChain 41 | */ 42 | public static SpringChain create() { 43 | return new SpringChain(); 44 | } 45 | 46 | /** 47 | * Factory method for creating a new SpringChain with the provided SpringConfig. 48 | * 49 | * @param mainTension tension for the main spring 50 | * @param mainFriction friction for the main spring 51 | * @param attachmentTension tension for the attachment spring 52 | * @param attachmentFriction friction for the attachment spring 53 | * @return the newly created SpringChain 54 | */ 55 | public static SpringChain create( 56 | int mainTension, 57 | int mainFriction, 58 | int attachmentTension, 59 | int attachmentFriction) { 60 | return new SpringChain(mainTension, mainFriction, attachmentTension, attachmentFriction); 61 | } 62 | 63 | private final SpringSystem mSpringSystem = SpringSystem.create(); 64 | private final CopyOnWriteArrayList mListeners = 65 | new CopyOnWriteArrayList(); 66 | private final CopyOnWriteArrayList mSprings = new CopyOnWriteArrayList(); 67 | private int mControlSpringIndex = -1; 68 | 69 | // The main spring config defines the tension and friction for the control spring. Keeping these 70 | // values separate allows the behavior of the trailing springs to be different than that of the 71 | // control point. 72 | private final SpringConfig mMainSpringConfig; 73 | 74 | // The attachment spring config defines the tension and friction for the rest of the springs in 75 | // the chain. 76 | private final SpringConfig mAttachmentSpringConfig; 77 | 78 | private SpringChain() { 79 | this( 80 | DEFAULT_MAIN_TENSION, 81 | DEFAULT_MAIN_FRICTION, 82 | DEFAULT_ATTACHMENT_TENSION, 83 | DEFAULT_ATTACHMENT_FRICTION); 84 | } 85 | 86 | private SpringChain( 87 | int mainTension, 88 | int mainFriction, 89 | int attachmentTension, 90 | int attachmentFriction) { 91 | mMainSpringConfig = SpringConfig.fromOrigamiTensionAndFriction(mainTension, mainFriction); 92 | mAttachmentSpringConfig = 93 | SpringConfig.fromOrigamiTensionAndFriction(attachmentTension, attachmentFriction); 94 | registry.addSpringConfig(mMainSpringConfig, "main spring " + id++); 95 | registry.addSpringConfig(mAttachmentSpringConfig, "attachment spring " + id++); 96 | } 97 | 98 | public SpringConfig getMainSpringConfig() { 99 | return mMainSpringConfig; 100 | } 101 | 102 | public SpringConfig getAttachmentSpringConfig() { 103 | return mAttachmentSpringConfig; 104 | } 105 | 106 | /** 107 | * Add a spring to the chain that will callback to the provided listener. 108 | * 109 | * @param listener the listener to notify for this Spring in the chain 110 | * @return this SpringChain for chaining 111 | */ 112 | public SpringChain addSpring(final SpringListener listener) { 113 | // We listen to each spring added to the SpringChain and dynamically chain the springs together 114 | // whenever the control spring state is modified. 115 | Spring spring = mSpringSystem 116 | .createSpring() 117 | .addListener(this) 118 | .setSpringConfig(mAttachmentSpringConfig); 119 | mSprings.add(spring); 120 | mListeners.add(listener); 121 | return this; 122 | } 123 | public int getControlSpringIndex() { 124 | return this.mControlSpringIndex; 125 | } 126 | public SpringChain addSpring(int position, SpringListener listener) { 127 | if (mSprings.size() == position) { 128 | mSprings.add(position, 129 | mSpringSystem.createSpring() 130 | .addListener(this) 131 | .setSpringConfig(this.mAttachmentSpringConfig)); 132 | } else if (mSprings.size() < position) { 133 | } 134 | if (mListeners.size() > position) { 135 | mListeners.set(position, listener); 136 | } else if (mListeners.size() == position) { 137 | mListeners.add(position, listener); 138 | } 139 | return this; 140 | } 141 | 142 | /** 143 | * Set the index of the control spring. This spring will drive the positions of all the springs 144 | * before and after it in the list when moved. 145 | * 146 | * @param i the index to use for the control spring 147 | * @return this SpringChain 148 | */ 149 | public SpringChain setControlSpringIndex(int i) { 150 | mControlSpringIndex = i; 151 | Spring controlSpring = mSprings.get(mControlSpringIndex); 152 | if (controlSpring == null) { 153 | return null; 154 | } 155 | for (Spring spring : mSpringSystem.getAllSprings()) { 156 | spring.setSpringConfig(mAttachmentSpringConfig); 157 | } 158 | getControlSpring().setSpringConfig(mMainSpringConfig); 159 | return this; 160 | } 161 | 162 | /** 163 | * Retrieve the control spring so you can manipulate it to drive the positions of the other 164 | * springs. 165 | * 166 | * @return the control spring. 167 | */ 168 | public Spring getControlSpring() { 169 | return mSprings.get(mControlSpringIndex); 170 | } 171 | 172 | /** 173 | * Retrieve the list of springs in the chain. 174 | * 175 | * @return the list of springs 176 | */ 177 | public List getAllSprings() { 178 | return mSprings; 179 | } 180 | 181 | @Override 182 | public void onSpringUpdate(Spring spring) { 183 | // Get the control spring index and update the endValue of each spring above and below it in the 184 | // spring collection triggering a cascading effect. 185 | int idx = mSprings.indexOf(spring); 186 | if (idx >= 0) { 187 | 188 | SpringListener listener = mListeners.get(idx); 189 | int above = -1; 190 | int below = -1; 191 | if (idx == mControlSpringIndex) { 192 | below = idx - 1; 193 | above = idx + 1; 194 | } else if (idx < mControlSpringIndex) { 195 | below = idx - 1; 196 | } else if (idx > mControlSpringIndex) { 197 | above = idx + 1; 198 | } 199 | if (above > -1 && above < mSprings.size()) { 200 | mSprings.get(above).setEndValue(spring.getCurrentValue()); 201 | } 202 | if (below > -1 && below < mSprings.size()) { 203 | mSprings.get(below).setEndValue(spring.getCurrentValue()); 204 | } 205 | if (listener != null) { 206 | listener.onSpringUpdate(spring); 207 | } 208 | } 209 | } 210 | 211 | 212 | public void onSpringChainUpdate(Spring spring, Spring springControl) { 213 | int idx = mSprings.indexOf(spring); 214 | if (idx >= 0) { 215 | 216 | SpringListener listener = mListeners.get(idx); 217 | int above = -1; 218 | int below = -1; 219 | if (idx == mControlSpringIndex) { 220 | below = idx - 1; 221 | above = idx + 1; 222 | } else if (idx < mControlSpringIndex) { 223 | below = idx - 1; 224 | } else if (idx > mControlSpringIndex) { 225 | above = idx + 1; 226 | } 227 | if (above > -1 && above < mSprings.size()) { 228 | mSprings.get(above).setEndValue(spring.getCurrentValue()); 229 | } 230 | if (below > -1 && below < mSprings.size()) { 231 | mSprings.get(below).setEndValue(spring.getCurrentValue()); 232 | } 233 | Spring controlSpring = mSprings.get(mControlSpringIndex); 234 | if (listener != null) { 235 | listener.onSpringChainUpdate(spring, controlSpring); 236 | } 237 | } 238 | } 239 | 240 | 241 | @Override 242 | public void onSpringAtRest(Spring spring) { 243 | } 244 | 245 | @Override 246 | public void onSpringActivate(Spring spring) { 247 | } 248 | 249 | @Override 250 | public void onSpringEndStateChange(Spring spring) { 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/SpringConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | /** 14 | * Data structure for storing spring configuration. 15 | */ 16 | public class SpringConfig { 17 | public double friction; 18 | public double tension; 19 | 20 | public static SpringConfig defaultConfig = SpringConfig.fromOrigamiTensionAndFriction(40, 7); 21 | 22 | /** 23 | * constructor for the SpringConfig 24 | * @param tension tension value for the SpringConfig 25 | * @param friction friction value for the SpringConfig 26 | */ 27 | public SpringConfig(double tension, double friction) { 28 | this.tension = tension; 29 | this.friction = friction; 30 | } 31 | 32 | /** 33 | * A helper to make creating a SpringConfig easier with values mapping to the Origami values. 34 | * @param qcTension tension as defined in the Quartz Composition 35 | * @param qcFriction friction as defined in the Quartz Composition 36 | * @return a SpringConfig that maps to these values 37 | */ 38 | public static SpringConfig fromOrigamiTensionAndFriction(double qcTension, double qcFriction) { 39 | return new SpringConfig( 40 | OrigamiValueConverter.tensionFromOrigamiValue(qcTension), 41 | OrigamiValueConverter.frictionFromOrigamiValue(qcFriction) 42 | ); 43 | } 44 | 45 | /** 46 | * Map values from the Origami POP Animation patch, which are based on a bounciness and speed 47 | * value. 48 | * @param bounciness bounciness of the POP Animation 49 | * @param speed speed of the POP Animation 50 | * @return a SpringConfig mapping to the specified POP Animation values. 51 | */ 52 | public static SpringConfig fromBouncinessAndSpeed(double bounciness, double speed) { 53 | BouncyConversion bouncyConversion = new BouncyConversion(speed, bounciness); 54 | return fromOrigamiTensionAndFriction( 55 | bouncyConversion.getBouncyTension(), 56 | bouncyConversion.getBouncyFriction()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/SpringConfigRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * class for maintaining a registry of all spring configs 19 | */ 20 | public class SpringConfigRegistry { 21 | 22 | private static final SpringConfigRegistry INSTANCE = new SpringConfigRegistry(true); 23 | 24 | public static SpringConfigRegistry getInstance() { 25 | return INSTANCE; 26 | } 27 | 28 | private final Map mSpringConfigMap; 29 | 30 | /** 31 | * constructor for the SpringConfigRegistry 32 | */ 33 | SpringConfigRegistry(boolean includeDefaultEntry) { 34 | mSpringConfigMap = new HashMap(); 35 | if (includeDefaultEntry) { 36 | addSpringConfig(SpringConfig.defaultConfig, "default config"); 37 | } 38 | } 39 | 40 | /** 41 | * add a SpringConfig to the registry 42 | * 43 | * @param springConfig SpringConfig to add to the registry 44 | * @param configName name to give the SpringConfig in the registry 45 | * @return true if the SpringConfig was added, false if a config with that name is already 46 | * present. 47 | */ 48 | public boolean addSpringConfig(SpringConfig springConfig, String configName) { 49 | if (springConfig == null) { 50 | throw new IllegalArgumentException("springConfig is required"); 51 | } 52 | if (configName == null) { 53 | throw new IllegalArgumentException("configName is required"); 54 | } 55 | if (mSpringConfigMap.containsKey(springConfig)) { 56 | return false; 57 | } 58 | mSpringConfigMap.put(springConfig, configName); 59 | return true; 60 | } 61 | 62 | /** 63 | * remove a specific SpringConfig from the registry 64 | * @param springConfig the of the SpringConfig to remove 65 | * @return true if the SpringConfig was removed, false if it was not present. 66 | */ 67 | public boolean removeSpringConfig(SpringConfig springConfig) { 68 | if (springConfig == null) { 69 | throw new IllegalArgumentException("springConfig is required"); 70 | } 71 | return mSpringConfigMap.remove(springConfig) != null; 72 | } 73 | 74 | /** 75 | * retrieve all SpringConfig in the registry 76 | * @return a list of all SpringConfig 77 | */ 78 | public Map getAllSpringConfig() { 79 | return Collections.unmodifiableMap(mSpringConfigMap); 80 | } 81 | 82 | /** 83 | * clear all SpringConfig in the registry 84 | */ 85 | public void removeAllSpringConfig() { 86 | mSpringConfigMap.clear(); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/SpringListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | import dodola.spring.SpringListenerExt; 14 | 15 | public interface SpringListener extends SpringListenerExt { 16 | 17 | /** 18 | * called whenever the spring is updated 19 | * @param spring the Spring sending the update 20 | */ 21 | void onSpringUpdate(Spring spring); 22 | 23 | /** 24 | * called whenever the spring achieves a resting state 25 | * @param spring the spring that's now resting 26 | */ 27 | void onSpringAtRest(Spring spring); 28 | 29 | /** 30 | * called whenever the spring leaves its resting state 31 | * @param spring the spring that has left its resting state 32 | */ 33 | void onSpringActivate(Spring spring); 34 | 35 | /** 36 | * called whenever the spring notifies of displacement state changes 37 | * @param spring the spring whose end state has changed 38 | */ 39 | void onSpringEndStateChange(Spring spring); 40 | } 41 | 42 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/SpringLooper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | /** 14 | * The spring looper is an interface for implementing platform-dependent run loops. 15 | */ 16 | public abstract class SpringLooper { 17 | 18 | protected BaseSpringSystem mSpringSystem; 19 | 20 | /** 21 | * Set the BaseSpringSystem that the SpringLooper will call back to. 22 | * @param springSystem the spring system to call loop on. 23 | */ 24 | public void setSpringSystem(BaseSpringSystem springSystem) { 25 | mSpringSystem = springSystem; 26 | } 27 | 28 | /** 29 | * The BaseSpringSystem has requested that the looper begins running this {@link Runnable} 30 | * on every frame. The {@link Runnable} will continue running on every frame until 31 | * {@link #stop()} is called. 32 | * If an existing {@link Runnable} had been started on this looper, it will be cancelled. 33 | */ 34 | public abstract void start(); 35 | 36 | /** 37 | * The looper will no longer run the {@link Runnable}. 38 | */ 39 | public abstract void stop(); 40 | } -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/SpringSystem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | /** 14 | * This is a wrapper for BaseSpringSystem that provides the convenience of automatically providing 15 | * the AndroidSpringLooper dependency in {@link SpringSystem#create}. 16 | */ 17 | public class SpringSystem extends BaseSpringSystem { 18 | 19 | /** 20 | * Create a new SpringSystem providing the appropriate constructor parameters to work properly 21 | * in an Android environment. 22 | * @return the SpringSystem 23 | */ 24 | public static SpringSystem create() { 25 | return new SpringSystem(AndroidSpringLooperFactory.createSpringLooper()); 26 | } 27 | 28 | private SpringSystem(SpringLooper springLooper) { 29 | super(springLooper); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/SpringSystemListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | /** 14 | * SpringSystemListener provides an interface for listening to events before and after each Physics 15 | * solving loop the BaseSpringSystem runs. 16 | */ 17 | public interface SpringSystemListener { 18 | 19 | /** 20 | * Runs before each pass through the physics integration loop providing an opportunity to do any 21 | * setup or alterations to the Physics state before integrating. 22 | * @param springSystem the BaseSpringSystem listened to 23 | */ 24 | void onBeforeIntegrate(BaseSpringSystem springSystem); 25 | 26 | /** 27 | * Runs after each pass through the physics integration loop providing an opportunity to do any 28 | * setup or alterations to the Physics state after integrating. 29 | * @param springSystem the BaseSpringSystem listened to 30 | */ 31 | void onAfterIntegrate(BaseSpringSystem springSystem); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/SpringUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | public class SpringUtil { 14 | 15 | /** 16 | * Map a value within a given range to another range. 17 | * @param value the value to map 18 | * @param fromLow the low end of the range the value is within 19 | * @param fromHigh the high end of the range the value is within 20 | * @param toLow the low end of the range to map to 21 | * @param toHigh the high end of the range to map to 22 | * @return the mapped value 23 | */ 24 | public static double mapValueFromRangeToRange( 25 | double value, 26 | double fromLow, 27 | double fromHigh, 28 | double toLow, 29 | double toHigh) { 30 | double fromRangeSize = fromHigh - fromLow; 31 | double toRangeSize = toHigh - toLow; 32 | double valueScale = (value - fromLow) / fromRangeSize; 33 | return toLow + (valueScale * toRangeSize); 34 | } 35 | 36 | /** 37 | * Clamp a value to be within the provided range. 38 | * @param value the value to clamp 39 | * @param low the low end of the range 40 | * @param high the high end of the range 41 | * @return the clamped value 42 | */ 43 | public static double clamp(double value, double low, double high) { 44 | return Math.min(Math.max(value, low), high); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/SteppingLooper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.rebound; 11 | 12 | public class SteppingLooper extends SpringLooper { 13 | 14 | private boolean mStarted; 15 | private long mLastTime; 16 | 17 | @Override 18 | public void start() { 19 | mStarted = true; 20 | mLastTime = 0; 21 | } 22 | 23 | public boolean step(long interval) { 24 | if (mSpringSystem == null || !mStarted) { 25 | return false; 26 | } 27 | long currentTime = mLastTime + interval; 28 | mSpringSystem.loop(currentTime); 29 | mLastTime = currentTime; 30 | return mSpringSystem.getIsIdle(); 31 | } 32 | 33 | @Override 34 | public void stop() { 35 | mStarted = false; 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/SynchronousLooper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.rebound; 11 | 12 | public class SynchronousLooper extends SpringLooper { 13 | 14 | public static double SIXTY_FPS = 16.6667; 15 | private double mTimeStep; 16 | private boolean mRunning; 17 | 18 | public SynchronousLooper() { 19 | mTimeStep = SIXTY_FPS; 20 | } 21 | 22 | public double getTimeStep() { 23 | return mTimeStep; 24 | } 25 | 26 | public void setTimeStep(double timeStep) { 27 | mTimeStep = timeStep; 28 | } 29 | 30 | @Override 31 | public void start() { 32 | mRunning = true; 33 | while (!mSpringSystem.getIsIdle()) { 34 | if (mRunning == false) { 35 | break; 36 | } 37 | mSpringSystem.loop(mTimeStep); 38 | } 39 | } 40 | 41 | @Override 42 | public void stop() { 43 | mRunning = false; 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /spring/src/main/java/com/facebook/rebound/ui/Util.java: -------------------------------------------------------------------------------- 1 | package com.facebook.rebound.ui; 2 | 3 | import android.content.res.Resources; 4 | import android.util.TypedValue; 5 | import android.view.ViewGroup; 6 | import android.widget.FrameLayout; 7 | import android.widget.RelativeLayout; 8 | 9 | /** 10 | * Utilities for generating view hierarchies without using resources. 11 | */ 12 | public abstract class Util { 13 | 14 | public static final int dpToPx(float dp, Resources res) { 15 | return (int) TypedValue.applyDimension( 16 | TypedValue.COMPLEX_UNIT_DIP, 17 | dp, 18 | res.getDisplayMetrics()); 19 | } 20 | 21 | public static final FrameLayout.LayoutParams createLayoutParams(int width, int height) { 22 | return new FrameLayout.LayoutParams(width, height); 23 | } 24 | 25 | public static final FrameLayout.LayoutParams createMatchParams() { 26 | return createLayoutParams( 27 | ViewGroup.LayoutParams.MATCH_PARENT, 28 | ViewGroup.LayoutParams.MATCH_PARENT); 29 | } 30 | 31 | public static final FrameLayout.LayoutParams createWrapParams() { 32 | return createLayoutParams( 33 | ViewGroup.LayoutParams.WRAP_CONTENT, 34 | ViewGroup.LayoutParams.WRAP_CONTENT); 35 | } 36 | 37 | public static final FrameLayout.LayoutParams createWrapMatchParams() { 38 | return createLayoutParams( 39 | ViewGroup.LayoutParams.WRAP_CONTENT, 40 | ViewGroup.LayoutParams.MATCH_PARENT); 41 | } 42 | 43 | public static final FrameLayout.LayoutParams createMatchWrapParams() { 44 | return createLayoutParams( 45 | ViewGroup.LayoutParams.MATCH_PARENT, 46 | ViewGroup.LayoutParams.WRAP_CONTENT); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /spring/src/main/java/dodola/spring/RecyclerViewWrapper.java: -------------------------------------------------------------------------------- 1 | package dodola.spring; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Iterator; 5 | 6 | import android.content.Context; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.util.AttributeSet; 10 | import android.view.MotionEvent; 11 | import android.view.VelocityTracker; 12 | import android.view.View; 13 | 14 | import com.facebook.rebound.SpringChain; 15 | 16 | public class RecyclerViewWrapper extends RecyclerView { 17 | private static ArrayList sListeners = new ArrayList<>(); 18 | private int mActivePointerId = -1; 19 | private boolean mFirstAtTopOrBottom = true; 20 | private boolean mFirstMove = true; 21 | private boolean mReachBottom; 22 | private boolean mReachTop; 23 | private final SpringChain mSpringChain = SpringChain.create(40, 6, 70, 10); 24 | private float mStartY; 25 | private int[] mVelocity; 26 | private VelocityTracker mVelocityTracker; 27 | 28 | public RecyclerViewWrapper(Context context) { 29 | super(context); 30 | init(); 31 | } 32 | 33 | public RecyclerViewWrapper(Context context, AttributeSet attrs) { 34 | super(context, attrs); 35 | init(); 36 | } 37 | 38 | public RecyclerViewWrapper(Context context, AttributeSet attrs, int defStyle) { 39 | super(context, attrs, defStyle); 40 | init(); 41 | } 42 | 43 | private void init() { 44 | mVelocity = new int[6]; 45 | } 46 | 47 | public void setOnScrollListener(OnScrollListener listener) { 48 | super.setOnScrollListener(new OnScrollListener() { 49 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 50 | super.onScrolled(recyclerView, dx, dy); 51 | for (OnScrollListener l : sListeners) { 52 | l.onScrolled(recyclerView, dx, dy); 53 | } 54 | } 55 | }); 56 | sListeners.add(listener); 57 | } 58 | 59 | public void addOnScrollListener(OnScrollListener listener) { 60 | sListeners.add(listener); 61 | } 62 | 63 | public void removeScrollListeners() { 64 | sListeners.clear(); 65 | } 66 | 67 | public boolean onTouchEvent(MotionEvent event) { 68 | if (mVelocityTracker == null) { 69 | mVelocityTracker = VelocityTracker.obtain(); 70 | } 71 | int pointerIndex; 72 | int x; 73 | int y; 74 | int i; 75 | switch (event.getActionMasked()) { 76 | case 1: 77 | case 3: 78 | pointerIndex = event.findPointerIndex(mActivePointerId); 79 | if (pointerIndex >= 0) { 80 | x = (int) (event.getX(pointerIndex) + 0.5f); 81 | y = (int) (event.getY(pointerIndex) + 0.5f); 82 | for (i = 0; i < getChildCount(); i++) { 83 | ((SpringFrameLayout) getChildAt(i).findViewById(R.id.container)).onSpringTouchChanged 84 | (false); 85 | } 86 | if (mReachTop || mReachBottom) { 87 | mVelocityTracker.addMovement(event); 88 | mVelocityTracker.computeCurrentVelocity(16); 89 | mSpringChain.getControlSpring().setEndValue(0.0d); 90 | } else { 91 | mVelocityTracker.addMovement(event); 92 | mVelocityTracker.computeCurrentVelocity(16); 93 | mSpringChain.getControlSpring() 94 | .setCurrentValue((double) mVelocityTracker.getYVelocity()).setEndValue(0.0d); 95 | } 96 | mVelocityTracker.clear(); 97 | mFirstMove = true; 98 | break; 99 | } 100 | return false; 101 | case 2: 102 | int firstVisibleItem = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition(); 103 | int visibleItemCount = getChildCount(); 104 | int totalItemCount = ((LinearLayoutManager) getLayoutManager()).getItemCount(); 105 | if (!mFirstMove) { 106 | pointerIndex = event.findPointerIndex(mActivePointerId); 107 | if (pointerIndex >= 0) { 108 | x = (int) (event.getX(pointerIndex) + 0.5f); 109 | y = (int) (event.getY(pointerIndex) + 0.5f); 110 | if (firstVisibleItem == 0) { 111 | View firstView = getChildAt(firstVisibleItem); 112 | if (firstView == null || firstView.getTop() + getPaddingTop() < 0 113 | || ((float) y) - mStartY <= 0.0f) { 114 | mReachTop = false; 115 | } else { 116 | mReachTop = true; 117 | } 118 | } else { 119 | mReachTop = false; 120 | } 121 | if (firstVisibleItem + visibleItemCount == totalItemCount) { 122 | View lastView = getChildAt(visibleItemCount - 1); 123 | if (lastView == null || lastView.getBottom() + getPaddingBottom() > getHeight() 124 | || totalItemCount < visibleItemCount || ((float) y) - mStartY >= 0.0f) { 125 | mReachBottom = false; 126 | } else { 127 | mReachBottom = true; 128 | } 129 | } else { 130 | mReachBottom = false; 131 | } 132 | for (i = 0; i < getChildCount(); i++) { 133 | SpringFrameLayout layout = (SpringFrameLayout) getChildAt(i).findViewById(R.id 134 | .container); 135 | layout.onSpringScrollChanged(mReachTop, mReachBottom, totalItemCount); 136 | layout.onSpringTouchChanged(true); 137 | } 138 | if (!mReachTop && !mReachBottom) { 139 | mFirstAtTopOrBottom = true; 140 | } else if (mFirstAtTopOrBottom) { 141 | mStartY = (float) y; 142 | mFirstAtTopOrBottom = false; 143 | } 144 | if (!mReachTop && !mReachBottom) { 145 | mVelocityTracker.addMovement(event); 146 | break; 147 | } 148 | float distance = (((float) y) - mStartY) * 0.05f; 149 | mVelocityTracker.clear(); 150 | mSpringChain.getControlSpring().setCurrentValue((double) distance); 151 | if (mReachTop && ((float) y) - mStartY > 0.0f) { 152 | return true; 153 | } 154 | if (mReachBottom && ((float) y) - mStartY < 0.0f) { 155 | return true; 156 | } 157 | } 158 | return false; 159 | } 160 | mActivePointerId = event.getPointerId(0); 161 | int initialTouchX = (int) (event.getX() + 0.5f); 162 | int initialTouchY = (int) (event.getY() + 0.5f); 163 | mVelocityTracker.addMovement(event); 164 | int itemPosition = 165 | getChildLayoutPosition(findChildViewUnder((float) initialTouchX, (float) initialTouchY)); 166 | if (itemPosition == -1) { 167 | itemPosition = ((LinearLayoutManager) getLayoutManager()).findLastVisibleItemPosition(); 168 | } 169 | mSpringChain.setControlSpringIndex(itemPosition).getControlSpring().setCurrentValue(0.0d); 170 | mFirstMove = false; 171 | mFirstAtTopOrBottom = true; 172 | mStartY = (float) initialTouchY; 173 | break; 174 | } 175 | return super.onTouchEvent(event); 176 | } 177 | 178 | public SpringChain getSpringChain() { 179 | return mSpringChain; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /spring/src/main/java/dodola/spring/SpringFrameLayout.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Baidu, Inc. All Rights Reserved. 3 | */ 4 | package dodola.spring; 5 | 6 | import android.content.Context; 7 | import android.util.AttributeSet; 8 | import android.widget.FrameLayout; 9 | 10 | import com.facebook.rebound.Spring; 11 | import com.facebook.rebound.SpringChain; 12 | import com.facebook.rebound.SpringListener; 13 | 14 | public class SpringFrameLayout extends FrameLayout implements SpringListener { 15 | private boolean mIsReachBottom; 16 | private boolean mIsReachTop; 17 | private boolean mIsTouchMove; 18 | private int mItemCount; 19 | private float mLastTranslationY; 20 | private int mPosition; 21 | private SpringChain mSpringChain; 22 | 23 | public SpringFrameLayout(Context context) { 24 | super(context); 25 | } 26 | 27 | public SpringFrameLayout(Context context, AttributeSet attrs) { 28 | super(context, attrs); 29 | } 30 | 31 | public SpringFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { 32 | super(context, attrs, defStyleAttr); 33 | } 34 | 35 | public void onSpringUpdate(Spring spring) { 36 | float val = (float) spring.getCurrentValue(); 37 | setTranslationY((getTranslationY() - mLastTranslationY) + val); 38 | mLastTranslationY = val; 39 | } 40 | 41 | public void onSpringChainUpdate(Spring spring, Spring springControl) { 42 | float distance; 43 | float val = (float) spring.getCurrentValue(); 44 | float valControl = (float) springControl.getCurrentValue(); 45 | int index = mPosition; 46 | int ctrlIndex = mSpringChain.getControlSpringIndex(); 47 | float currentTranslationY = getTranslationY(); 48 | int multiple; 49 | if (mIsReachTop) { 50 | multiple = index; 51 | if (index > ctrlIndex) { 52 | multiple = Math.max(1, ctrlIndex); 53 | } 54 | distance = ((float) multiple) * valControl; 55 | setTranslationY(currentTranslationY + (distance - mLastTranslationY)); 56 | } else if (mIsReachBottom) { 57 | multiple = (mItemCount - index) - 1; 58 | if (index < ctrlIndex) { 59 | multiple = Math.max(1, (mItemCount - ctrlIndex) - 1); 60 | } 61 | distance = ((float) multiple) * valControl; 62 | setTranslationY(currentTranslationY + (distance - mLastTranslationY)); 63 | } else if (mIsTouchMove) { 64 | distance = val - valControl; 65 | setTranslationY(currentTranslationY + (distance - mLastTranslationY)); 66 | } else { 67 | distance = val; 68 | setTranslationY(currentTranslationY + (distance - mLastTranslationY)); 69 | } 70 | mLastTranslationY = distance; 71 | } 72 | 73 | public void onSpringAtRest(Spring spring) { 74 | } 75 | 76 | public void onSpringActivate(Spring spring) { 77 | } 78 | 79 | public void onSpringEndStateChange(Spring spring) { 80 | } 81 | 82 | public void onSpringScrollChanged(boolean isReachTop, boolean isReachBottom, int visibleItemCount) { 83 | mIsReachTop = isReachTop; 84 | mIsReachBottom = isReachBottom; 85 | mItemCount = visibleItemCount; 86 | } 87 | 88 | public void onSpringTouchChanged(boolean isTouchMove) { 89 | mIsTouchMove = isTouchMove; 90 | } 91 | 92 | public void setPositionInSpringChain(int position) { 93 | mPosition = position; 94 | } 95 | 96 | public int getPositionInSpringChain() { 97 | return mPosition; 98 | } 99 | 100 | public void setSpringChain(SpringChain springChain) { 101 | mSpringChain = springChain; 102 | } 103 | 104 | public void setLastTranslationY(float lastTranslationY) { 105 | mLastTranslationY = lastTranslationY; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /spring/src/main/java/dodola/spring/SpringListenerExt.java: -------------------------------------------------------------------------------- 1 | package dodola.spring; 2 | 3 | import com.facebook.rebound.Spring; 4 | 5 | public interface SpringListenerExt { 6 | void onSpringChainUpdate(Spring spring, Spring spring2); 7 | } 8 | -------------------------------------------------------------------------------- /spring/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /spring/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | spring 6 | 7 | -------------------------------------------------------------------------------- /spring/src/test/java/dodola/spring/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Baidu, Inc. All Rights Reserved. 3 | */ 4 | package dodola.spring; 5 | 6 | import org.junit.Test; 7 | 8 | import static org.junit.Assert.*; 9 | 10 | /** 11 | * Example local unit test, which will execute on the development machine (host). 12 | * 13 | * @see Testing documentation 14 | */ 15 | public class ExampleUnitTest { 16 | @Test 17 | public void addition_isCorrect() throws Exception { 18 | assertEquals(4, 2 + 2); 19 | } 20 | } --------------------------------------------------------------------------------