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