├── .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 | 
--------------------------------------------------------------------------------
/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 |
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 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/base-colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 | #00BCD4
21 | #00838F
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/base-template-styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
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 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
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 | }
--------------------------------------------------------------------------------