Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 package org.chromium.chrome.browser.widget; | |
| 6 | |
| 7 import android.animation.Animator; | |
| 8 import android.animation.AnimatorListenerAdapter; | |
| 9 import android.animation.ObjectAnimator; | |
| 10 import android.content.Context; | |
| 11 import android.graphics.Region; | |
| 12 import android.support.annotation.IntDef; | |
| 13 import android.util.AttributeSet; | |
| 14 import android.view.GestureDetector; | |
| 15 import android.view.MotionEvent; | |
| 16 import android.view.VelocityTracker; | |
| 17 import android.view.View; | |
| 18 import android.view.animation.DecelerateInterpolator; | |
| 19 import android.view.animation.Interpolator; | |
| 20 import android.widget.LinearLayout; | |
| 21 | |
| 22 import org.chromium.chrome.browser.util.MathUtils; | |
| 23 | |
| 24 import java.lang.annotation.Retention; | |
| 25 import java.lang.annotation.RetentionPolicy; | |
| 26 | |
| 27 /** | |
| 28 * This class defines the bottom sheet that has multiple states and a persistent ly showing toolbar. | |
| 29 * Namely, the states are: | |
| 30 * - PEEK: Only the toolbar is visible at the bottom of the screen. | |
| 31 * - HALF: The sheet is expanded to consume around half of the screen. | |
| 32 * - FULL: The sheet is expanded to its full height. | |
| 33 * | |
| 34 * All the computation in this file is based off of the bottom of the screen ins tead of the top | |
| 35 * for simplicity. This means that the bottom of the screen is 0 on the Y axis. | |
| 36 */ | |
| 37 public class BottomSheet extends LinearLayout { | |
| 38 /** The different states that the bottom sheet can have. */ | |
| 39 @IntDef({SHEET_STATE_PEEK, SHEET_STATE_HALF, SHEET_STATE_FULL}) | |
| 40 @Retention(RetentionPolicy.SOURCE) | |
| 41 public @interface SheetState {} | |
| 42 public static final int SHEET_STATE_PEEK = 0; | |
| 43 public static final int SHEET_STATE_HALF = 1; | |
| 44 public static final int SHEET_STATE_FULL = 2; | |
| 45 | |
| 46 /** | |
| 47 * The base duration of the settling animation of the sheet. 218 ms is a spe c for material | |
| 48 * design (this is the minimum time a user is guaranteed to pay attention to something). | |
| 49 */ | |
| 50 private static final long BASE_ANIMATION_DURATION_MS = 218; | |
| 51 | |
| 52 /** | |
| 53 * The fraction of the way to the next state the sheet must be swiped to ani mate there when | |
| 54 * released. A smaller value here means a smaller swipe is needed to move th e sheet around. | |
| 55 */ | |
| 56 private static final float THRESHOLD_TO_NEXT_STATE = 0.5f; | |
| 57 | |
| 58 /** The minimum y/x ratio that a scroll must have to be considered vertical. */ | |
| 59 private static final float MIN_VERTICAL_SCROLL_SLOPE = 2.0f; | |
| 60 | |
| 61 /** | |
| 62 * Information about the different scroll states of the sheet. Order is impo rtant for these, | |
| 63 * they go from smallest to largest. | |
| 64 */ | |
| 65 private static final int[] sStates = | |
| 66 new int[] {SHEET_STATE_PEEK, SHEET_STATE_HALF, SHEET_STATE_FULL}; | |
| 67 private final float[] mStateRatios = new float[] {0.0f, 0.55f, 0.95f}; | |
| 68 | |
| 69 /** The interpolator that the height animator uses. */ | |
| 70 private final Interpolator mInterpolator = new DecelerateInterpolator(1.0f); | |
| 71 | |
| 72 /** For detecting scroll and fling events on the bottom sheet. */ | |
| 73 private GestureDetector mGestureDetector; | |
| 74 | |
| 75 /** Whether or not the user is scrolling the bottom sheet. */ | |
| 76 private boolean mIsScrolling; | |
| 77 | |
| 78 /** Track the velocity of the user's scrolls to determine up or down directi on. */ | |
| 79 private VelocityTracker mVelocityTracker; | |
| 80 | |
| 81 /** The animator used to move the sheet to a fixed state when released by th e user. */ | |
| 82 private ObjectAnimator mSettleAnimator; | |
| 83 | |
| 84 /** The height of the toolbar. */ | |
| 85 private float mToolbarHeight; | |
| 86 | |
| 87 /** The height of the view that contains the bottom sheet. */ | |
| 88 private float mContainerHeight; | |
| 89 | |
| 90 /** The current sheet state. If the sheet is moving, this will be the target state. */ | |
| 91 private int mCurrentState; | |
| 92 | |
| 93 /** | |
| 94 * This class is responsible for detecting swipe and scroll events on the bo ttom sheet or | |
| 95 * ignoring them when appropriate. | |
| 96 */ | |
| 97 private class BottomSheetSwipeDetector extends GestureDetector.SimpleOnGestu reListener { | |
| 98 @Override | |
| 99 public boolean onDown(MotionEvent e) { | |
| 100 return true; | |
| 101 } | |
| 102 | |
| 103 @Override | |
| 104 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, | |
| 105 float distanceY) { | |
| 106 // Only start scrolling if the scroll is up or down. If the user is already scrolling, | |
| 107 // continue moving the sheet. | |
| 108 float slope = Math.abs(distanceX) > 0f ? Math.abs(distanceY) / Math. abs(distanceX) : 0f; | |
| 109 if (!mIsScrolling && slope < MIN_VERTICAL_SCROLL_SLOPE) { | |
| 110 mVelocityTracker.clear(); | |
| 111 return false; | |
| 112 } | |
| 113 | |
| 114 // Cancel the settling animation if it is running so it doesn't conf lict with where the | |
| 115 // user wants to move the sheet. | |
| 116 cancelAnimation(); | |
| 117 | |
| 118 mVelocityTracker.addMovement(e2); | |
| 119 | |
| 120 float currentShownRatio = | |
| 121 mContainerHeight > 0 ? getSheetOffsetFromBottom() / mContain erHeight : 0; | |
| 122 | |
| 123 // If the sheet is in the max position, don't move if the scroll is upward. | |
| 124 if (currentShownRatio >= mStateRatios[mStateRatios.length - 1] | |
| 125 && distanceY > 0) { | |
| 126 mIsScrolling = false; | |
| 127 return false; | |
| 128 } | |
| 129 | |
| 130 // Similarly, if the sheet is in the min position, don't move if the scroll is downward. | |
| 131 if (currentShownRatio <= mStateRatios[0] && distanceY < 0) { | |
| 132 mIsScrolling = false; | |
| 133 return false; | |
| 134 } | |
| 135 | |
| 136 float newOffset = getSheetOffsetFromBottom() + distanceY; | |
| 137 setSheetOffsetFromBottom(MathUtils.clamp(newOffset, getMinOffset(), getMaxOffset())); | |
| 138 | |
| 139 mIsScrolling = true; | |
| 140 return true; | |
| 141 } | |
| 142 | |
| 143 @Override | |
| 144 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, | |
| 145 float velocityY) { | |
| 146 cancelAnimation(); | |
| 147 | |
| 148 // Figure out the projected state of the sheet and animate there. No te that a swipe up | |
| 149 // will have a negative velocity, swipe down will have a positive ve locity. Negate this | |
| 150 // values so that the logic is more intuitive. | |
| 151 int targetState = getTargetSheetState( | |
| 152 getSheetOffsetFromBottom() + getFlingDistance(-velocityY), - velocityY); | |
| 153 setSheetState(targetState, true); | |
| 154 mIsScrolling = false; | |
| 155 | |
| 156 return true; | |
| 157 } | |
| 158 } | |
| 159 | |
| 160 /** | |
| 161 * Constructor for inflation from XML. | |
| 162 * @param context An Android context. | |
| 163 * @param atts The XML attributes. | |
| 164 */ | |
| 165 public BottomSheet(Context context, AttributeSet atts) { | |
| 166 super(context, atts); | |
| 167 | |
| 168 setOrientation(LinearLayout.VERTICAL); | |
| 169 | |
| 170 mVelocityTracker = VelocityTracker.obtain(); | |
| 171 | |
| 172 mGestureDetector = new GestureDetector(context, new BottomSheetSwipeDete ctor()); | |
| 173 mGestureDetector.setIsLongpressEnabled(false); | |
| 174 } | |
| 175 | |
| 176 @Override | |
| 177 public boolean onInterceptTouchEvent(MotionEvent e) { | |
| 178 // The incoming motion event may have been adjusted by the view sending it down. Create a | |
| 179 // motion event with the raw (x, y) coordinates of the original so the g esture detector | |
| 180 // functions properly. | |
| 181 mGestureDetector.onTouchEvent(createRawMotionEvent(e)); | |
| 182 return mIsScrolling; | |
| 183 } | |
| 184 | |
| 185 @Override | |
| 186 public boolean onTouchEvent(MotionEvent e) { | |
| 187 // The down event is interpreted above in onInterceptTouchEvent, it does not need to be | |
| 188 // interpreted a second time. | |
| 189 if (e.getActionMasked() != MotionEvent.ACTION_DOWN) { | |
| 190 mGestureDetector.onTouchEvent(createRawMotionEvent(e)); | |
| 191 } | |
| 192 | |
| 193 // If the user is scrolling and the event is a cancel or up action, upda te scroll state | |
| 194 // and return. | |
| 195 if (e.getActionMasked() == MotionEvent.ACTION_UP | |
| 196 || e.getActionMasked() == MotionEvent.ACTION_CANCEL) { | |
| 197 mIsScrolling = false; | |
| 198 | |
| 199 mVelocityTracker.computeCurrentVelocity(1000); | |
| 200 | |
| 201 // If an animation was not created to settle the sheet at some state , do it now. | |
| 202 if (mSettleAnimator == null) { | |
| 203 // Negate velocity so a positive number indicates a swipe up. | |
| 204 float currentVelocity = -mVelocityTracker.getYVelocity(); | |
| 205 int targetState = getTargetSheetState(getSheetOffsetFromBottom() , currentVelocity); | |
| 206 | |
| 207 setSheetState(targetState, true); | |
| 208 } | |
| 209 } | |
| 210 | |
| 211 return true; | |
| 212 } | |
| 213 | |
| 214 @Override | |
| 215 public boolean gatherTransparentRegion(Region region) { | |
| 216 // TODO(mdjones): Figure out what this should actually be set to since t he view animates | |
| 217 // without necessarily calling this method again. | |
| 218 region.setEmpty(); | |
|
Bernhard Bauer
2017/01/25 12:02:28
Drive-by: I would be very careful with this -- the
| |
| 219 return true; | |
| 220 } | |
| 221 | |
| 222 /** | |
| 223 * Adds layout change listeners to the views that the bottom sheet depends o n. Namely the | |
| 224 * heights of the root view and control container are important as they are used in many of the | |
| 225 * calculations in this class. | |
| 226 * @param root The container of the bottom sheet. | |
| 227 * @param controlContainer The container for the toolbar. | |
| 228 */ | |
| 229 public void init(View root, View controlContainer) { | |
| 230 mToolbarHeight = controlContainer.getHeight(); | |
| 231 mCurrentState = SHEET_STATE_PEEK; | |
| 232 | |
| 233 // Listen to height changes on the root. | |
| 234 root.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { | |
| 235 public void onLayoutChange(View v, int left, int top, int right, int bottom, | |
| 236 int oldLeft, int oldTop, int oldRight, int oldBottom) { | |
| 237 mContainerHeight = bottom - top; | |
| 238 updateSheetPeekHeight(); | |
| 239 setSheetState(mCurrentState, false); | |
| 240 } | |
| 241 }); | |
| 242 | |
| 243 // Listen to height changes on the toolbar. | |
| 244 controlContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListen er() { | |
| 245 public void onLayoutChange(View v, int left, int top, int right, int bottom, | |
| 246 int oldLeft, int oldTop, int oldRight, int oldBottom) { | |
| 247 mToolbarHeight = bottom - top; | |
| 248 updateSheetPeekHeight(); | |
| 249 setSheetState(mCurrentState, false); | |
| 250 } | |
| 251 }); | |
| 252 } | |
| 253 | |
| 254 /** | |
| 255 * Creates an unadjusted version of a MotionEvent. | |
| 256 * @param e The original event. | |
| 257 * @return The unadjusted version of the event. | |
| 258 */ | |
| 259 private MotionEvent createRawMotionEvent(MotionEvent e) { | |
| 260 MotionEvent rawEvent = MotionEvent.obtain(e); | |
| 261 rawEvent.setLocation(e.getRawX(), e.getRawY()); | |
| 262 return rawEvent; | |
| 263 } | |
| 264 | |
| 265 /** | |
| 266 * Updates the bottom sheet's peeking height. | |
| 267 */ | |
| 268 private void updateSheetPeekHeight() { | |
| 269 if (mContainerHeight <= 0) return; | |
| 270 | |
| 271 mStateRatios[0] = mToolbarHeight / mContainerHeight; | |
| 272 } | |
| 273 | |
| 274 /** | |
| 275 * Cancels and nulls the height animation if it exists. | |
| 276 */ | |
| 277 private void cancelAnimation() { | |
| 278 if (mSettleAnimator == null) return; | |
| 279 mSettleAnimator.cancel(); | |
| 280 mSettleAnimator = null; | |
| 281 } | |
| 282 | |
| 283 /** | |
| 284 * Creates the sheet's animation to a target state. | |
| 285 * @param targetState The target state. | |
| 286 */ | |
| 287 private void createSettleAnimation(@SheetState int targetState) { | |
| 288 mCurrentState = targetState; | |
| 289 mSettleAnimator = ObjectAnimator.ofFloat( | |
| 290 this, View.TRANSLATION_Y, mContainerHeight - getSheetHeightForSt ate(targetState)); | |
| 291 mSettleAnimator.setDuration(BASE_ANIMATION_DURATION_MS); | |
| 292 mSettleAnimator.setInterpolator(mInterpolator); | |
| 293 | |
| 294 // When the animation is canceled or ends, reset the handle to null. | |
| 295 mSettleAnimator.addListener(new AnimatorListenerAdapter() { | |
| 296 @Override | |
| 297 public void onAnimationEnd(Animator animator) { | |
| 298 mSettleAnimator = null; | |
| 299 } | |
| 300 }); | |
| 301 | |
| 302 mSettleAnimator.start(); | |
| 303 } | |
| 304 | |
| 305 /** | |
| 306 * Gets the distance of a fling based on the velocity and the base animation time. This formula | |
| 307 * assumes the deceleration curve is quadratic (t^2), hence the displacement formula should be: | |
| 308 * displacement = initialVelocity * duration / 2. | |
| 309 * @param velocity The velocity of the fling. | |
| 310 * @return The distance the fling would cover. | |
| 311 */ | |
| 312 private float getFlingDistance(float velocity) { | |
| 313 // This includes conversion from seconds to ms. | |
| 314 return velocity * BASE_ANIMATION_DURATION_MS / 2000f; | |
| 315 } | |
| 316 | |
| 317 /** | |
| 318 * Gets the maximum offset of the bottom sheet. | |
| 319 * @return The max offset. | |
| 320 */ | |
| 321 private float getMaxOffset() { | |
| 322 return mStateRatios[mStateRatios.length - 1] * mContainerHeight; | |
| 323 } | |
| 324 | |
| 325 /** | |
| 326 * Gets the minimum offset of the bottom sheet. | |
| 327 * @return The min offset. | |
| 328 */ | |
| 329 private float getMinOffset() { | |
| 330 return mStateRatios[0] * mContainerHeight; | |
| 331 } | |
| 332 | |
| 333 /** | |
| 334 * Gets the sheet's offset from the bottom of the screen. | |
| 335 * @return The sheet's distance from the bottom of the screen. | |
| 336 */ | |
| 337 private float getSheetOffsetFromBottom() { | |
| 338 return mContainerHeight - getTranslationY(); | |
| 339 } | |
| 340 | |
| 341 /** | |
| 342 * Sets the sheet's offset relative to the bottom of the screen. | |
| 343 * @param offset The offset that the sheet should be. | |
| 344 */ | |
| 345 private void setSheetOffsetFromBottom(float offset) { | |
| 346 setTranslationY(mContainerHeight - offset); | |
| 347 } | |
| 348 | |
| 349 /** | |
| 350 * Moves the sheet to the provided state. | |
| 351 * @param state The state to move the panel to. | |
| 352 * @param animate If true, the sheet will animate to the provided state, oth erwise it will | |
| 353 * move there instantly. | |
| 354 */ | |
| 355 private void setSheetState(@SheetState int state, boolean animate) { | |
| 356 mCurrentState = state; | |
| 357 | |
| 358 if (animate) { | |
| 359 createSettleAnimation(state); | |
| 360 } else { | |
| 361 setSheetOffsetFromBottom(getSheetHeightForState(state)); | |
| 362 } | |
| 363 } | |
| 364 | |
| 365 /** | |
| 366 * Gets the height of the bottom sheet based on a provided state. | |
| 367 * @param state The state to get the height from. | |
| 368 * @return The height of the sheet at the provided state. | |
| 369 */ | |
| 370 private float getSheetHeightForState(@SheetState int state) { | |
| 371 return mStateRatios[state] * mContainerHeight; | |
| 372 } | |
| 373 | |
| 374 /** | |
| 375 * Gets the target state of the sheet based on the sheet's height and veloci ty. | |
| 376 * @param sheetHeight The current height of the sheet. | |
| 377 * @param yVelocity The current Y velocity of the sheet. This is only used f or determining the | |
| 378 * scroll or fling direction. If this value is positive, th e movement is from | |
| 379 * bottom to top. | |
| 380 * @return The target state of the bottom sheet. | |
| 381 */ | |
| 382 private int getTargetSheetState(float sheetHeight, float yVelocity) { | |
| 383 if (sheetHeight <= getMinOffset()) return SHEET_STATE_PEEK; | |
| 384 if (sheetHeight >= getMaxOffset()) return SHEET_STATE_FULL; | |
| 385 | |
| 386 // First, find the two states that the sheet height is between. | |
| 387 int nextState = sStates[0]; | |
| 388 int prevState = nextState; | |
| 389 for (int i = 0; i < sStates.length; i++) { | |
| 390 prevState = nextState; | |
| 391 nextState = sStates[i]; | |
| 392 // The values in PanelState are ascending, they should be kept that way in order for | |
| 393 // this to work. | |
| 394 if (sheetHeight >= getSheetHeightForState(prevState) | |
| 395 && sheetHeight < getSheetHeightForState(nextState)) { | |
| 396 break; | |
| 397 } | |
| 398 } | |
| 399 | |
| 400 // If the desired height is close enough to a certain state, depending o n the direction of | |
| 401 // the velocity, move to that state. | |
| 402 float lowerBound = getSheetHeightForState(prevState); | |
| 403 float distance = getSheetHeightForState(nextState) - lowerBound; | |
| 404 | |
| 405 float inverseThreshold = 1.0f - THRESHOLD_TO_NEXT_STATE; | |
| 406 float thresholdToNextState = yVelocity < 0.0f ? THRESHOLD_TO_NEXT_STATE : inverseThreshold; | |
| 407 | |
| 408 if ((sheetHeight - lowerBound) / distance > thresholdToNextState) { | |
| 409 return nextState; | |
| 410 } | |
| 411 return prevState; | |
| 412 } | |
| 413 } | |
| OLD | NEW |