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