├── elasticity-app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable-hdpi
│ │ │ ├── ic_grid.png
│ │ │ ├── ic_list.png
│ │ │ ├── ic_misc.png
│ │ │ ├── app_icon.png
│ │ │ ├── ic_recycler.png
│ │ │ ├── ic_scroller.png
│ │ │ └── ic_view_pager.png
│ │ ├── drawable-ldpi
│ │ │ ├── ic_grid.png
│ │ │ ├── ic_list.png
│ │ │ ├── ic_misc.png
│ │ │ ├── app_icon.png
│ │ │ ├── ic_recycler.png
│ │ │ ├── ic_scroller.png
│ │ │ └── ic_view_pager.png
│ │ ├── drawable-mdpi
│ │ │ ├── ic_grid.png
│ │ │ ├── ic_list.png
│ │ │ ├── ic_misc.png
│ │ │ ├── app_icon.png
│ │ │ ├── ic_recycler.png
│ │ │ ├── ic_scroller.png
│ │ │ └── ic_view_pager.png
│ │ ├── drawable-xhdpi
│ │ │ ├── app_icon.png
│ │ │ ├── ic_grid.png
│ │ │ ├── ic_list.png
│ │ │ ├── ic_misc.png
│ │ │ ├── ic_recycler.png
│ │ │ ├── ic_scroller.png
│ │ │ └── ic_view_pager.png
│ │ ├── drawable-xxhdpi
│ │ │ ├── ic_grid.png
│ │ │ ├── ic_list.png
│ │ │ ├── ic_misc.png
│ │ │ ├── app_icon.png
│ │ │ ├── ic_recycler.png
│ │ │ ├── ic_scroller.png
│ │ │ └── ic_view_pager.png
│ │ ├── drawable
│ │ │ ├── drawer_items_bkg.xml
│ │ │ └── drawer_header_bkg.xml
│ │ ├── color
│ │ │ └── drawer_items_text.xml
│ │ ├── animator
│ │ │ ├── fade_in_slow.xml
│ │ │ └── fade_out_quick.xml
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ ├── values-v21
│ │ │ └── styles.xml
│ │ ├── layout
│ │ │ ├── grid_item.xml
│ │ │ ├── vertical_list_item.xml
│ │ │ ├── list_item.xml
│ │ │ ├── horizontal_list_item.xml
│ │ │ ├── viewpager_overscroll_demo.xml
│ │ │ ├── scrollview_horizontal_item.xml
│ │ │ ├── scrollview_vertical_item.xml
│ │ │ ├── vert_recycler.xml
│ │ │ ├── activity_overscroll_demo.xml
│ │ │ ├── listview_overscroll_demo.xml
│ │ │ ├── activity_overscroll_demo_content.xml
│ │ │ ├── drawer_header_overscroll_demo.xml
│ │ │ ├── gridview_overscroll_demo.xml
│ │ │ ├── misc_overscroll_demo.xml
│ │ │ ├── recyclerview_stgrid_overscroll_demo.xml
│ │ │ ├── recyclerview_overscroll_demo.xml
│ │ │ └── scrollview_overscroll_demo.xml
│ │ ├── values-w820dp
│ │ │ └── dimens.xml
│ │ └── menu
│ │ │ └── activity_overscroll_demo_drawer_items.xml
│ │ ├── java
│ │ └── xander
│ │ │ └── elasticity
│ │ │ └── demo
│ │ │ ├── control
│ │ │ ├── DemoItem.java
│ │ │ └── DemoContentHelper.java
│ │ │ ├── view
│ │ │ ├── DemoGridAdapter.java
│ │ │ ├── DemoListAdapter.java
│ │ │ ├── DemoRecyclerAdapterVertical.java
│ │ │ ├── DemoRecyclerAdapterHorizontal.java
│ │ │ ├── ScrollViewDemoFragment.java
│ │ │ ├── GridViewDemoFragment.java
│ │ │ ├── MiscViewsDemoFragment.java
│ │ │ ├── RecyclerViewStaggeredGridDemoFragment.java
│ │ │ ├── ListViewDemoFragment.java
│ │ │ ├── DemoRecyclerAdapterBase.java
│ │ │ ├── DemoListAdapterBase.java
│ │ │ ├── ViewPagerDemoFragment.java
│ │ │ └── RecyclerViewDemoFragment.java
│ │ │ └── OverScrollDemoActivity.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── elasticity-lib
├── .gitignore
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── res
│ │ │ └── values
│ │ │ │ └── ids.xml
│ │ └── java
│ │ │ └── xander
│ │ │ └── elasticity
│ │ │ ├── ORIENTATION.java
│ │ │ ├── ElasticityListenerStubs.java
│ │ │ ├── IElasticityState.java
│ │ │ ├── IElasticityUpdateListener.java
│ │ │ ├── adapters
│ │ │ ├── StaticElasticityAdapter.java
│ │ │ ├── IElasticityAdapter.java
│ │ │ ├── HorizontalScrollViewElasticityAdapter.java
│ │ │ ├── ScrollViewElasticityAdapter.java
│ │ │ ├── ViewPagerElasticityAdapter.java
│ │ │ ├── AbsListViewElasticityAdapter.java
│ │ │ └── RecyclerViewElasticityAdapter.java
│ │ │ ├── IElasticityStateListener.java
│ │ │ ├── IElasticity.java
│ │ │ ├── VerticalElasticityBounceEffect.java
│ │ │ ├── HorizontalElasticityBounceEffect.java
│ │ │ ├── ElasticityHelper.java
│ │ │ └── ElasticityBounceEffectBase.java
│ └── test
│ │ └── java
│ │ └── xander
│ │ └── elasticity
│ │ └── android
│ │ └── ui
│ │ └── overscroll
│ │ └── VerticalOverScrollBounceEffectDecoratorTest.java
└── build.gradle
├── demo.gif
├── elasticity-app-debug.apk
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── LICENSE
├── gradlew
├── README_zh.md
└── README.md
/elasticity-app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/elasticity-lib/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/demo.gif
--------------------------------------------------------------------------------
/elasticity-app-debug.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/elasticity-app-debug.apk
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'elasticity'
2 | include ':elasticity-lib'
3 | include ':elasticity-app'
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/workspace.xml
4 | /.idea/libraries
5 | .DS_Store
6 | /build
7 | /captures
8 | /.idea
9 | *iml
10 |
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/drawable-hdpi/ic_grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/elasticity-app/src/main/res/drawable-hdpi/ic_grid.png
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/drawable-hdpi/ic_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/elasticity-app/src/main/res/drawable-hdpi/ic_list.png
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/drawable-hdpi/ic_misc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/elasticity-app/src/main/res/drawable-hdpi/ic_misc.png
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/drawable-ldpi/ic_grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/elasticity-app/src/main/res/drawable-ldpi/ic_grid.png
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/drawable-ldpi/ic_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/elasticity-app/src/main/res/drawable-ldpi/ic_list.png
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/drawable-ldpi/ic_misc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/elasticity-app/src/main/res/drawable-ldpi/ic_misc.png
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/drawable-mdpi/ic_grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/elasticity-app/src/main/res/drawable-mdpi/ic_grid.png
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/drawable-mdpi/ic_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/elasticity-app/src/main/res/drawable-mdpi/ic_list.png
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/drawable-mdpi/ic_misc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xanderwang/elasticity/HEAD/elasticity-app/src/main/res/drawable-mdpi/ic_misc.png
--------------------------------------------------------------------------------
/elasticity-lib/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 | * It is best to call this only when over-scroll isn't currently in-effect - i.e. verify that
27 | *
31 | * Note: Upon detachment completion, the view in question will return to the default
32 | * Android over-scroll configuration (i.e. {@link View.OVER_SCROLL_ALWAYS} mode). This can be
33 | * overridden by calling
25 | * Design-wise, being a standalone class, this decorator powerfully provides the ability to add
26 | * the over-scroll effect over any view without adjusting the view's implementation. In essence, this
27 | * eliminates the need to repeatedly implement the effect per each view type (list-view,
28 | * recycler-view, image-view, etc.). Therefore, using it is highly recommended compared to other
29 | * more intrusive solutions.
31 | * Note that this class is abstract, having {@link HorizontalElasticityBounceEffect} and
32 | * {@link VerticalElasticityBounceEffect} providing concrete implementations that are
33 | * view-orientation specific.
35 | *
38 | * At it's core, the class simply registers itself as a touch-listener over the decorated view and
39 | * intercepts touch events as needed.
41 | * Internally, it delegates the over-scrolling calculations onto 3 state-based classes:
42 | *
Invoked whenever state is transitioned onto one of {@link IElasticityState#STATE_IDLE},
6 | * {@link IElasticityState#STATE_DRAG_START_SIDE}, {@link IElasticityState#STATE_DRAG_END_SIDE}
7 | * or {@link IElasticityState#STATE_BOUNCE_BACK}.
8 | *
9 | * @author amit
10 | *
11 | * @see IElasticityUpdateListener
12 | */
13 | public interface IElasticityStateListener {
14 |
15 | /**
16 | * The invoked callback.
17 | *
18 | * @param decor The associated over-scroll 'decorator'.
19 | * @param oldState The old over-scroll state; ID's specified by {@link IElasticityState}, e.g.
20 | * {@link IElasticityState#STATE_IDLE}.
21 | * @param newState The new over-scroll state; ID's specified by {@link IElasticityState},
22 | * e.g. {@link IElasticityState#STATE_IDLE}.
23 | */
24 | void onOverScrollStateChange(IElasticity decor, int oldState, int newState);
25 | }
26 |
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/layout/activity_overscroll_demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Seeing that {@link HorizontalScrollView} only supports horizontal scrolling, this adapter
12 | * should only be used with a {@link HorizontalElasticityBounceEffect}.
13 | *
14 | * @author amit
15 | *
16 | * @see HorizontalElasticityBounceEffect
17 | * @see VerticalElasticityBounceEffect
18 | */
19 | public class HorizontalScrollViewElasticityAdapter implements IElasticityAdapter {
20 |
21 | protected final HorizontalScrollView mView;
22 |
23 | public HorizontalScrollViewElasticityAdapter(HorizontalScrollView view) {
24 | mView = view;
25 | }
26 |
27 | @Override
28 | public View getView() {
29 | return mView;
30 | }
31 |
32 | @Override
33 | public boolean isInAbsoluteStart() {
34 | return !mView.canScrollHorizontally(-1);
35 | }
36 |
37 | @Override
38 | public boolean isInAbsoluteEnd() {
39 | return !mView.canScrollHorizontally(1);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/elasticity-lib/src/main/java/xander/elasticity/IElasticity.java:
--------------------------------------------------------------------------------
1 | package xander.elasticity;
2 |
3 | import android.view.View;
4 |
5 | /**
6 | * @author amit
7 | */
8 | public interface IElasticity {
9 |
10 | View getView();
11 |
12 | void setOverScrollStateListener(IElasticityStateListener listener);
13 |
14 | void setOverScrollUpdateListener(IElasticityUpdateListener listener);
15 |
16 | /**
17 | * Get the current decorator's runtime state, i.e. one of the values specified by {@link IElasticityState}.
18 | *
19 | * @return The state.
20 | */
21 | int getCurrentState();
22 |
23 | /**
24 | * Detach the decorator from its associated view, thus disabling it entirely.
25 | * getCurrentState()==IElasticityState.STATE_IDLE as a precondition, or otherwise
28 | * use a state listener previously installed using
29 | * {@link #setOverScrollStateListener(IElasticityStateListener)}.View.setOverScrollMode(mode) immediately thereafter.
Seeing that {@link ScrollView} only supports vertical scrolling, this adapter
12 | * should only be used with a {@link VerticalElasticityBounceEffect}. For horizontal
13 | * over-scrolling, use {@link HorizontalScrollViewElasticityAdapter} in conjunction with
14 | * a {@link android.widget.HorizontalScrollView}.
15 | *
16 | * @author amit
17 | *
18 | * @see HorizontalElasticityBounceEffect
19 | * @see VerticalElasticityBounceEffect
20 | */
21 | public class ScrollViewElasticityAdapter implements IElasticityAdapter {
22 |
23 | protected final ScrollView mView;
24 |
25 | public ScrollViewElasticityAdapter(ScrollView view) {
26 | mView = view;
27 | }
28 |
29 | @Override
30 | public View getView() {
31 | return mView;
32 | }
33 |
34 | @Override
35 | public boolean isInAbsoluteStart() {
36 | return !mView.canScrollVertically(-1);
37 | }
38 |
39 | @Override
40 | public boolean isInAbsoluteEnd() {
41 | return !mView.canScrollVertically(1);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/elasticity-app/src/main/res/layout/listview_overscroll_demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
Touch-drag ratio in 'forward' direction will be set to DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD.
64 | *
Touch-drag ratio in 'backwards' direction will be set to DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK.
65 | *
Deceleration factor (for the bounce-back effect) will be set to DEFAULT_DECELERATE_FACTOR.
66 | *
67 | * @param viewAdapter The view's encapsulation.
68 | */
69 | public VerticalElasticityBounceEffect(IElasticityAdapter viewAdapter) {
70 | this(viewAdapter, DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD, DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK, DEFAULT_DECELERATE_FACTOR, MAX_SCALE_FACTOR);
71 | }
72 |
73 | public VerticalElasticityBounceEffect(IElasticityAdapter viewAdapter,float scaleFactor) {
74 | this(viewAdapter, DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD, DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK, DEFAULT_DECELERATE_FACTOR, scaleFactor);
75 | }
76 |
77 | /**
78 | * C'tor, creating the effect with explicit arguments.
79 | *
80 | * @param viewAdapter The view's encapsulation.
81 | * @param touchDragRatioFwd Ratio of touch distance to actual drag distance when in 'forward' direction.
82 | * @param touchDragRatioBck Ratio of touch distance to actual drag distance when in 'backward'
83 | * direction (opposite to initial one).
84 | * @param decelerateFactor Deceleration factor used when decelerating the motion to create the
85 | * bounce-back effect.
86 | */
87 | public VerticalElasticityBounceEffect(IElasticityAdapter viewAdapter, float touchDragRatioFwd, float touchDragRatioBck,
88 | float decelerateFactor, float scaleFactor) {
89 | super(viewAdapter, decelerateFactor, scaleFactor, touchDragRatioFwd, touchDragRatioBck);
90 | }
91 |
92 | @Override
93 | protected MotionAttributes createMotionAttributes() {
94 | return new MotionAttributesVertical();
95 | }
96 |
97 | @Override
98 | protected AnimationAttributes createAnimationAttributes() {
99 | return new AnimationAttributesVertical();
100 | }
101 |
102 | @Override
103 | protected void translateView(View view, boolean dir, float offset) {
104 | Log.d("wxy-motion", String.format("translateView setTag %s", offset));
105 | setViewOffset(view, offset);
106 | view.setPivotX(0.f);
107 | if (dir) {
108 | Log.d("wxy-motion", String.format("translateView setPivotY %s", 0));
109 | view.setPivotY(0.f);
110 | } else {
111 | view.setPivotY(view.getMeasuredHeight());
112 | Log.d("wxy-motion", String.format("translateView setPivotY %s", view.getMeasuredHeight()));
113 | }
114 | view.setScaleY(Math.min(getMaxScaleFactor(), (1.f + Math.abs(offset) / view.getWidth())));
115 | view.postInvalidate();
116 |
117 | // view.setTranslationY(offset);
118 | }
119 |
120 | @Override
121 | protected void translateViewAndEvent(View view, boolean dir, float offset, MotionEvent event) {
122 | Log.d("wxy-motion", String.format("translateViewAndEvent setTag %s", offset));
123 | translateView(view, dir, offset);
124 |
125 | // view.setTranslationY(offset);
126 | event.offsetLocation(offset - event.getY(0), 0f);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/elasticity-lib/src/main/java/xander/elasticity/HorizontalElasticityBounceEffect.java:
--------------------------------------------------------------------------------
1 | package xander.elasticity;
2 |
3 | import android.util.Log;
4 | import android.view.MotionEvent;
5 | import android.view.View;
6 |
7 | import xander.elasticity.adapters.IElasticityAdapter;
8 |
9 | /**
10 | * A concrete implementation of {@link ElasticityBounceEffectBase} for a horizontal orientation.
11 | *
12 | * @author amit
13 | */
14 | public class HorizontalElasticityBounceEffect extends ElasticityBounceEffectBase {
15 |
16 | protected class MotionAttributesHorizontal extends MotionAttributes {
17 |
18 | public boolean init(View view, MotionEvent event) {
19 |
20 | // We must have history available to calc the dx. Normally it's there - if it isn't temporarily,
21 | // we declare the event 'invalid' and expect it in consequent events.
22 | if (event.getHistorySize() == 0) {
23 | return false;
24 | }
25 |
26 | // Allow for counter-orientation-direction operations (e.g. item swiping) to run fluently.
27 | final float dx = event.getX(0) - event.getHistoricalX(0, 0);
28 | final float dy = event.getY(0) - event.getHistoricalY(0, 0);
29 | if (Math.abs(dx) < Math.abs(dy)) {
30 | return false;
31 | }
32 |
33 | if( dx == 0.f ) { // just click event
34 | return false;
35 | }
36 |
37 | // mAbsOffset = view.getTranslationX();
38 | mAbsOffset = getViewOffset(view);
39 | mDeltaOffset = dx;
40 | mDir = mDeltaOffset > 0;
41 | Log.d("wxy-motion", String.format("mAbsOffset %s mDeltaOffset %s", mAbsOffset, mDeltaOffset));
42 | Log.d("wxy-motion","mDir = " + mDir);
43 | return true;
44 | }
45 | }
46 |
47 | protected class AnimationAttributesHorizontal extends AnimationAttributes {
48 |
49 | public AnimationAttributesHorizontal() {
50 | mProperty = View.TRANSLATION_X;
51 | // mProperty = View.SCALE_X;
52 | }
53 |
54 | @Override
55 | protected void init(View view) {
56 | // mAbsOffset = view.getTranslationX();
57 | mAbsOffset = getViewOffset(view);
58 | mMaxOffset = view.getWidth();
59 | }
60 | }
61 |
62 | /**
63 | * C'tor, creating the effect with default arguments:
64 | *
Touch-drag ratio in 'forward' direction will be set to DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD.
65 | *
Touch-drag ratio in 'backwards' direction will be set to DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK.
66 | *
Deceleration factor (for the bounce-back effect) will be set to DEFAULT_DECELERATE_FACTOR.
67 | *
68 | * @param viewAdapter The view's encapsulation.
69 | */
70 | public HorizontalElasticityBounceEffect(IElasticityAdapter viewAdapter) {
71 | this(viewAdapter, DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD, DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK, DEFAULT_DECELERATE_FACTOR, MAX_SCALE_FACTOR);
72 | }
73 |
74 | public HorizontalElasticityBounceEffect(IElasticityAdapter viewAdapter, float scaleFactor) {
75 | this(viewAdapter, DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD, DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK, DEFAULT_DECELERATE_FACTOR, scaleFactor);
76 | }
77 |
78 | /**
79 | * C'tor, creating the effect with explicit arguments.
80 | *
81 | * @param viewAdapter The view's encapsulation.
82 | * @param touchDragRatioFwd Ratio of touch distance to actual drag distance when in 'forward' direction.
83 | * @param touchDragRatioBck Ratio of touch distance to actual drag distance when in 'backward'
84 | * direction (opposite to initial one).
85 | * @param decelerateFactor Deceleration factor used when decelerating the motion to create the
86 | * bounce-back effect.
87 | */
88 | public HorizontalElasticityBounceEffect(IElasticityAdapter viewAdapter, float touchDragRatioFwd, float touchDragRatioBck,
89 | float decelerateFactor, float scaleFactor) {
90 | super(viewAdapter, decelerateFactor, scaleFactor, touchDragRatioFwd, touchDragRatioBck);
91 | }
92 |
93 | @Override
94 | protected MotionAttributes createMotionAttributes() {
95 | return new MotionAttributesHorizontal();
96 | }
97 |
98 | @Override
99 | protected AnimationAttributes createAnimationAttributes() {
100 | return new AnimationAttributesHorizontal();
101 | }
102 |
103 | @Override
104 | protected void translateView(View view, boolean dir, float offset) {
105 | Log.d("wxy-motion", String.format("translateView setTag %s", offset));
106 | setViewOffset(view, offset);
107 | view.setPivotY(0.f);
108 | if (dir) {
109 | Log.d("wxy-motion", String.format("setPivotX setTag %s", 0));
110 | view.setPivotX(0.f);
111 | } else {
112 | Log.d("wxy-motion", String.format("setPivotX setTag %s", view.getMeasuredWidth()));
113 | view.setPivotX(view.getMeasuredWidth());
114 | }
115 | view.setScaleX(Math.min(getMaxScaleFactor(), (1.f + Math.abs(offset) / view.getWidth())));
116 | view.postInvalidate();
117 | // view.setTranslationX(offset);
118 | }
119 |
120 | @Override
121 | protected void translateViewAndEvent(View view, boolean dir, float offset, MotionEvent event) {
122 | Log.d("wxy-motion", String.format("translateViewAndEvent setTag %s", offset));
123 | translateView(view, dir, offset);
124 |
125 | // view.setTranslationX(offset);
126 | event.offsetLocation(offset - event.getX(0), 0f);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/elasticity-lib/src/main/java/xander/elasticity/ElasticityHelper.java:
--------------------------------------------------------------------------------
1 | package xander.elasticity;
2 |
3 | import android.support.v4.view.ViewPager;
4 | import android.support.v7.widget.GridLayoutManager;
5 | import android.support.v7.widget.LinearLayoutManager;
6 | import android.support.v7.widget.RecyclerView;
7 | import android.support.v7.widget.StaggeredGridLayoutManager;
8 | import android.view.View;
9 | import android.widget.GridView;
10 | import android.widget.HorizontalScrollView;
11 | import android.widget.ListView;
12 | import android.widget.ScrollView;
13 |
14 | import xander.elasticity.adapters.AbsListViewElasticityAdapter;
15 | import xander.elasticity.adapters.HorizontalScrollViewElasticityAdapter;
16 | import xander.elasticity.adapters.RecyclerViewElasticityAdapter;
17 | import xander.elasticity.adapters.ScrollViewElasticityAdapter;
18 | import xander.elasticity.adapters.StaticElasticityAdapter;
19 | import xander.elasticity.adapters.ViewPagerElasticityAdapter;
20 |
21 | /**
22 | * @author amit
23 | */
24 | public class ElasticityHelper {
25 |
26 | /**
27 | * Set up the over-scroll effect over a specified {@link RecyclerView} view.
28 | *
Only recycler-views using native Android layout managers (i.e. {@link LinearLayoutManager},
29 | * {@link GridLayoutManager} and {@link StaggeredGridLayoutManager}) are currently supported
30 | * by this convenience method.
31 | *
32 | * @param recyclerView The view.
33 | * @param orientation One of {@link ORIENTATION}
34 | * @return The over-scroll effect 'decorator', enabling further effect configuration.
35 | */
36 | public static IElasticity setUpOverScroll(RecyclerView recyclerView, ORIENTATION orientation) {
37 | return setUpOverScroll(recyclerView, orientation, ElasticityBounceEffectBase.MAX_SCALE_FACTOR);
38 | }
39 |
40 | /**
41 | * Set up the over-scroll effect over a specified {@link RecyclerView} view.
42 | *
Only recycler-views using native Android layout managers (i.e. {@link LinearLayoutManager},
43 | * {@link GridLayoutManager} and {@link StaggeredGridLayoutManager}) are currently supported
44 | * by this convenience method.
45 | *
46 | * @param recyclerView The view.
47 | * @param orientation One of {@link ORIENTATION}
48 | * @param scaleFactor the scale factor for view , defalte is 1.2f
49 | * @return The over-scroll effect 'decorator', enabling further effect configuration.
50 | */
51 | public static IElasticity setUpOverScroll(RecyclerView recyclerView, ORIENTATION orientation, float scaleFactor) {
52 | if (orientation == ORIENTATION.HORIZONTAL) {
53 | return new HorizontalElasticityBounceEffect(new RecyclerViewElasticityAdapter(recyclerView), scaleFactor);
54 | } else if (orientation == ORIENTATION.VERTICAL) {
55 | return new VerticalElasticityBounceEffect(new RecyclerViewElasticityAdapter(recyclerView), scaleFactor);
56 | } else {
57 | throw new IllegalArgumentException("orientation");
58 | }
59 | }
60 |
61 | public static IElasticity setUpOverScroll(ListView listView, float scaleFactor) {
62 | return new VerticalElasticityBounceEffect(new AbsListViewElasticityAdapter(listView), scaleFactor);
63 | }
64 |
65 | public static IElasticity setUpOverScroll(ListView listView) {
66 | return setUpOverScroll(listView, ElasticityBounceEffectBase.MAX_SCALE_FACTOR);
67 | }
68 |
69 | public static IElasticity setUpOverScroll(GridView gridView, float scaleFactor) {
70 | return new VerticalElasticityBounceEffect(new AbsListViewElasticityAdapter(gridView), scaleFactor);
71 | }
72 |
73 | public static IElasticity setUpOverScroll(GridView gridView) {
74 | return setUpOverScroll(gridView, ElasticityBounceEffectBase.MAX_SCALE_FACTOR);
75 | }
76 |
77 | public static IElasticity setUpOverScroll(ScrollView scrollView, float scaleFactor) {
78 | return new VerticalElasticityBounceEffect(new ScrollViewElasticityAdapter(scrollView), scaleFactor);
79 | }
80 |
81 | public static IElasticity setUpOverScroll(ScrollView scrollView) {
82 | return setUpOverScroll(scrollView, ElasticityBounceEffectBase.MAX_SCALE_FACTOR);
83 | }
84 |
85 | public static IElasticity setUpOverScroll(HorizontalScrollView scrollView, float scaleFactor) {
86 | return new HorizontalElasticityBounceEffect(new HorizontalScrollViewElasticityAdapter(scrollView), scaleFactor);
87 | }
88 |
89 | public static IElasticity setUpOverScroll(HorizontalScrollView scrollView) {
90 | return setUpOverScroll(scrollView, ElasticityBounceEffectBase.MAX_SCALE_FACTOR);
91 | }
92 |
93 | /**
94 | * Set up the over-scroll over a generic view, assumed to always be over-scroll ready (e.g.
95 | * a plain text field, image view).
96 | *
97 | * @param view The view.
98 | * @param orientation One {@link ORIENTATION}
99 | * @return The over-scroll effect 'decorator', enabling further effect configuration.
100 | */
101 | public static IElasticity setUpStaticOverScroll(View view, ORIENTATION orientation) {
102 | return setUpStaticOverScroll(view, orientation, ElasticityBounceEffectBase.MAX_SCALE_FACTOR);
103 | }
104 |
105 | /**
106 | * Set up the over-scroll over a generic view, assumed to always be over-scroll ready (e.g.
107 | * a plain text field, image view).
108 | *
109 | * @param view The view.
110 | * @param orientation One {@link ORIENTATION}
111 | * @param scaleFactor the scale factor for view , defalte is 1.2f
112 | * @return The over-scroll effect 'decorator', enabling further effect configuration.
113 | */
114 | public static IElasticity setUpStaticOverScroll(View view, ORIENTATION orientation, float scaleFactor) {
115 | if (orientation == ORIENTATION.HORIZONTAL) {
116 | return new HorizontalElasticityBounceEffect(new StaticElasticityAdapter(view), scaleFactor);
117 | } else if (orientation == ORIENTATION.VERTICAL) {
118 | return new VerticalElasticityBounceEffect(new StaticElasticityAdapter(view), scaleFactor);
119 | } else {
120 | throw new IllegalArgumentException("orientation");
121 | }
122 | }
123 |
124 | public static IElasticity setUpOverScroll(ViewPager viewPager, float scaleFactor) {
125 | return new HorizontalElasticityBounceEffect(new ViewPagerElasticityAdapter(viewPager), scaleFactor);
126 | }
127 |
128 | public static IElasticity setUpOverScroll(ViewPager viewPager) {
129 | return setUpOverScroll(viewPager, ElasticityBounceEffectBase.MAX_SCALE_FACTOR);
130 | }
131 |
132 | }
133 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 |
2 | # 首先申明:这个库是基于 [Over-Scroll](https://github.com/EverythingMe/overscroll-decor)
3 |
4 | ---
5 |
6 | # Elasticity 支持 Android 原生控件 RecyclerView, ListView, GridView, ScrollView ...
7 |
8 | 这个库可以让几乎所有的 Android View 具有类似 MIUI 系统里面的一个弹性拉伸的效果。具体的效果可以参考下面的动图。
9 |
10 | [demo apk](elasticity-app-debug.apk)
11 |
12 | 
13 |
14 | # Gradle 依赖
15 |
16 | 在你的项目的 `build.gradle` 文件添加如下内容:
17 | ```groovy
18 | allprojects {
19 | repositories {
20 | ...
21 | maven { url "https://jitpack.io" }
22 | }
23 | }
24 | ```
25 |
26 | 在你的 module 的 `build.gradle` 文件中添加如下内容:
27 |
28 | ```groovy
29 | dependencies {
30 | // ...
31 |
32 | compile 'com.github.XanderWang:elasticity:1.0.1'
33 | }
34 | ```
35 |
36 | # 使用
37 |
38 | ### RecyclerView
39 | 支持线性和瀑布流的 layout managers,可以很容易接入。实例接入代码如下:
40 |
41 | ```java
42 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
43 |
44 | // Horizontal
45 | ElasticityHelper.setUpOverScroll(recyclerView, ORIENTATION.HORIZONTAL);
46 | // Vertical
47 | ElasticityHelper.setUpOverScroll(recyclerView, ORIENTATION.VERTICAL);
48 | ```
49 |
50 | ### RecyclerView items 的 swiping / dragging 支持
51 | 查看 _高级用法_ .
52 |
53 |
54 | ### ListView
55 |
56 | ```java
57 | ListView listView = (ListView) findViewById(R.id.list_view);
58 | ElasticityHelper.setUpOverScroll(listView);
59 | ```
60 |
61 | ### GridView
62 |
63 | ```java
64 | GridView gridView = (GridView) findViewById(R.id.grid_view);
65 | ElasticityHelper.setUpOverScroll(gridView);
66 | ```
67 |
68 | ### ViewPager
69 |
70 | ```java
71 | ViewPager viewPager = (ViewPager) findViewById(R.id.view_pager);
72 | ElasticityHelper.setUpOverScroll(viewPager);
73 | ```
74 |
75 | ### ScrollView, HorizontalScrollView
76 |
77 | ```java
78 | ScrollView scrollView = (ScrollView) findViewById(R.id.scroll_view);
79 | ElasticityHelper.setUpOverScroll(scrollView);
80 |
81 | HorizontalScrollView horizontalScrollView = (HorizontalScrollView) findViewById(R.id.horizontal_scroll_view);
82 | ElasticityHelper.setUpOverScroll(horizontalScrollView);
83 | ```
84 |
85 | ### 任何 View - Text, Image... (可以认为一开始就是 Over-Scroll 状态)
86 |
87 | ```java
88 | View view = findViewById(R.id.demo_view);
89 |
90 | // Horizontal
91 | ElasticityHelper.setUpStaticOverScroll(view, ORIENTATION.HORIZONTAL);
92 | // Vertical
93 | ElasticityHelper.setUpStaticOverScroll(view, ORIENTATION.VERTICAL);
94 | ```
95 |
96 | # 高级用法
97 |
98 | ```java
99 | // Horizontal RecyclerView
100 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
101 | new HorizontalElasticityBounceEffect(new RecyclerViewElasticityAdapter(recyclerView));
102 |
103 | // ListView (vertical)
104 | ListView listView = (ListView) findViewById(R.id.list_view);
105 | new VerticalElasticityBounceEffect(new AbsListViewElasticityAdapter(listView));
106 |
107 | // GridView (vertical)
108 | GridView gridView = (GridView) findViewById(R.id.grid_view);
109 | new VerticalElasticityBounceEffect(new AbsListViewElasticityAdapter(gridView));
110 |
111 | // ViewPager
112 | ViewPager viewPager = (ViewPager) findViewById(R.id.view_pager);
113 | new HorizontalElasticityBounceEffect(new ViewPagerElasticityAdapter(viewPager));
114 |
115 | // A simple TextView - horizontal
116 | View textView = findViewById(R.id.title);
117 | new HorizontalElasticityBounceEffect(new StaticElasticityAdapter(view));
118 | ```
119 |
120 | ### RecyclerView 借助 [ItemTouchHelper](http://developer.android.com/reference/android/support/v7/widget/helper/ItemTouchHelper.html) 实现 item swiping / dragging
121 | 理论上可以很好的支持 item swiping / dragging (based on [ItemTouchHelper](http://developer.android.com/reference/android/support/v7/widget/helper/ItemTouchHelper.html)), 但是还是建议按如下代码使用。
122 |
123 | ```java
124 | // Normally you would attach an ItemTouchHelper & a callback to a RecyclerView, this way:
125 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
126 | ItemTouchHelper.Callback myCallback = new ItemTouchHelper.Callback() {
127 | ...
128 | };
129 | ItemTouchHelper myHelper = new ItemTouchHelper(myCallback);
130 | myHelper.attachToRecyclerView(recyclerView);
131 |
132 | // INSTEAD of attaching the helper yourself, simply use the dedicated adapter c'tor, e.g.:
133 | new VerticalElasticityBounceEffect(new RecyclerViewElasticityAdapter(recyclerView, myCallback));
134 |
135 | ```
136 |
137 | 查看更多的 swiping / dragging 原理, 可以参考 [this useful tutorial](https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf).
138 |
139 | ### Over-Scroll Listeners
140 | 提供 2 种监听的方式来获取滚动过程中的状态,具体如下
141 |
142 | #### State-Change Listener
143 | 状态改变监听,通过这个监听回调,你可以知道状态的改变,使用范例如下:
144 |
145 | ```java
146 |
147 | // Note: over-scroll is set-up using the helper method.
148 | IElasticity elasticity = ElasticityHelper.setUpOverScroll(recyclerView, ORIENTATION.HORIZONTAL);
149 |
150 | elasticity.setOverScrollStateListener(new IElasticityStateListener() {
151 | @Override
152 | public void onOverScrollStateChange(IElasticity elasticity, int oldState, int newState) {
153 | switch (newState) {
154 | case STATE_IDLE:
155 | // No over-scroll is in effect.
156 | break;
157 | case STATE_DRAG_START_SIDE:
158 | // Dragging started at the left-end.
159 | break;
160 | case STATE_DRAG_END_SIDE:
161 | // Dragging started at the right-end.
162 | break;
163 | case STATE_BOUNCE_BACK:
164 | if (oldState == STATE_DRAG_START_SIDE) {
165 | // Dragging stopped -- view is starting to bounce back from the *left-end* onto natural position.
166 | } else { // i.e. (oldState == STATE_DRAG_END_SIDE)
167 | // View is starting to bounce back from the *right-end*.
168 | }
169 | break;
170 | }
171 | }
172 | }
173 | ```
174 |
175 | #### Real-time Updates Listener
176 | 滑动过程监听,可以监听滑动过程中手势的具体变化。
177 |
178 | ```java
179 | // Note: over-scroll is set-up by explicity instantiating a decorator rather than using the helper; The two methods can be used interchangeably for registering listeners.
180 | IElasticity elasticity = new VerticalElasticityBounceEffect(new RecyclerViewElasticityAdapter(recyclerView, itemTouchHelperCallback));
181 |
182 | elasticity.setOverScrollUpdateListener(new IElasticityUpdateListener() {
183 | @Override
184 | public void onOverScrollUpdate(IElasticity elasticity, int state, float offset) {
185 | final View view = elasticity.getView();
186 | if (offset > 0) {
187 | // 'view' is currently being over-scrolled from the top.
188 | } else if (offset < 0) {
189 | // 'view' is currently being over-scrolled from the bottom.
190 | } else {
191 | // No over-scroll is in-effect.
192 | // This is synonymous with having (state == STATE_IDLE).
193 | }
194 | }
195 | });
196 |
197 | ```
198 |
199 | 这两个监听可以单独使用,也可以同时使用,具体看你的需求。
200 |
201 | ### 自定义 Views
202 |
203 | ```java
204 | public class CustomView extends View {
205 | // ...
206 | }
207 |
208 | final CustomView view = (CustomView) findViewById(R.id.custom_view);
209 | new VerticalElasticityBounceEffect(new IElasticityAdapter() {
210 |
211 | @Override
212 | public View getView() {
213 | return view;
214 | }
215 |
216 | @Override
217 | public boolean isInAbsoluteStart() {
218 | // canScrollUp() is an example of a method you must implement
219 | return !view.canScrollUp();
220 | }
221 |
222 | @Override
223 | public boolean isInAbsoluteEnd() {
224 | // canScrollDown() is an example of a method you must implement
225 | return !view.canScrollDown();
226 | }
227 | });
228 | ```
229 |
230 | ### 完全自定义
231 |
232 | ```java
233 | /// Make over-scroll applied over a list-view feel more 'stiff'
234 | new VerticalElasticityBounceEffect(new AbsListViewElasticityAdapter(view),
235 | 5f, // Default is 3
236 | VerticalElasticityBounceEffect.DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK,
237 | VerticalElasticityBounceEffect.DEFAULT_DECELERATE_FACTOR,
238 | VerticalElasticityBounceEffect.MAX_SCALE_FACTOR);
239 |
240 | // Make over-scroll applied over a list-view bounce-back more softly
241 | new VerticalElasticityBounceEffect(new AbsListViewElasticityAdapter(view),
242 | VerticalElasticityBounceEffect.DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD,
243 | VerticalElasticityBounceEffect.DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK,
244 | -1f // Default is -2,
245 | VerticalElasticityBounceEffect.MAX_SCALE_FACTOR);
246 |
247 | ```
248 |
249 | ## 感谢
250 |
251 | App icons by P.J. Onori,
252 | Timothy Miller,
253 | Icons4Android,
254 | Icons8.com
255 |
256 |
257 | # 修改的 bug
258 | 1. 修改了某些情况下点击不响应的问题
--------------------------------------------------------------------------------
/elasticity-lib/src/main/java/xander/elasticity/adapters/RecyclerViewElasticityAdapter.java:
--------------------------------------------------------------------------------
1 | package xander.elasticity.adapters;
2 |
3 | import android.graphics.Canvas;
4 | import android.support.v7.widget.LinearLayoutManager;
5 | import android.support.v7.widget.RecyclerView;
6 | import android.support.v7.widget.StaggeredGridLayoutManager;
7 | import android.support.v7.widget.helper.ItemTouchHelper;
8 | import android.view.View;
9 |
10 | import java.util.List;
11 |
12 | import xander.elasticity.HorizontalElasticityBounceEffect;
13 | import xander.elasticity.VerticalElasticityBounceEffect;
14 |
15 | /**
16 | * @author amitd
17 | *
18 | * @see HorizontalElasticityBounceEffect
19 | * @see VerticalElasticityBounceEffect
20 | */
21 | public class RecyclerViewElasticityAdapter implements IElasticityAdapter {
22 |
23 | /**
24 | * A delegation of the adapter implementation of this view that should provide the processing
25 | * of {@link #isInAbsoluteStart()} and {@link #isInAbsoluteEnd()}. Essentially needed simply
26 | * because the implementation depends on the layout manager implementation being used.
27 | */
28 | protected interface Impl {
29 | boolean isInAbsoluteStart();
30 | boolean isInAbsoluteEnd();
31 | }
32 |
33 | protected final RecyclerView mRecyclerView;
34 | protected final Impl mImpl;
35 |
36 | protected boolean mIsItemTouchInEffect = false;
37 |
38 | public RecyclerViewElasticityAdapter(RecyclerView recyclerView) {
39 |
40 | mRecyclerView = recyclerView;
41 |
42 | final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
43 | if (layoutManager instanceof LinearLayoutManager ||
44 | layoutManager instanceof StaggeredGridLayoutManager)
45 | {
46 | final int orientation =
47 | (layoutManager instanceof LinearLayoutManager
48 | ? ((LinearLayoutManager) layoutManager).getOrientation()
49 | : ((StaggeredGridLayoutManager) layoutManager).getOrientation());
50 |
51 | if (orientation == LinearLayoutManager.HORIZONTAL) {
52 | mImpl = new ImplHorizLayout();
53 | } else {
54 | mImpl = new ImplVerticalLayout();
55 | }
56 | }
57 | else
58 | {
59 | throw new IllegalArgumentException("Recycler views with custom layout managers are not supported by this adapter out of the box." +
60 | "Try implementing and providing an explicit 'impl' parameter to the other c'tors, or otherwise create a custom adapter subclass of your own.");
61 | }
62 | }
63 |
64 | public RecyclerViewElasticityAdapter(RecyclerView recyclerView, Impl impl) {
65 | mRecyclerView = recyclerView;
66 | mImpl = impl;
67 | }
68 |
69 | public RecyclerViewElasticityAdapter(RecyclerView recyclerView, ItemTouchHelper.Callback itemTouchHelperCallback) {
70 | this(recyclerView);
71 | setUpTouchHelperCallback(itemTouchHelperCallback);
72 | }
73 |
74 | public RecyclerViewElasticityAdapter(RecyclerView recyclerView, Impl impl, ItemTouchHelper.Callback itemTouchHelperCallback) {
75 | this(recyclerView, impl);
76 | setUpTouchHelperCallback(itemTouchHelperCallback);
77 | }
78 |
79 | protected void setUpTouchHelperCallback(final ItemTouchHelper.Callback itemTouchHelperCallback) {
80 | new ItemTouchHelper(new ItemTouchHelperCallbackWrapper(itemTouchHelperCallback) {
81 | @Override
82 | public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
83 | mIsItemTouchInEffect = actionState != 0;
84 | super.onSelectedChanged(viewHolder, actionState);
85 | }
86 | }).attachToRecyclerView(mRecyclerView);
87 | }
88 |
89 | @Override
90 | public View getView() {
91 | return mRecyclerView;
92 | }
93 |
94 | @Override
95 | public boolean isInAbsoluteStart() {
96 | return !mIsItemTouchInEffect && mImpl.isInAbsoluteStart();
97 | }
98 |
99 | @Override
100 | public boolean isInAbsoluteEnd() {
101 | return !mIsItemTouchInEffect && mImpl.isInAbsoluteEnd();
102 | }
103 |
104 | protected class ImplHorizLayout implements Impl {
105 |
106 | @Override
107 | public boolean isInAbsoluteStart() {
108 | return !mRecyclerView.canScrollHorizontally(-1);
109 | }
110 |
111 | @Override
112 | public boolean isInAbsoluteEnd() {
113 | return !mRecyclerView.canScrollHorizontally(1);
114 | }
115 | }
116 |
117 | protected class ImplVerticalLayout implements Impl {
118 |
119 | @Override
120 | public boolean isInAbsoluteStart() {
121 | return !mRecyclerView.canScrollVertically(-1);
122 | }
123 |
124 | @Override
125 | public boolean isInAbsoluteEnd() {
126 | return !mRecyclerView.canScrollVertically(1);
127 | }
128 | }
129 |
130 | private static class ItemTouchHelperCallbackWrapper extends ItemTouchHelper.Callback {
131 |
132 | final ItemTouchHelper.Callback mCallback;
133 |
134 | private ItemTouchHelperCallbackWrapper(ItemTouchHelper.Callback callback) {
135 | mCallback = callback;
136 | }
137 |
138 | @Override
139 | public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
140 | return mCallback.getMovementFlags(recyclerView, viewHolder);
141 | }
142 |
143 | @Override
144 | public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
145 | return mCallback.onMove(recyclerView, viewHolder, target);
146 | }
147 |
148 | @Override
149 | public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
150 | mCallback.onSwiped(viewHolder, direction);
151 | }
152 |
153 | @Override
154 | public int convertToAbsoluteDirection(int flags, int layoutDirection) {
155 | return mCallback.convertToAbsoluteDirection(flags, layoutDirection);
156 | }
157 |
158 | @Override
159 | public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) {
160 | return mCallback.canDropOver(recyclerView, current, target);
161 | }
162 |
163 | @Override
164 | public boolean isLongPressDragEnabled() {
165 | return mCallback.isLongPressDragEnabled();
166 | }
167 |
168 | @Override
169 | public boolean isItemViewSwipeEnabled() {
170 | return mCallback.isItemViewSwipeEnabled();
171 | }
172 |
173 | @Override
174 | public int getBoundingBoxMargin() {
175 | return mCallback.getBoundingBoxMargin();
176 | }
177 |
178 | @Override
179 | public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
180 | return mCallback.getSwipeThreshold(viewHolder);
181 | }
182 |
183 | @Override
184 | public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) {
185 | return mCallback.getMoveThreshold(viewHolder);
186 | }
187 |
188 | @Override
189 | public RecyclerView.ViewHolder chooseDropTarget(RecyclerView.ViewHolder selected, List
36 | * Implementation Notes
37 | *
43 | *
51 | *
212 | *
The state is exited - thus completing over-scroll handling, in one of two cases:
213 | *
When user lets go of the view, it transitions control to the bounce-back state.
214 | *
When user moves the view back onto a potential 'under-scroll' state, it abruptly
215 | * transitions control to the idle-state, so as to return touch-events management to the
216 | * normal over-scroll-less environment (thus preventing under-scrolling and potentially regaining
217 | * regular scrolling).
218 | */
219 | protected class OverScrollingState implements IDecoratorState {
220 |
221 | protected final float mTouchDragRatioFwd;
222 | protected final float mTouchDragRatioBck;
223 |
224 | final MotionAttributes mMoveAttr;
225 | int mCurrDragState;
226 |
227 | public OverScrollingState(float touchDragRatioFwd, float touchDragRatioBck) {
228 | mMoveAttr = createMotionAttributes();
229 | mTouchDragRatioFwd = touchDragRatioFwd;
230 | mTouchDragRatioBck = touchDragRatioBck;
231 | }
232 |
233 | @Override
234 | public int getStateId() {
235 | // This is really a single class that implements 2 states, so our ID depends on what
236 | // it was during the last invocation.
237 | return mCurrDragState;
238 | }
239 |
240 | @Override
241 | public boolean handleMoveTouchEvent(MotionEvent event) {
242 |
243 | // Switching 'pointers' (e.g. fingers) on-the-fly isn't supported -- abort over-scroll
244 | // smoothly using the default bounce-back animation in this case.
245 | if (mOverScrollStartAttr.mPointerId != event.getPointerId(0)) {
246 | issueStateTransition(mBounceBackState);
247 | return true;
248 | }
249 |
250 | final View view = mViewAdapter.getView();
251 | if (!mMoveAttr.init(view, event)) {
252 | // Keep intercepting the touch event as long as we're still over-scrolling...
253 | return true;
254 | }
255 |
256 | float deltaOffset = mMoveAttr.mDeltaOffset / (mMoveAttr.mDir == mOverScrollStartAttr.mDir ? mTouchDragRatioFwd : mTouchDragRatioBck);
257 | float newOffset = mMoveAttr.mAbsOffset + deltaOffset;
258 |
259 | // If moved in counter direction onto a potential under-scroll state -- don't. Instead, abort
260 | // over-scrolling abruptly, thus returning control to which-ever touch handlers there
261 | // are waiting (e.g. regular scroller handlers).
262 | if ((mOverScrollStartAttr.mDir && !mMoveAttr.mDir && (newOffset <= mOverScrollStartAttr.mAbsOffset)) ||
263 | (!mOverScrollStartAttr.mDir && mMoveAttr.mDir && (newOffset >= mOverScrollStartAttr.mAbsOffset))) {
264 | translateViewAndEvent(view, mOverScrollStartAttr.mDir, mOverScrollStartAttr.mAbsOffset, event);
265 | mUpdateListener.onOverScrollUpdate(ElasticityBounceEffectBase.this, mCurrDragState, 0);
266 |
267 | issueStateTransition(mIdleState);
268 | return true;
269 | }
270 |
271 | if (view.getParent() != null) {
272 | view.getParent().requestDisallowInterceptTouchEvent(true);
273 | }
274 |
275 | long dt = event.getEventTime() - event.getHistoricalEventTime(0);
276 | if (dt > 0) { // Sometimes (though rarely) dt==0 cause originally timing is in nanos, but is presented in millis.
277 | mVelocity = deltaOffset / dt;
278 | }
279 |
280 | translateView(view, mOverScrollStartAttr.mDir, newOffset);
281 | mUpdateListener.onOverScrollUpdate(ElasticityBounceEffectBase.this, mCurrDragState, newOffset);
282 |
283 | return true;
284 | }
285 |
286 | @Override
287 | public boolean handleUpOrCancelTouchEvent(MotionEvent event) {
288 | issueStateTransition(mBounceBackState);
289 | return true;
290 | }
291 |
292 | @Override
293 | public void handleEntryTransition(IDecoratorState fromState) {
294 | mCurrDragState = (mOverScrollStartAttr.mDir ? STATE_DRAG_START_SIDE : STATE_DRAG_END_SIDE);
295 | mStateListener.onOverScrollStateChange(ElasticityBounceEffectBase.this, fromState.getStateId(), this.getStateId());
296 | }
297 | }
298 |
299 | /**
300 | * When entered, starts the bounce-back animation.
301 | *
Upon animation completion, transitions control onto the idle state; Does so by
302 | * registering itself as an animation listener.
303 | *
In the meantime, blocks (intercepts) all touch events.
304 | */
305 | protected class BounceBackState implements IDecoratorState, Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener {
306 |
307 | protected final Interpolator mBounceBackInterpolator = new BounceInterpolator();
308 | protected final float mDecelerateFactor;
309 | protected final float mDoubleDecelerateFactor;
310 |
311 | protected final AnimationAttributes mAnimAttributes;
312 |
313 | public BounceBackState(float decelerateFactor) {
314 | mDecelerateFactor = decelerateFactor;
315 | mDoubleDecelerateFactor = 2f * decelerateFactor;
316 |
317 | mAnimAttributes = createAnimationAttributes();
318 | }
319 |
320 | @Override
321 | public int getStateId() {
322 | return STATE_BOUNCE_BACK;
323 | }
324 |
325 | @Override
326 | public void handleEntryTransition(IDecoratorState fromState) {
327 | mStateListener.onOverScrollStateChange(ElasticityBounceEffectBase.this, fromState.getStateId(), this.getStateId());
328 | Animator bounceBackAnim = createAnimator();
329 | bounceBackAnim.addListener(this);
330 | bounceBackAnim.start();
331 | }
332 |
333 | @Override
334 | public boolean handleMoveTouchEvent(MotionEvent event) {
335 | // Flush all touches down the drain till animation is over.
336 | return true;
337 | }
338 |
339 | @Override
340 | public boolean handleUpOrCancelTouchEvent(MotionEvent event) {
341 | // Flush all touches down the drain till animation is over.
342 | return true;
343 | }
344 |
345 | @Override
346 | public void onAnimationEnd(Animator animation) {
347 | issueStateTransition(mIdleState);
348 | }
349 |
350 | @Override
351 | public void onAnimationUpdate(ValueAnimator animation) {
352 | translateView(mViewAdapter.getView(), mOverScrollStartAttr.mDir, (Float) animation.getAnimatedValue());
353 | mUpdateListener.onOverScrollUpdate(ElasticityBounceEffectBase.this, STATE_BOUNCE_BACK, (Float) animation.getAnimatedValue());
354 | }
355 |
356 | @Override
357 | public void onAnimationStart(Animator animation) {
358 | }
359 |
360 | @Override
361 | public void onAnimationCancel(Animator animation) {
362 | issueStateTransition(mIdleState);
363 | }
364 |
365 | @Override
366 | public void onAnimationRepeat(Animator animation) {
367 | }
368 |
369 | protected Animator createAnimator() {
370 | final View view = mViewAdapter.getView();
371 | mAnimAttributes.init(view);
372 |
373 | // Set up a low-duration slow-down animation IN the drag direction.
374 |
375 | // Exception: If wasn't dragging in 'forward' direction (or velocity=0 -- i.e. not dragging at all),
376 | // skip slow-down anim directly to the bounce-back.
377 | if (mVelocity == 0f || (mVelocity < 0 && mOverScrollStartAttr.mDir) || (mVelocity > 0 && !mOverScrollStartAttr.mDir)) {
378 | return createBounceBackAnimator(mAnimAttributes.mAbsOffset, 0);
379 | }
380 |
381 | // dt = (Vt - Vo) / a; Vt=0 ==> dt = -Vo / a
382 | float slowdownDuration = -mVelocity / mDecelerateFactor;
383 | slowdownDuration = (slowdownDuration < 0 ? 0 : slowdownDuration); // Happens in counter-direction dragging
384 |
385 | // dx = (Vt^2 - Vo^2) / 2a; Vt=0 ==> dx = -Vo^2 / 2a
386 | float slowdownDistance = -mVelocity * mVelocity / mDoubleDecelerateFactor;
387 | float slowdownEndOffset = mAnimAttributes.mAbsOffset + slowdownDistance;
388 |
389 | ValueAnimator slowdownAnim = createSlowdownAnimator((int) slowdownDuration, mAnimAttributes.mAbsOffset, slowdownEndOffset);
390 |
391 | // Set up the bounce back animation, bringing the view back into the original, pre-overscroll position (translation=0).
392 | ValueAnimator bounceBackAnim = createBounceBackAnimator(slowdownEndOffset, 0);
393 |
394 | // Play the 2 animations as a sequence.
395 | AnimatorSet wholeAnim = new AnimatorSet();
396 | wholeAnim.playSequentially(slowdownAnim, bounceBackAnim);
397 | return wholeAnim;
398 | }
399 |
400 | protected ValueAnimator createSlowdownAnimator(int slowdownDuration, float slowdownStartOffset, float slowdownEndOffset) {
401 | ValueAnimator slowdownAnim = ValueAnimator.ofFloat(slowdownStartOffset, slowdownEndOffset);
402 | slowdownAnim.setDuration(slowdownDuration);
403 | slowdownAnim.setInterpolator(new DecelerateInterpolator());
404 | slowdownAnim.addUpdateListener(this);
405 | return slowdownAnim;
406 | }
407 |
408 | protected ValueAnimator createBounceBackAnimator(float startOffset, float endOffset) {
409 | // Duration is proportional to the view's size.
410 | float bounceBackDuration = (Math.abs(startOffset) / mAnimAttributes.mMaxOffset) * MAX_BOUNCE_BACK_DURATION_MS;
411 | ValueAnimator bounceBackAnim = ValueAnimator.ofFloat(startOffset, endOffset);
412 | bounceBackAnim.setDuration(Math.max((int) bounceBackDuration, MIN_BOUNCE_BACK_DURATION_MS));
413 | bounceBackAnim.setInterpolator(mBounceBackInterpolator);
414 | bounceBackAnim.addUpdateListener(this);
415 | return bounceBackAnim;
416 | }
417 | }
418 |
419 | public ElasticityBounceEffectBase(IElasticityAdapter viewAdapter, float decelerateFactor, float scaleFactor ,float touchDragRatioFwd, float touchDragRatioBck) {
420 | mViewAdapter = viewAdapter;
421 |
422 | mIdleState = new IdleState();
423 | mOverScrollingState = new OverScrollingState(touchDragRatioFwd, touchDragRatioBck);
424 | mBounceBackState = new BounceBackState(decelerateFactor);
425 |
426 | mCurrentState = mIdleState;
427 |
428 | maxScaleFactor = scaleFactor;
429 |
430 | attach();
431 | }
432 |
433 | @Override
434 | public boolean onTouch(View v, MotionEvent event) {
435 | switch (event.getAction()) {
436 | case MotionEvent.ACTION_MOVE:
437 | return mCurrentState.handleMoveTouchEvent(event);
438 |
439 | case MotionEvent.ACTION_CANCEL:
440 | case MotionEvent.ACTION_UP:
441 | return mCurrentState.handleUpOrCancelTouchEvent(event);
442 | }
443 |
444 | return false;
445 | }
446 |
447 | @Override
448 | public void setOverScrollStateListener(IElasticityStateListener listener) {
449 | mStateListener = (listener != null ? listener : new ElasticityListenerStubs.ElasticityStateListenerStub());
450 | }
451 |
452 | @Override
453 | public void setOverScrollUpdateListener(IElasticityUpdateListener listener) {
454 | mUpdateListener = (listener != null ? listener : new ElasticityListenerStubs.ElasticityUpdateListenerStub());
455 | }
456 |
457 | @Override
458 | public int getCurrentState() {
459 | return mCurrentState.getStateId();
460 | }
461 |
462 | @Override
463 | public View getView() {
464 | return mViewAdapter.getView();
465 | }
466 |
467 | protected void issueStateTransition(IDecoratorState state) {
468 | IDecoratorState oldState = mCurrentState;
469 | mCurrentState = state;
470 | mCurrentState.handleEntryTransition(oldState);
471 | }
472 |
473 | protected void attach() {
474 | getView().setOnTouchListener(this);
475 | getView().setOverScrollMode(View.OVER_SCROLL_NEVER);
476 | }
477 |
478 | @Override
479 | public void detach() {
480 | if (mCurrentState != mIdleState) {
481 | Log.w(TAG, "Decorator detached while over-scroll is in effect. You might want to add a precondition of that getCurrentState()==STATE_IDLE, first.");
482 | }
483 | getView().setOnTouchListener(null);
484 | getView().setOverScrollMode(View.OVER_SCROLL_ALWAYS);
485 | }
486 |
487 | protected abstract MotionAttributes createMotionAttributes();
488 |
489 | protected abstract AnimationAttributes createAnimationAttributes();
490 |
491 | protected abstract void translateView(View view, boolean dir, float offset);
492 |
493 | protected abstract void translateViewAndEvent(View view, boolean dir, float offset, MotionEvent event);
494 |
495 | protected float getViewOffset(View view) {
496 | if (view.getTag(R.id.offsetValue) != null) {
497 | return (float) view.getTag(R.id.offsetValue);
498 | }
499 | return 0;
500 | }
501 |
502 | protected void setViewOffset(View view, float offset) {
503 | view.setTag(R.id.offsetValue, offset);
504 | }
505 |
506 | protected float getMaxScaleFactor() {
507 | return maxScaleFactor;
508 | }
509 |
510 | }
511 |
--------------------------------------------------------------------------------
/elasticity-lib/src/test/java/xander/elasticity/android/ui/overscroll/VerticalOverScrollBounceEffectDecoratorTest.java:
--------------------------------------------------------------------------------
1 | package xander.elasticity.android.ui.overscroll;
2 |
3 | import android.view.MotionEvent;
4 | import android.view.View;
5 |
6 | import org.junit.Before;
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 | import org.robolectric.RobolectricTestRunner;
10 | import org.robolectric.annotation.Config;
11 |
12 | import xander.elasticity.HorizontalElasticityBounceEffect;
13 | import xander.elasticity.IElasticityStateListener;
14 | import xander.elasticity.IElasticityUpdateListener;
15 | import xander.elasticity.VerticalElasticityBounceEffect;
16 | import xander.elasticity.adapters.IElasticityAdapter;
17 |
18 | import static xander.elasticity.IElasticityState.*;
19 | import static xander.elasticity.VerticalElasticityBounceEffect.DEFAULT_DECELERATE_FACTOR;
20 | import static xander.elasticity.VerticalElasticityBounceEffect.DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD;
21 | import static org.junit.Assert.assertEquals;
22 | import static org.junit.Assert.assertFalse;
23 | import static org.junit.Assert.assertTrue;
24 | import static org.mockito.Matchers.*;
25 | import static org.mockito.Mockito.*;
26 |
27 | /**
28 | * @author amitd
29 | */
30 | @RunWith(RobolectricTestRunner.class)
31 | @Config(manifest = Config.NONE)
32 | public class VerticalOverScrollBounceEffectDecoratorTest {
33 |
34 | View mView;
35 | IElasticityAdapter mViewAdapter;
36 | IElasticityStateListener mStateListener;
37 | IElasticityUpdateListener mUpdateListener;
38 |
39 | @Before
40 | public void setUp() throws Exception {
41 | mView = mock(View.class);
42 | mViewAdapter = mock(IElasticityAdapter.class);
43 | when(mViewAdapter.getView()).thenReturn(mView);
44 |
45 | mStateListener = mock(IElasticityStateListener.class);
46 | mUpdateListener = mock(IElasticityUpdateListener.class);
47 | }
48 |
49 | @Test
50 | public void detach_decoratorIsAttached_detachFromView() throws Exception {
51 |
52 | // Arrange
53 |
54 | HorizontalElasticityBounceEffect uut = new HorizontalElasticityBounceEffect(mViewAdapter);
55 |
56 | // Act
57 |
58 | uut.detach();
59 |
60 | // Assert
61 |
62 | verify(mView).setOnTouchListener(eq((View.OnTouchListener) null));
63 | verify(mView).setOverScrollMode(View.OVER_SCROLL_ALWAYS);
64 | }
65 |
66 | @Test
67 | public void detach_overScrollInEffect_detachFromView() throws Exception {
68 |
69 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(true);
70 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(false);
71 |
72 | VerticalElasticityBounceEffect uut = getUUT();
73 | uut.onTouch(mView, createShortDownwardsMoveEvent());
74 |
75 | // Act
76 |
77 | uut.detach();
78 |
79 | // Assert
80 |
81 | verify(mView).setOnTouchListener(eq((View.OnTouchListener) null));
82 | verify(mView).setOverScrollMode(View.OVER_SCROLL_ALWAYS);
83 | }
84 |
85 | /*
86 | * Move-action event
87 | */
88 |
89 | @Test
90 | public void onTouchMoveAction_notInViewEnds_ignoreTouchEvent() throws Exception {
91 |
92 | // Arrange
93 |
94 | MotionEvent event = createShortDownwardsMoveEvent();
95 |
96 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(false);
97 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(false);
98 |
99 | VerticalElasticityBounceEffect uut = getUUT();
100 |
101 | // Act
102 |
103 | boolean ret = uut.onTouch(mView, event);
104 |
105 | // Assert
106 |
107 | verify(mView, never()).setTranslationX(anyFloat());
108 | verify(mView, never()).setTranslationY(anyFloat());
109 | assertFalse(ret);
110 | assertEquals(STATE_IDLE, uut.getCurrentState());
111 |
112 | verify(mStateListener, never()).onOverScrollStateChange(eq(uut),anyInt(), anyInt());
113 | verify(mUpdateListener, never()).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
114 | }
115 |
116 | @Test
117 | public void onTouchMoveAction_dragDownInUpperEnd_overscrollDownwards() throws Exception {
118 |
119 | // Arrange
120 |
121 | MotionEvent event = createShortDownwardsMoveEvent();
122 |
123 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(true);
124 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(false);
125 |
126 | VerticalElasticityBounceEffect uut = getUUT();
127 |
128 | // Act
129 |
130 | boolean ret = uut.onTouch(mView, event);
131 |
132 | // Assert
133 |
134 | float expectedTransY = (event.getY() - event.getHistoricalY(0)) / DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD;
135 | verify(mView).setTranslationY(expectedTransY);
136 | verify(mView, never()).setTranslationX(anyFloat());
137 | assertTrue(ret);
138 | assertEquals(STATE_DRAG_START_SIDE, uut.getCurrentState());
139 |
140 | verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_START_SIDE));
141 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(expectedTransY));
142 | }
143 |
144 | @Test
145 | public void onTouchMoveAction_dragUpInBottomEnd_overscrollUpwards() throws Exception {
146 |
147 | // Arrange
148 |
149 | MotionEvent event = createShortUpwardsMoveEvent();
150 |
151 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(false);
152 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(true);
153 |
154 | VerticalElasticityBounceEffect uut = getUUT();
155 |
156 | // Act
157 |
158 | boolean ret = uut.onTouch(mView, event);
159 |
160 | // Assert
161 |
162 | float expectedTransY = (event.getY() - event.getHistoricalY(0)) / DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD;
163 | verify(mView).setTranslationY(expectedTransY);
164 | verify(mView, never()).setTranslationX(anyFloat());
165 | assertTrue(ret);
166 | assertEquals(STATE_DRAG_END_SIDE, uut.getCurrentState());
167 |
168 | verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_END_SIDE));
169 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(expectedTransY));
170 | }
171 |
172 | @Test
173 | public void onTouchMoveAction_dragUpInUpperEnd_ignoreTouchEvent() throws Exception {
174 |
175 | // Arrange
176 |
177 | MotionEvent event = createShortUpwardsMoveEvent();
178 |
179 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(true);
180 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(false);
181 |
182 | VerticalElasticityBounceEffect uut = getUUT();
183 |
184 | // Act
185 |
186 | boolean ret = uut.onTouch(mView, event);
187 |
188 | // Assert
189 |
190 | verify(mView, never()).setTranslationX(anyFloat());
191 | verify(mView, never()).setTranslationY(anyFloat());
192 | assertFalse(ret);
193 | assertEquals(STATE_IDLE, uut.getCurrentState());
194 |
195 | verify(mStateListener, never()).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
196 | verify(mUpdateListener, never()).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
197 | }
198 |
199 | @Test
200 | public void onTouchMoveAction_dragDownInBottomEnd_ignoreTouchEvent() throws Exception {
201 |
202 | // Arrange
203 |
204 | MotionEvent event = createShortDownwardsMoveEvent();
205 |
206 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(false);
207 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(true);
208 |
209 | VerticalElasticityBounceEffect uut = getUUT();
210 |
211 | // Act
212 |
213 | boolean ret = uut.onTouch(mView, event);
214 |
215 | // Assert
216 |
217 | verify(mView, never()).setTranslationX(anyFloat());
218 | verify(mView, never()).setTranslationY(anyFloat());
219 | assertFalse(ret);
220 | assertEquals(STATE_IDLE, uut.getCurrentState());
221 |
222 | verify(mStateListener, never()).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
223 | verify(mUpdateListener, never()).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
224 | }
225 |
226 | @Test
227 | public void onTouchMoveAction_2ndDownDragInUpperEnd_overscrollDownwardsFurther() throws Exception {
228 |
229 | // Arrange
230 |
231 | // Bring UUT to a downwards-overscroll state
232 | MotionEvent event1 = createShortDownwardsMoveEvent();
233 |
234 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(true);
235 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(false);
236 |
237 | VerticalElasticityBounceEffect uut = getUUT();
238 | uut.onTouch(mView, event1);
239 | reset(mView);
240 |
241 | // Create 2nd downwards-drag event
242 | MotionEvent event2 = createLongDownwardsMoveEvent();
243 |
244 | // Act
245 |
246 | final boolean ret = uut.onTouch(mView, event2);
247 |
248 | // Assert
249 |
250 | final float expectedTransY1 = (event1.getY() - event1.getHistoricalY(0)) / DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD;
251 | final float expectedTransY2 = (event2.getY() - event2.getHistoricalY(0)) / DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD;
252 | verify(mView).setTranslationY(expectedTransY2);
253 | verify(mView, never()).setTranslationX(anyFloat());
254 | assertTrue(ret);
255 | assertEquals(STATE_DRAG_START_SIDE, uut.getCurrentState());
256 |
257 | // State-change listener called only once?
258 | verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_START_SIDE));
259 | verify(mStateListener).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
260 | // Update-listener called exactly twice?
261 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(expectedTransY1));
262 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(expectedTransY2));
263 | verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
264 | }
265 |
266 | @Test
267 | public void onTouchMoveAction_2ndUpDragInBottomEnd_overscrollUpwardsFurther() throws Exception {
268 |
269 | // Arrange
270 |
271 | // Bring UUT to an upwards-overscroll state
272 | MotionEvent event1 = createShortUpwardsMoveEvent();
273 |
274 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(false);
275 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(true);
276 |
277 | VerticalElasticityBounceEffect uut = getUUT();
278 | uut.onTouch(mView, event1);
279 | reset(mView);
280 |
281 | // Create 2nd upward-drag event
282 | MotionEvent event2 = createLongUpwardsMoveEvent();
283 |
284 | // Act
285 |
286 | final boolean ret = uut.onTouch(mView, event2);
287 |
288 | // Assert
289 |
290 | final float expectedTransY1 = (event1.getY() - event1.getHistoricalY(0)) / DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD;
291 | final float expectedTransY2 = (event2.getY() - event2.getHistoricalY(0)) / DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD;
292 | verify(mView).setTranslationY(expectedTransY2);
293 | verify(mView, never()).setTranslationX(anyFloat());
294 | assertTrue(ret);
295 | assertEquals(STATE_DRAG_END_SIDE, uut.getCurrentState());
296 |
297 | // State-change listener called only once?
298 | verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_END_SIDE));
299 | verify(mStateListener).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
300 | // Update-listener called exactly twice?
301 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(expectedTransY1));
302 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(expectedTransY2));
303 | verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
304 | }
305 |
306 | /**
307 | * When over-scroll has already started (downwards in this case) and suddenly the user changes
308 | * their mind and scrolls a bit in the other direction:
309 | *
We expect the touch to still be intercepted in that case, and the overscroll to remain in effect.
310 | */
311 | @Test
312 | public void onTouchMoveAction_dragUpWhenDownOverscolled_continueOverscrollingUpwards() throws Exception {
313 |
314 | // Arrange
315 |
316 | // In down & up drag tests we use equal ratios to avoid the effect's under-scroll handling
317 | final float touchDragRatioFwd = 3f;
318 | final float touchDragRatioBck = 3f;
319 |
320 | // Bring UUT to a downwrads-overscroll state
321 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(true);
322 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(false);
323 |
324 | VerticalElasticityBounceEffect uut = getUUT(touchDragRatioFwd, touchDragRatioBck);
325 | MotionEvent eventMoveRight = createLongDownwardsMoveEvent();
326 | uut.onTouch(mView, eventMoveRight);
327 | reset(mView);
328 | float startTransY = (eventMoveRight.getY() - eventMoveRight.getHistoricalY(0)) / touchDragRatioFwd;
329 | when(mView.getTranslationY()).thenReturn(startTransY);
330 |
331 | // Create the up-drag event
332 | MotionEvent eventMoveUpwards = createShortUpwardsMoveEvent();
333 |
334 | // Act
335 |
336 | boolean ret = uut.onTouch(mView, eventMoveUpwards);
337 |
338 | // Assert
339 |
340 | float expectedTransY = startTransY +
341 | (eventMoveUpwards.getY() - eventMoveUpwards.getHistoricalY(0)) / touchDragRatioBck;
342 | verify(mView).setTranslationY(expectedTransY);
343 | verify(mView, never()).setTranslationX(anyFloat());
344 | assertTrue(ret);
345 | assertEquals(STATE_DRAG_START_SIDE, uut.getCurrentState());
346 |
347 | // State-change listener called only once?
348 | verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_START_SIDE));
349 | verify(mStateListener).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
350 | // Update-listener called exactly twice?
351 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(startTransY));
352 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(expectedTransY));
353 | verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
354 | }
355 |
356 | /**
357 | * When over-scroll has already started (upwards in this case) and suddenly the user changes
358 | * their mind and scrolls a bit in the other direction:
359 | *
We expect the touch to still be intercepted in that case, and the overscroll to remain in effect.
360 | */
361 | @Test
362 | public void onTouchMoveAction_dragDownWhenUpOverscolled_continueOverscrollingDownwards() throws Exception {
363 |
364 | // Arrange
365 |
366 | // In up & down drag tests we use equal ratios to avoid the effect's under-scroll handling
367 | final float touchDragRatioFwd = 3f;
368 | final float touchDragRatioBck = 3f;
369 |
370 | // Bring UUT to an upwards-overscroll state
371 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(false);
372 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(true);
373 |
374 | VerticalElasticityBounceEffect uut = getUUT(touchDragRatioFwd, touchDragRatioBck);
375 | MotionEvent eventMoveUp = createLongUpwardsMoveEvent();
376 | uut.onTouch(mView, eventMoveUp);
377 | reset(mView);
378 |
379 | float startTransY = (eventMoveUp.getY() - eventMoveUp.getHistoricalY(0)) / touchDragRatioFwd;
380 | when(mView.getTranslationY()).thenReturn(startTransY);
381 |
382 | // Create the down-drag event
383 | MotionEvent eventMoveDown = createShortDownwardsMoveEvent();
384 |
385 | // Act
386 |
387 | boolean ret = uut.onTouch(mView, eventMoveDown);
388 |
389 | // Assert
390 |
391 | float expectedTransY = startTransY + (eventMoveDown.getY() - eventMoveDown.getHistoricalY(0)) / touchDragRatioBck;
392 | verify(mView).setTranslationY(expectedTransY);
393 | verify(mView, never()).setTranslationX(anyFloat());
394 | assertTrue(ret);
395 | assertEquals(STATE_DRAG_END_SIDE, uut.getCurrentState());
396 |
397 | // State-change listener called only once?
398 | verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_END_SIDE));
399 | verify(mStateListener).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
400 | // Update-listener called exactly twice?
401 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(startTransY));
402 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(expectedTransY));
403 | verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
404 | }
405 |
406 | @Test
407 | public void onTouchMoveAction_undragWhenDownOverscrolled_endOverscrolling() throws Exception {
408 |
409 | // Arrange
410 |
411 | // In left & right tests we use equal ratios to avoid the effect's under-scroll handling
412 | final float touchDragRatioFwd = 3f;
413 | final float touchDragRatioBck = 3f;
414 |
415 | // Bring UUT to a downwards-overscroll state
416 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(true);
417 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(false);
418 |
419 | VerticalElasticityBounceEffect uut = getUUT(touchDragRatioFwd, touchDragRatioBck);
420 | MotionEvent eventMoveDown = createLongDownwardsMoveEvent();
421 | uut.onTouch(mView, eventMoveDown);
422 | reset(mView);
423 | float startTransX = (eventMoveDown.getX() - eventMoveDown.getHistoricalX(0)) / touchDragRatioFwd;
424 | when(mView.getTranslationX()).thenReturn(startTransX);
425 |
426 | // Create the (negative) upwards-drag event
427 | MotionEvent eventMoveUp = createLongUpwardsMoveEvent();
428 |
429 | // Act
430 |
431 | boolean ret = uut.onTouch(mView, eventMoveUp);
432 |
433 | // Assert
434 |
435 | verify(mView, never()).setTranslationX(anyFloat());
436 | verify(mView).setTranslationY(0);
437 | assertTrue(ret);
438 | assertEquals(STATE_IDLE, uut.getCurrentState());
439 |
440 | // State-change listener invoked to say drag-on and drag-off (idle).
441 | verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_START_SIDE));
442 | verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_DRAG_START_SIDE), eq(STATE_IDLE));
443 | verify(mStateListener, times(2)).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
444 | // Update-listener called exactly twice?
445 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(startTransX));
446 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_START_SIDE), eq(0f));
447 | verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
448 | }
449 |
450 | @Test
451 | public void onTouchMoveAction_undragWhenUpOverscrolled_endOverscrolling() throws Exception {
452 |
453 | // Arrange
454 |
455 | // In left & right tests we use equal ratios to avoid the effect's under-scroll handling
456 | final float touchDragRatioFwd = 3f;
457 | final float touchDragRatioBck = 3f;
458 |
459 | // Bring UUT to a left-overscroll state
460 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(false);
461 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(true);
462 |
463 | VerticalElasticityBounceEffect uut = getUUT(touchDragRatioFwd, touchDragRatioBck);
464 | MotionEvent eventMoveUp = createLongUpwardsMoveEvent();
465 | uut.onTouch(mView, eventMoveUp);
466 | reset(mView);
467 | float startTransX = (eventMoveUp.getX() - eventMoveUp.getHistoricalX(0)) / touchDragRatioFwd;
468 | when(mView.getTranslationX()).thenReturn(startTransX);
469 |
470 | // Create the (negative) downwards-drag event
471 | MotionEvent eventMoveDown = createLongDownwardsMoveEvent();
472 |
473 | // Act
474 |
475 | boolean ret = uut.onTouch(mView, eventMoveDown);
476 |
477 | // Assert
478 |
479 | verify(mView, never()).setTranslationX(anyFloat());
480 | verify(mView).setTranslationY(0);
481 | assertTrue(ret);
482 | assertEquals(STATE_IDLE, uut.getCurrentState());
483 |
484 | // State-change listener invoked to say drag-on and drag-off (idle).
485 | verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_IDLE), eq(STATE_DRAG_END_SIDE));
486 | verify(mStateListener).onOverScrollStateChange(eq(uut), eq(STATE_DRAG_END_SIDE), eq(STATE_IDLE));
487 | verify(mStateListener, times(2)).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
488 | // Update-listener called exactly twice?
489 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(startTransX));
490 | verify(mUpdateListener).onOverScrollUpdate(eq(uut), eq(STATE_DRAG_END_SIDE), eq(0f));
491 | verify(mUpdateListener, times(2)).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
492 | }
493 |
494 | /*
495 | * Up action event
496 | */
497 |
498 | @Test
499 | public void onTouchUpAction_eventWhenNotOverscrolled_ignoreTouchEvent() throws Exception {
500 |
501 | // Arrange
502 |
503 | MotionEvent event = createDefaultUpActionEvent();
504 |
505 | when(mViewAdapter.isInAbsoluteStart()).thenReturn(true);
506 | when(mViewAdapter.isInAbsoluteEnd()).thenReturn(true);
507 |
508 | VerticalElasticityBounceEffect uut = getUUT();
509 |
510 | // Act
511 |
512 | boolean ret = uut.onTouch(mView, event);
513 |
514 | // Assert
515 |
516 | verify(mView, never()).setTranslationX(anyFloat());
517 | verify(mView, never()).setTranslationY(anyFloat());
518 | assertFalse(ret);
519 | assertEquals(STATE_IDLE, uut.getCurrentState());
520 |
521 | verify(mStateListener, never()).onOverScrollStateChange(eq(uut), anyInt(), anyInt());
522 | verify(mUpdateListener, never()).onOverScrollUpdate(eq(uut), anyInt(), anyFloat());
523 | }
524 |
525 | protected MotionEvent createShortDownwardsMoveEvent() {
526 | MotionEvent event = mock(MotionEvent.class);
527 | when(event.getAction()).thenReturn(MotionEvent.ACTION_MOVE);
528 | when(event.getX()).thenReturn(200f);
529 | when(event.getY()).thenReturn(100f);
530 | when(event.getX(0)).thenReturn(200f);
531 | when(event.getY(0)).thenReturn(100f);
532 | when(event.getHistorySize()).thenReturn(1);
533 | when(event.getHistoricalX(eq(0))).thenReturn(190f);
534 | when(event.getHistoricalY(eq(0))).thenReturn(80f);
535 | when(event.getHistoricalX(eq(0), eq(0))).thenReturn(190f);
536 | when(event.getHistoricalY(eq(0), eq(0))).thenReturn(80f);
537 | return event;
538 | }
539 |
540 | protected MotionEvent createLongDownwardsMoveEvent() {
541 | MotionEvent event = mock(MotionEvent.class);
542 | when(event.getAction()).thenReturn(MotionEvent.ACTION_MOVE);
543 | when(event.getX()).thenReturn(250f);
544 | when(event.getY()).thenReturn(150f);
545 | when(event.getX(0)).thenReturn(250f);
546 | when(event.getY(0)).thenReturn(150f);
547 | when(event.getHistorySize()).thenReturn(1);
548 | when(event.getHistoricalX(eq(0))).thenReturn(200f);
549 | when(event.getHistoricalY(eq(0))).thenReturn(100f);
550 | when(event.getHistoricalX(eq(0), eq(0))).thenReturn(200f);
551 | when(event.getHistoricalY(eq(0), eq(0))).thenReturn(100f);
552 | return event;
553 | }
554 |
555 | protected MotionEvent createShortUpwardsMoveEvent() {
556 | MotionEvent event = mock(MotionEvent.class);
557 | when(event.getAction()).thenReturn(MotionEvent.ACTION_MOVE);
558 | when(event.getX()).thenReturn(200f);
559 | when(event.getY()).thenReturn(100f);
560 | when(event.getX(0)).thenReturn(200f);
561 | when(event.getY(0)).thenReturn(100f);
562 | when(event.getHistorySize()).thenReturn(1);
563 | when(event.getHistoricalX(eq(0))).thenReturn(220f);
564 | when(event.getHistoricalY(eq(0))).thenReturn(120f);
565 | when(event.getHistoricalX(eq(0), eq(0))).thenReturn(220f);
566 | when(event.getHistoricalY(eq(0), eq(0))).thenReturn(120f);
567 | return event;
568 | }
569 |
570 | protected MotionEvent createLongUpwardsMoveEvent() {
571 | MotionEvent event = mock(MotionEvent.class);
572 | when(event.getAction()).thenReturn(MotionEvent.ACTION_MOVE);
573 | when(event.getX()).thenReturn(200f);
574 | when(event.getY()).thenReturn(100f);
575 | when(event.getX(0)).thenReturn(200f);
576 | when(event.getY(0)).thenReturn(100f);
577 | when(event.getHistorySize()).thenReturn(1);
578 | when(event.getHistoricalX(eq(0))).thenReturn(250f);
579 | when(event.getHistoricalY(eq(0))).thenReturn(150f);
580 | when(event.getHistoricalX(eq(0), eq(0))).thenReturn(250f);
581 | when(event.getHistoricalY(eq(0), eq(0))).thenReturn(150f);
582 | return event;
583 | }
584 |
585 | protected MotionEvent createDefaultUpActionEvent() {
586 | MotionEvent event = mock(MotionEvent.class);
587 | when(event.getAction()).thenReturn(MotionEvent.ACTION_UP);
588 | return event;
589 | }
590 |
591 | protected VerticalElasticityBounceEffect getUUT() {
592 | VerticalElasticityBounceEffect uut = new VerticalElasticityBounceEffect(mViewAdapter);
593 | uut.setOverScrollStateListener(mStateListener);
594 | uut.setOverScrollUpdateListener(mUpdateListener);
595 | return uut;
596 | }
597 |
598 | protected VerticalElasticityBounceEffect getUUT(float touchDragRatioFwd, float touchDragRatioBck) {
599 | VerticalElasticityBounceEffect uut = new VerticalElasticityBounceEffect(mViewAdapter, touchDragRatioFwd, touchDragRatioBck, DEFAULT_DECELERATE_FACTOR);
600 | uut.setOverScrollStateListener(mStateListener);
601 | uut.setOverScrollUpdateListener(mUpdateListener);
602 | return uut;
603 | }
604 | }
--------------------------------------------------------------------------------