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