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