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    private 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 = MotionEventCompat.getPointerId(ev, 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 = MotionEventCompat.findPointerIndex(ev, activePointerId);
87                if (pointerIndex == -1) {
88                    break;
89                }
90
91                final int y = (int) MotionEventCompat.getY(ev, 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 = MotionEventCompat.getPointerId(ev, 0);
133                    ensureVelocityTracker();
134                } else {
135                    return false;
136                }
137                break;
138            }
139
140            case MotionEvent.ACTION_MOVE: {
141                final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
142                        mActivePointerId);
143                if (activePointerIndex == -1) {
144                    return false;
145                }
146
147                final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
148                int dy = mLastMotionY - y;
149
150                if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {
151                    mIsBeingDragged = true;
152                    if (dy > 0) {
153                        dy -= mTouchSlop;
154                    } else {
155                        dy += mTouchSlop;
156                    }
157                }
158
159                if (mIsBeingDragged) {
160                    mLastMotionY = y;
161                    // We're being dragged so scroll the ABL
162                    scroll(parent, child, dy, getMaxDragOffset(child), 0);
163                }
164                break;
165            }
166
167            case MotionEvent.ACTION_UP:
168                if (mVelocityTracker != null) {
169                    mVelocityTracker.addMovement(ev);
170                    mVelocityTracker.computeCurrentVelocity(1000);
171                    float yvel = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
172                            mActivePointerId);
173                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
174                }
175                // $FALLTHROUGH
176            case MotionEvent.ACTION_CANCEL: {
177                mIsBeingDragged = false;
178                mActivePointerId = INVALID_POINTER;
179                if (mVelocityTracker != null) {
180                    mVelocityTracker.recycle();
181                    mVelocityTracker = null;
182                }
183                break;
184            }
185        }
186
187        if (mVelocityTracker != null) {
188            mVelocityTracker.addMovement(ev);
189        }
190
191        return true;
192    }
193
194    int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset) {
195        return setHeaderTopBottomOffset(parent, header, newOffset,
196                Integer.MIN_VALUE, Integer.MAX_VALUE);
197    }
198
199    int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset,
200            int minOffset, int maxOffset) {
201        final int curOffset = getTopAndBottomOffset();
202        int consumed = 0;
203
204        if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
205            // If we have some scrolling range, and we're currently within the min and max
206            // offsets, calculate a new offset
207            newOffset = MathUtils.constrain(newOffset, minOffset, maxOffset);
208
209            if (curOffset != newOffset) {
210                setTopAndBottomOffset(newOffset);
211                // Update how much dy we have consumed
212                consumed = curOffset - newOffset;
213            }
214        }
215
216        return consumed;
217    }
218
219    int getTopBottomOffsetForScrollingSibling() {
220        return getTopAndBottomOffset();
221    }
222
223    final int scroll(CoordinatorLayout coordinatorLayout, V header,
224            int dy, int minOffset, int maxOffset) {
225        return setHeaderTopBottomOffset(coordinatorLayout, header,
226                getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
227    }
228
229    final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset,
230            int maxOffset, float velocityY) {
231        if (mFlingRunnable != null) {
232            layout.removeCallbacks(mFlingRunnable);
233            mFlingRunnable = null;
234        }
235
236        if (mScroller == null) {
237            mScroller = ScrollerCompat.create(layout.getContext());
238        }
239
240        mScroller.fling(
241                0, getTopAndBottomOffset(), // curr
242                0, Math.round(velocityY), // velocity.
243                0, 0, // x
244                minOffset, maxOffset); // y
245
246        if (mScroller.computeScrollOffset()) {
247            mFlingRunnable = new FlingRunnable(coordinatorLayout, layout);
248            ViewCompat.postOnAnimation(layout, mFlingRunnable);
249            return true;
250        } else {
251            onFlingFinished(coordinatorLayout, layout);
252            return false;
253        }
254    }
255
256    /**
257     * Called when a fling has finished, or the fling was initiated but there wasn't enough
258     * velocity to start it.
259     */
260    void onFlingFinished(CoordinatorLayout parent, V layout) {
261        // no-op
262    }
263
264    /**
265     * Return true if the view can be dragged.
266     */
267    boolean canDragView(V view) {
268        return false;
269    }
270
271    /**
272     * Returns the maximum px offset when {@code view} is being dragged.
273     */
274    int getMaxDragOffset(V view) {
275        return -view.getHeight();
276    }
277
278    int getScrollRangeForDragFling(V view) {
279        return view.getHeight();
280    }
281
282    private void ensureVelocityTracker() {
283        if (mVelocityTracker == null) {
284            mVelocityTracker = VelocityTracker.obtain();
285        }
286    }
287
288    private class FlingRunnable implements Runnable {
289        private final CoordinatorLayout mParent;
290        private final V mLayout;
291
292        FlingRunnable(CoordinatorLayout parent, V layout) {
293            mParent = parent;
294            mLayout = layout;
295        }
296
297        @Override
298        public void run() {
299            if (mLayout != null && mScroller != null) {
300                if (mScroller.computeScrollOffset()) {
301                    setHeaderTopBottomOffset(mParent, mLayout, mScroller.getCurrY());
302                    // Post ourselves so that we run on the next animation
303                    ViewCompat.postOnAnimation(mLayout, this);
304                } else {
305                    onFlingFinished(mParent, mLayout);
306                }
307            }
308        }
309    }
310}
311