• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.design.widget;
18 
19 import android.content.Context;
20 import android.support.design.widget.CoordinatorLayout.Behavior;
21 import android.support.v4.math.MathUtils;
22 import android.support.v4.view.ViewCompat;
23 import android.util.AttributeSet;
24 import android.view.MotionEvent;
25 import android.view.VelocityTracker;
26 import android.view.View;
27 import android.view.ViewConfiguration;
28 import android.widget.OverScroller;
29 
30 /**
31  * The {@link Behavior} for a view that sits vertically above scrolling a view.
32  * See {@link HeaderScrollingViewBehavior}.
33  */
34 abstract class HeaderBehavior<V extends View> extends ViewOffsetBehavior<V> {
35 
36     private static final int INVALID_POINTER = -1;
37 
38     private Runnable mFlingRunnable;
39     OverScroller mScroller;
40 
41     private boolean mIsBeingDragged;
42     private int mActivePointerId = INVALID_POINTER;
43     private int mLastMotionY;
44     private int mTouchSlop = -1;
45     private VelocityTracker mVelocityTracker;
46 
HeaderBehavior()47     public HeaderBehavior() {}
48 
HeaderBehavior(Context context, AttributeSet attrs)49     public HeaderBehavior(Context context, AttributeSet attrs) {
50         super(context, attrs);
51     }
52 
53     @Override
onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev)54     public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
55         if (mTouchSlop < 0) {
56             mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
57         }
58 
59         final int action = ev.getAction();
60 
61         // Shortcut since we're being dragged
62         if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
63             return true;
64         }
65 
66         switch (ev.getActionMasked()) {
67             case MotionEvent.ACTION_DOWN: {
68                 mIsBeingDragged = false;
69                 final int x = (int) ev.getX();
70                 final int y = (int) ev.getY();
71                 if (canDragView(child) && parent.isPointInChildBounds(child, x, y)) {
72                     mLastMotionY = y;
73                     mActivePointerId = ev.getPointerId(0);
74                     ensureVelocityTracker();
75                 }
76                 break;
77             }
78 
79             case MotionEvent.ACTION_MOVE: {
80                 final int activePointerId = mActivePointerId;
81                 if (activePointerId == INVALID_POINTER) {
82                     // If we don't have a valid id, the touch down wasn't on content.
83                     break;
84                 }
85                 final int pointerIndex = ev.findPointerIndex(activePointerId);
86                 if (pointerIndex == -1) {
87                     break;
88                 }
89 
90                 final int y = (int) ev.getY(pointerIndex);
91                 final int yDiff = Math.abs(y - mLastMotionY);
92                 if (yDiff > mTouchSlop) {
93                     mIsBeingDragged = true;
94                     mLastMotionY = y;
95                 }
96                 break;
97             }
98 
99             case MotionEvent.ACTION_CANCEL:
100             case MotionEvent.ACTION_UP: {
101                 mIsBeingDragged = false;
102                 mActivePointerId = INVALID_POINTER;
103                 if (mVelocityTracker != null) {
104                     mVelocityTracker.recycle();
105                     mVelocityTracker = null;
106                 }
107                 break;
108             }
109         }
110 
111         if (mVelocityTracker != null) {
112             mVelocityTracker.addMovement(ev);
113         }
114 
115         return mIsBeingDragged;
116     }
117 
118     @Override
onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev)119     public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
120         if (mTouchSlop < 0) {
121             mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
122         }
123 
124         switch (ev.getActionMasked()) {
125             case MotionEvent.ACTION_DOWN: {
126                 final int x = (int) ev.getX();
127                 final int y = (int) ev.getY();
128 
129                 if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
130                     mLastMotionY = y;
131                     mActivePointerId = ev.getPointerId(0);
132                     ensureVelocityTracker();
133                 } else {
134                     return false;
135                 }
136                 break;
137             }
138 
139             case MotionEvent.ACTION_MOVE: {
140                 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
141                 if (activePointerIndex == -1) {
142                     return false;
143                 }
144 
145                 final int y = (int) ev.getY(activePointerIndex);
146                 int dy = mLastMotionY - y;
147 
148                 if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {
149                     mIsBeingDragged = true;
150                     if (dy > 0) {
151                         dy -= mTouchSlop;
152                     } else {
153                         dy += mTouchSlop;
154                     }
155                 }
156 
157                 if (mIsBeingDragged) {
158                     mLastMotionY = y;
159                     // We're being dragged so scroll the ABL
160                     scroll(parent, child, dy, getMaxDragOffset(child), 0);
161                 }
162                 break;
163             }
164 
165             case MotionEvent.ACTION_UP:
166                 if (mVelocityTracker != null) {
167                     mVelocityTracker.addMovement(ev);
168                     mVelocityTracker.computeCurrentVelocity(1000);
169                     float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
170                     fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
171                 }
172                 // $FALLTHROUGH
173             case MotionEvent.ACTION_CANCEL: {
174                 mIsBeingDragged = false;
175                 mActivePointerId = INVALID_POINTER;
176                 if (mVelocityTracker != null) {
177                     mVelocityTracker.recycle();
178                     mVelocityTracker = null;
179                 }
180                 break;
181             }
182         }
183 
184         if (mVelocityTracker != null) {
185             mVelocityTracker.addMovement(ev);
186         }
187 
188         return true;
189     }
190 
setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset)191     int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset) {
192         return setHeaderTopBottomOffset(parent, header, newOffset,
193                 Integer.MIN_VALUE, Integer.MAX_VALUE);
194     }
195 
setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset, int minOffset, int maxOffset)196     int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset,
197             int minOffset, int maxOffset) {
198         final int curOffset = getTopAndBottomOffset();
199         int consumed = 0;
200 
201         if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
202             // If we have some scrolling range, and we're currently within the min and max
203             // offsets, calculate a new offset
204             newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
205 
206             if (curOffset != newOffset) {
207                 setTopAndBottomOffset(newOffset);
208                 // Update how much dy we have consumed
209                 consumed = curOffset - newOffset;
210             }
211         }
212 
213         return consumed;
214     }
215 
getTopBottomOffsetForScrollingSibling()216     int getTopBottomOffsetForScrollingSibling() {
217         return getTopAndBottomOffset();
218     }
219 
scroll(CoordinatorLayout coordinatorLayout, V header, int dy, int minOffset, int maxOffset)220     final int scroll(CoordinatorLayout coordinatorLayout, V header,
221             int dy, int minOffset, int maxOffset) {
222         return setHeaderTopBottomOffset(coordinatorLayout, header,
223                 getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
224     }
225 
fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset, int maxOffset, float velocityY)226     final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset,
227             int maxOffset, float velocityY) {
228         if (mFlingRunnable != null) {
229             layout.removeCallbacks(mFlingRunnable);
230             mFlingRunnable = null;
231         }
232 
233         if (mScroller == null) {
234             mScroller = new OverScroller(layout.getContext());
235         }
236 
237         mScroller.fling(
238                 0, getTopAndBottomOffset(), // curr
239                 0, Math.round(velocityY), // velocity.
240                 0, 0, // x
241                 minOffset, maxOffset); // y
242 
243         if (mScroller.computeScrollOffset()) {
244             mFlingRunnable = new FlingRunnable(coordinatorLayout, layout);
245             ViewCompat.postOnAnimation(layout, mFlingRunnable);
246             return true;
247         } else {
248             onFlingFinished(coordinatorLayout, layout);
249             return false;
250         }
251     }
252 
253     /**
254      * Called when a fling has finished, or the fling was initiated but there wasn't enough
255      * velocity to start it.
256      */
onFlingFinished(CoordinatorLayout parent, V layout)257     void onFlingFinished(CoordinatorLayout parent, V layout) {
258         // no-op
259     }
260 
261     /**
262      * Return true if the view can be dragged.
263      */
canDragView(V view)264     boolean canDragView(V view) {
265         return false;
266     }
267 
268     /**
269      * Returns the maximum px offset when {@code view} is being dragged.
270      */
getMaxDragOffset(V view)271     int getMaxDragOffset(V view) {
272         return -view.getHeight();
273     }
274 
getScrollRangeForDragFling(V view)275     int getScrollRangeForDragFling(V view) {
276         return view.getHeight();
277     }
278 
ensureVelocityTracker()279     private void ensureVelocityTracker() {
280         if (mVelocityTracker == null) {
281             mVelocityTracker = VelocityTracker.obtain();
282         }
283     }
284 
285     private class FlingRunnable implements Runnable {
286         private final CoordinatorLayout mParent;
287         private final V mLayout;
288 
FlingRunnable(CoordinatorLayout parent, V layout)289         FlingRunnable(CoordinatorLayout parent, V layout) {
290             mParent = parent;
291             mLayout = layout;
292         }
293 
294         @Override
run()295         public void run() {
296             if (mLayout != null && mScroller != null) {
297                 if (mScroller.computeScrollOffset()) {
298                     setHeaderTopBottomOffset(mParent, mLayout, mScroller.getCurrY());
299                     // Post ourselves so that we run on the next animation
300                     ViewCompat.postOnAnimation(mLayout, this);
301                 } else {
302                     onFlingFinished(mParent, mLayout);
303                 }
304             }
305         }
306     }
307 }
308