| 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.ValueAnimator; | |
| 10 import android.content.Context; | |
| 11 import android.graphics.Region; | |
| 12 import android.support.annotation.IntDef; | |
| 13 import android.support.annotation.Nullable; | |
| 14 import android.util.AttributeSet; | |
| 15 import android.view.GestureDetector; | |
| 16 import android.view.MotionEvent; | |
| 17 import android.view.VelocityTracker; | |
| 18 import android.view.View; | |
| 19 import android.view.animation.DecelerateInterpolator; | |
| 20 import android.view.animation.Interpolator; | |
| 21 import android.widget.FrameLayout; | |
| 22 | |
| 23 import org.chromium.base.ApiCompatibilityUtils; | |
| 24 import org.chromium.base.ObserverList; | |
| 25 import org.chromium.base.VisibleForTesting; | |
| 26 import org.chromium.chrome.R; | |
| 27 import org.chromium.chrome.browser.NativePageHost; | |
| 28 import org.chromium.chrome.browser.TabLoadStatus; | |
| 29 import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager; | |
| 30 import org.chromium.chrome.browser.ntp.NativePageFactory; | |
| 31 import org.chromium.chrome.browser.tab.Tab; | |
| 32 import org.chromium.chrome.browser.tabmodel.TabModel; | |
| 33 import org.chromium.chrome.browser.tabmodel.TabModelSelector; | |
| 34 import org.chromium.chrome.browser.util.MathUtils; | |
| 35 import org.chromium.content_public.browser.LoadUrlParams; | |
| 36 | |
| 37 import java.lang.annotation.Retention; | |
| 38 import java.lang.annotation.RetentionPolicy; | |
| 39 | |
| 40 /** | |
| 41 * This class defines the bottom sheet that has multiple states and a persistent
ly showing toolbar. | |
| 42 * Namely, the states are: | |
| 43 * - PEEK: Only the toolbar is visible at the bottom of the screen. | |
| 44 * - HALF: The sheet is expanded to consume around half of the screen. | |
| 45 * - FULL: The sheet is expanded to its full height. | |
| 46 * | |
| 47 * All the computation in this file is based off of the bottom of the screen ins
tead of the top | |
| 48 * for simplicity. This means that the bottom of the screen is 0 on the Y axis. | |
| 49 */ | |
| 50 | |
| 51 public class BottomSheet | |
| 52 extends FrameLayout implements FadingBackgroundView.FadingViewObserver,
NativePageHost { | |
| 53 /** The different states that the bottom sheet can have. */ | |
| 54 @IntDef({SHEET_STATE_PEEK, SHEET_STATE_HALF, SHEET_STATE_FULL}) | |
| 55 @Retention(RetentionPolicy.SOURCE) | |
| 56 public @interface SheetState {} | |
| 57 public static final int SHEET_STATE_PEEK = 0; | |
| 58 public static final int SHEET_STATE_HALF = 1; | |
| 59 public static final int SHEET_STATE_FULL = 2; | |
| 60 | |
| 61 /** | |
| 62 * The base duration of the settling animation of the sheet. 218 ms is a spe
c for material | |
| 63 * design (this is the minimum time a user is guaranteed to pay attention to
something). | |
| 64 */ | |
| 65 private static final long BASE_ANIMATION_DURATION_MS = 218; | |
| 66 | |
| 67 /** | |
| 68 * The fraction of the way to the next state the sheet must be swiped to ani
mate there when | |
| 69 * released. A smaller value here means a smaller swipe is needed to move th
e sheet around. | |
| 70 */ | |
| 71 private static final float THRESHOLD_TO_NEXT_STATE = 0.5f; | |
| 72 | |
| 73 /** The minimum y/x ratio that a scroll must have to be considered vertical.
*/ | |
| 74 private static final float MIN_VERTICAL_SCROLL_SLOPE = 2.0f; | |
| 75 | |
| 76 /** | |
| 77 * Information about the different scroll states of the sheet. Order is impo
rtant for these, | |
| 78 * they go from smallest to largest. | |
| 79 */ | |
| 80 private static final int[] sStates = | |
| 81 new int[] {SHEET_STATE_PEEK, SHEET_STATE_HALF, SHEET_STATE_FULL}; | |
| 82 private final float[] mStateRatios = new float[] {0.0f, 0.55f, 0.95f}; | |
| 83 | |
| 84 /** The interpolator that the height animator uses. */ | |
| 85 private final Interpolator mInterpolator = new DecelerateInterpolator(1.0f); | |
| 86 | |
| 87 /** The list of observers of this sheet. */ | |
| 88 private final ObserverList<BottomSheetObserver> mObservers = new ObserverLis
t<>(); | |
| 89 | |
| 90 /** This is a cached array for getting the window location of different view
s. */ | |
| 91 private final int[] mLocationArray = new int[2]; | |
| 92 | |
| 93 /** For detecting scroll and fling events on the bottom sheet. */ | |
| 94 private GestureDetector mGestureDetector; | |
| 95 | |
| 96 /** Whether or not the user is scrolling the bottom sheet. */ | |
| 97 private boolean mIsScrolling; | |
| 98 | |
| 99 /** Track the velocity of the user's scrolls to determine up or down directi
on. */ | |
| 100 private VelocityTracker mVelocityTracker; | |
| 101 | |
| 102 /** The animator used to move the sheet to a fixed state when released by th
e user. */ | |
| 103 private ValueAnimator mSettleAnimator; | |
| 104 | |
| 105 /** The height of the toolbar. */ | |
| 106 private float mToolbarHeight; | |
| 107 | |
| 108 /** The width of the view that contains the bottom sheet. */ | |
| 109 private float mContainerWidth; | |
| 110 | |
| 111 /** The height of the view that contains the bottom sheet. */ | |
| 112 private float mContainerHeight; | |
| 113 | |
| 114 /** The current sheet state. If the sheet is moving, this will be the target
state. */ | |
| 115 private int mCurrentState; | |
| 116 | |
| 117 /** Used for getting the current tab. */ | |
| 118 private TabModelSelector mTabModelSelector; | |
| 119 | |
| 120 /** The fullscreen manager for information about toolbar offsets. */ | |
| 121 private ChromeFullscreenManager mFullscreenManager; | |
| 122 | |
| 123 /** A handle to the content being shown by the sheet. */ | |
| 124 private BottomSheetContent mSheetContent; | |
| 125 | |
| 126 /** A handle to the toolbar control container. */ | |
| 127 private View mControlContainer; | |
| 128 | |
| 129 /** A placeholder for if there is no content in the bottom sheet. */ | |
| 130 private View mPlaceholder; | |
| 131 | |
| 132 /** A handle to the FrameLayout that holds the content of the bottom sheet.
*/ | |
| 133 private FrameLayout mBottomSheetContentContainer; | |
| 134 | |
| 135 /** | |
| 136 * The last ratio sent to observers of onTransitionPeekToHalf(). This is use
d to ensure the | |
| 137 * final value sent to these observers is 1.0f. | |
| 138 */ | |
| 139 private float mLastPeekToHalfRatioSent; | |
| 140 | |
| 141 /** The FrameLayout used to hold the bottom sheet toolbar. */ | |
| 142 private FrameLayout mToolbarHolder; | |
| 143 | |
| 144 /** | |
| 145 * The default toolbar view. This is shown when the current bottom sheet con
tent doesn't have | |
| 146 * its own toolbar and when the bottom sheet is closed. | |
| 147 */ | |
| 148 private View mDefaultToolbarView; | |
| 149 | |
| 150 /** The last non-default toolbar view that was attached to mToolbarHolder. *
/ | |
| 151 private View mLastToolbarView; | |
| 152 | |
| 153 /** | |
| 154 * An interface defining content that can be displayed inside of the bottom
sheet for Chrome | |
| 155 * Home. | |
| 156 */ | |
| 157 public interface BottomSheetContent { | |
| 158 /** | |
| 159 * Gets the {@link View} that holds the content to be displayed in the C
hrome Home bottom | |
| 160 * sheet. | |
| 161 * @return The content view. | |
| 162 */ | |
| 163 View getContentView(); | |
| 164 | |
| 165 /** | |
| 166 * Get the {@link View} that contains the toolbar specific to the conten
t being displayed. | |
| 167 * If null is returned, the omnibox is used. | |
| 168 * TODO(mdjones): This still needs implementation in the sheet. | |
| 169 * | |
| 170 * @return The toolbar view. | |
| 171 */ | |
| 172 @Nullable | |
| 173 View getToolbarView(); | |
| 174 | |
| 175 /** | |
| 176 * @return The vertical scroll offset of the content view. | |
| 177 */ | |
| 178 int getVerticalScrollOffset(); | |
| 179 | |
| 180 /** | |
| 181 * Called to destroy the BottomSheetContent when it is no longer in use. | |
| 182 */ | |
| 183 void destroy(); | |
| 184 } | |
| 185 | |
| 186 /** | |
| 187 * This class is responsible for detecting swipe and scroll events on the bo
ttom sheet or | |
| 188 * ignoring them when appropriate. | |
| 189 */ | |
| 190 private class BottomSheetSwipeDetector extends GestureDetector.SimpleOnGestu
reListener { | |
| 191 @Override | |
| 192 public boolean onDown(MotionEvent e) { | |
| 193 return true; | |
| 194 } | |
| 195 | |
| 196 @Override | |
| 197 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, | |
| 198 float distanceY) { | |
| 199 // Only start scrolling if the scroll is up or down. If the user is
already scrolling, | |
| 200 // continue moving the sheet. | |
| 201 float slope = Math.abs(distanceX) > 0f ? Math.abs(distanceY) / Math.
abs(distanceX) : 0f; | |
| 202 if (!mIsScrolling && slope < MIN_VERTICAL_SCROLL_SLOPE) { | |
| 203 mVelocityTracker.clear(); | |
| 204 return false; | |
| 205 } | |
| 206 | |
| 207 // Cancel the settling animation if it is running so it doesn't conf
lict with where the | |
| 208 // user wants to move the sheet. | |
| 209 cancelAnimation(); | |
| 210 | |
| 211 mVelocityTracker.addMovement(e2); | |
| 212 | |
| 213 float currentShownRatio = | |
| 214 mContainerHeight > 0 ? getSheetOffsetFromBottom() / mContain
erHeight : 0; | |
| 215 boolean isSheetInMaxPosition = | |
| 216 MathUtils.areFloatsEqual(currentShownRatio, getFullRatio()); | |
| 217 | |
| 218 // Allow the bottom sheet's content to be scrolled up without draggi
ng the sheet down. | |
| 219 if (!isTouchEventInToolbar(e2) && isSheetInMaxPosition && mSheetCont
ent != null | |
| 220 && mSheetContent.getVerticalScrollOffset() > 0) { | |
| 221 mIsScrolling = false; | |
| 222 return false; | |
| 223 } | |
| 224 | |
| 225 // If the sheet is in the max position, don't move the sheet if the
scroll is upward. | |
| 226 // Instead, allow the sheet's content to handle it if it needs to. | |
| 227 if (isSheetInMaxPosition && distanceY > 0) { | |
| 228 mIsScrolling = false; | |
| 229 return false; | |
| 230 } | |
| 231 | |
| 232 // Similarly, if the sheet is in the min position, don't move if the
scroll is downward. | |
| 233 if (currentShownRatio <= getPeekRatio() && distanceY < 0) { | |
| 234 mIsScrolling = false; | |
| 235 return false; | |
| 236 } | |
| 237 | |
| 238 float newOffset = getSheetOffsetFromBottom() + distanceY; | |
| 239 setSheetOffsetFromBottom(MathUtils.clamp(newOffset, getMinOffset(),
getMaxOffset())); | |
| 240 | |
| 241 mIsScrolling = true; | |
| 242 return true; | |
| 243 } | |
| 244 | |
| 245 @Override | |
| 246 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, | |
| 247 float velocityY) { | |
| 248 cancelAnimation(); | |
| 249 | |
| 250 // Figure out the projected state of the sheet and animate there. No
te that a swipe up | |
| 251 // will have a negative velocity, swipe down will have a positive ve
locity. Negate this | |
| 252 // values so that the logic is more intuitive. | |
| 253 @SheetState | |
| 254 int targetState = getTargetSheetState( | |
| 255 getSheetOffsetFromBottom() + getFlingDistance(-velocityY), -
velocityY); | |
| 256 setSheetState(targetState, true); | |
| 257 mIsScrolling = false; | |
| 258 | |
| 259 return true; | |
| 260 } | |
| 261 } | |
| 262 | |
| 263 /** | |
| 264 * Constructor for inflation from XML. | |
| 265 * @param context An Android context. | |
| 266 * @param atts The XML attributes. | |
| 267 */ | |
| 268 public BottomSheet(Context context, AttributeSet atts) { | |
| 269 super(context, atts); | |
| 270 | |
| 271 mVelocityTracker = VelocityTracker.obtain(); | |
| 272 | |
| 273 mGestureDetector = new GestureDetector(context, new BottomSheetSwipeDete
ctor()); | |
| 274 mGestureDetector.setIsLongpressEnabled(false); | |
| 275 } | |
| 276 | |
| 277 @Override | |
| 278 public boolean onInterceptTouchEvent(MotionEvent e) { | |
| 279 if (!canMoveSheet()) return false; | |
| 280 | |
| 281 // The incoming motion event may have been adjusted by the view sending
it down. Create a | |
| 282 // motion event with the raw (x, y) coordinates of the original so the g
esture detector | |
| 283 // functions properly. | |
| 284 mGestureDetector.onTouchEvent(createRawMotionEvent(e)); | |
| 285 return mIsScrolling; | |
| 286 } | |
| 287 | |
| 288 @Override | |
| 289 public boolean onTouchEvent(MotionEvent e) { | |
| 290 if (isToolbarAndroidViewHidden()) return false; | |
| 291 | |
| 292 // The down event is interpreted above in onInterceptTouchEvent, it does
not need to be | |
| 293 // interpreted a second time. | |
| 294 if (e.getActionMasked() != MotionEvent.ACTION_DOWN) { | |
| 295 mGestureDetector.onTouchEvent(createRawMotionEvent(e)); | |
| 296 } | |
| 297 | |
| 298 // If the user is scrolling and the event is a cancel or up action, upda
te scroll state | |
| 299 // and return. | |
| 300 if (e.getActionMasked() == MotionEvent.ACTION_UP | |
| 301 || e.getActionMasked() == MotionEvent.ACTION_CANCEL) { | |
| 302 mIsScrolling = false; | |
| 303 | |
| 304 mVelocityTracker.computeCurrentVelocity(1000); | |
| 305 | |
| 306 // If an animation was not created to settle the sheet at some state
, do it now. | |
| 307 if (mSettleAnimator == null) { | |
| 308 // Negate velocity so a positive number indicates a swipe up. | |
| 309 float currentVelocity = -mVelocityTracker.getYVelocity(); | |
| 310 @SheetState | |
| 311 int targetState = getTargetSheetState(getSheetOffsetFromBottom()
, currentVelocity); | |
| 312 | |
| 313 setSheetState(targetState, true); | |
| 314 } | |
| 315 } | |
| 316 | |
| 317 return true; | |
| 318 } | |
| 319 | |
| 320 @Override | |
| 321 public boolean gatherTransparentRegion(Region region) { | |
| 322 // TODO(mdjones): Figure out what this should actually be set to since t
he view animates | |
| 323 // without necessarily calling this method again. | |
| 324 region.setEmpty(); | |
| 325 return true; | |
| 326 } | |
| 327 | |
| 328 /** | |
| 329 * @param tabModelSelector A TabModelSelector for getting the current tab an
d activity. | |
| 330 */ | |
| 331 public void setTabModelSelector(TabModelSelector tabModelSelector) { | |
| 332 mTabModelSelector = tabModelSelector; | |
| 333 } | |
| 334 | |
| 335 /** | |
| 336 * @param fullscreenManager Chrome's fullscreen manager for information abou
t toolbar offsets. | |
| 337 */ | |
| 338 public void setFullscreenManager(ChromeFullscreenManager fullscreenManager)
{ | |
| 339 mFullscreenManager = fullscreenManager; | |
| 340 } | |
| 341 | |
| 342 /** | |
| 343 * @return Whether or not the toolbar Android View is hidden due to being sc
rolled off-screen. | |
| 344 */ | |
| 345 private boolean isToolbarAndroidViewHidden() { | |
| 346 return mFullscreenManager == null || mFullscreenManager.getBottomControl
Offset() > 0 | |
| 347 || mControlContainer.getVisibility() != VISIBLE; | |
| 348 } | |
| 349 | |
| 350 /** | |
| 351 * Adds layout change listeners to the views that the bottom sheet depends o
n. Namely the | |
| 352 * heights of the root view and control container are important as they are
used in many of the | |
| 353 * calculations in this class. | |
| 354 * @param root The container of the bottom sheet. | |
| 355 * @param controlContainer The container for the toolbar. | |
| 356 */ | |
| 357 public void init(View root, View controlContainer) { | |
| 358 mControlContainer = controlContainer; | |
| 359 mToolbarHeight = mControlContainer.getHeight(); | |
| 360 | |
| 361 mBottomSheetContentContainer = (FrameLayout) findViewById(R.id.bottom_sh
eet_content); | |
| 362 | |
| 363 mCurrentState = SHEET_STATE_PEEK; | |
| 364 | |
| 365 // Listen to height changes on the root. | |
| 366 root.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { | |
| 367 @Override | |
| 368 public void onLayoutChange(View v, int left, int top, int right, int
bottom, | |
| 369 int oldLeft, int oldTop, int oldRight, int oldBottom) { | |
| 370 // Make sure the size of the layout actually changed. | |
| 371 if (bottom - top == oldBottom - oldTop && right - left == oldRig
ht - oldLeft) { | |
| 372 return; | |
| 373 } | |
| 374 | |
| 375 mContainerWidth = right - left; | |
| 376 mContainerHeight = bottom - top; | |
| 377 updateSheetDimensions(); | |
| 378 | |
| 379 cancelAnimation(); | |
| 380 setSheetState(mCurrentState, false); | |
| 381 } | |
| 382 }); | |
| 383 | |
| 384 // Listen to height changes on the toolbar. | |
| 385 controlContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListen
er() { | |
| 386 @Override | |
| 387 public void onLayoutChange(View v, int left, int top, int right, int
bottom, | |
| 388 int oldLeft, int oldTop, int oldRight, int oldBottom) { | |
| 389 // Make sure the size of the layout actually changed. | |
| 390 if (bottom - top == oldBottom - oldTop && right - left == oldRig
ht - oldLeft) { | |
| 391 return; | |
| 392 } | |
| 393 | |
| 394 mToolbarHeight = bottom - top; | |
| 395 updateSheetDimensions(); | |
| 396 | |
| 397 cancelAnimation(); | |
| 398 setSheetState(mCurrentState, false); | |
| 399 } | |
| 400 }); | |
| 401 | |
| 402 mPlaceholder = new View(getContext()); | |
| 403 LayoutParams placeHolderParams = | |
| 404 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_P
ARENT); | |
| 405 mPlaceholder.setBackgroundColor( | |
| 406 ApiCompatibilityUtils.getColor(getResources(), android.R.color.w
hite)); | |
| 407 mBottomSheetContentContainer.addView(mPlaceholder, placeHolderParams); | |
| 408 | |
| 409 mToolbarHolder = (FrameLayout) mControlContainer.findViewById(R.id.toolb
ar_holder); | |
| 410 mDefaultToolbarView = mControlContainer.findViewById(R.id.toolbar); | |
| 411 } | |
| 412 | |
| 413 @Override | |
| 414 public int loadUrl(LoadUrlParams params, boolean incognito) { | |
| 415 for (BottomSheetObserver o : mObservers) o.onLoadUrl(params.getUrl()); | |
| 416 | |
| 417 // Native page URLs in this context do not need to communicate with the
tab. | |
| 418 if (NativePageFactory.isNativePageUrl(params.getUrl(), incognito)) { | |
| 419 return TabLoadStatus.PAGE_LOAD_FAILED; | |
| 420 } | |
| 421 | |
| 422 // In all non-native cases, minimize the sheet. | |
| 423 setSheetState(SHEET_STATE_PEEK, true); | |
| 424 | |
| 425 assert mTabModelSelector != null; | |
| 426 | |
| 427 // First try to get the tab behind the sheet. | |
| 428 if (getActiveTab() != null && getActiveTab().isIncognito() == incognito)
{ | |
| 429 return getActiveTab().loadUrl(params); | |
| 430 } | |
| 431 | |
| 432 // If no compatible tab is active behind the sheet, open a new one. | |
| 433 mTabModelSelector.openNewTab( | |
| 434 params, TabModel.TabLaunchType.FROM_CHROME_UI, getActiveTab(), i
ncognito); | |
| 435 return TabLoadStatus.DEFAULT_PAGE_LOAD; | |
| 436 } | |
| 437 | |
| 438 @Override | |
| 439 public boolean isIncognito() { | |
| 440 if (getActiveTab() == null) return false; | |
| 441 return getActiveTab().isIncognito(); | |
| 442 } | |
| 443 | |
| 444 @Override | |
| 445 public int getParentId() { | |
| 446 return Tab.INVALID_TAB_ID; | |
| 447 } | |
| 448 | |
| 449 @Override | |
| 450 public Tab getActiveTab() { | |
| 451 return mTabModelSelector.getCurrentTab(); | |
| 452 } | |
| 453 | |
| 454 @Override | |
| 455 public boolean isVisible() { | |
| 456 return mCurrentState != SHEET_STATE_PEEK; | |
| 457 } | |
| 458 | |
| 459 /** | |
| 460 * Gets the minimum offset of the bottom sheet. | |
| 461 * @return The min offset. | |
| 462 */ | |
| 463 public float getMinOffset() { | |
| 464 return getPeekRatio() * mContainerHeight; | |
| 465 } | |
| 466 | |
| 467 /** | |
| 468 * Gets the sheet's offset from the bottom of the screen. | |
| 469 * @return The sheet's distance from the bottom of the screen. | |
| 470 */ | |
| 471 public float getSheetOffsetFromBottom() { | |
| 472 return mContainerHeight - getTranslationY(); | |
| 473 } | |
| 474 | |
| 475 /** | |
| 476 * Show content in the bottom sheet's content area. | |
| 477 * @param content The {@link BottomSheetContent} to show. | |
| 478 */ | |
| 479 public void showContent(BottomSheetContent content) { | |
| 480 // If the desired content is already showing, do nothing. | |
| 481 if (mSheetContent == content) return; | |
| 482 | |
| 483 if (mSheetContent != null) { | |
| 484 mBottomSheetContentContainer.removeView(mSheetContent.getContentView
()); | |
| 485 mSheetContent = null; | |
| 486 } | |
| 487 | |
| 488 if (content == null) { | |
| 489 mBottomSheetContentContainer.addView(mPlaceholder); | |
| 490 return; | |
| 491 } | |
| 492 | |
| 493 mBottomSheetContentContainer.removeView(mPlaceholder); | |
| 494 mSheetContent = content; | |
| 495 mBottomSheetContentContainer.addView(mSheetContent.getContentView()); | |
| 496 | |
| 497 if (mLastToolbarView != null) { | |
| 498 mToolbarHolder.removeView(mLastToolbarView); | |
| 499 mLastToolbarView = null; | |
| 500 } | |
| 501 | |
| 502 if (mSheetContent.getToolbarView() != null) { | |
| 503 mLastToolbarView = mSheetContent.getToolbarView(); | |
| 504 mToolbarHolder.addView(mSheetContent.getToolbarView()); | |
| 505 mDefaultToolbarView.setVisibility(View.GONE); | |
| 506 } else { | |
| 507 mDefaultToolbarView.setVisibility(View.VISIBLE); | |
| 508 } | |
| 509 } | |
| 510 | |
| 511 /** | |
| 512 * Determines if a touch event is inside the toolbar. This assumes the toolb
ar is the full | |
| 513 * width of the screen and that the toolbar is at the top of the bottom shee
t. | |
| 514 * @param e The motion event to test. | |
| 515 * @return True if the event occured in the toolbar region. | |
| 516 */ | |
| 517 private boolean isTouchEventInToolbar(MotionEvent e) { | |
| 518 if (mControlContainer == null) return false; | |
| 519 | |
| 520 mControlContainer.getLocationInWindow(mLocationArray); | |
| 521 | |
| 522 return e.getRawY() < mLocationArray[1] + mToolbarHeight; | |
| 523 } | |
| 524 | |
| 525 /** | |
| 526 * A notification that the sheet is exiting the peek state into one that sho
ws content. | |
| 527 */ | |
| 528 private void onSheetOpened() { | |
| 529 for (BottomSheetObserver o : mObservers) o.onSheetOpened(); | |
| 530 } | |
| 531 | |
| 532 /** | |
| 533 * A notification that the sheet has returned to the peeking state. | |
| 534 */ | |
| 535 private void onSheetClosed() { | |
| 536 for (BottomSheetObserver o : mObservers) o.onSheetClosed(); | |
| 537 } | |
| 538 | |
| 539 /** | |
| 540 * Creates an unadjusted version of a MotionEvent. | |
| 541 * @param e The original event. | |
| 542 * @return The unadjusted version of the event. | |
| 543 */ | |
| 544 private MotionEvent createRawMotionEvent(MotionEvent e) { | |
| 545 MotionEvent rawEvent = MotionEvent.obtain(e); | |
| 546 rawEvent.setLocation(e.getRawX(), e.getRawY()); | |
| 547 return rawEvent; | |
| 548 } | |
| 549 | |
| 550 /** | |
| 551 * Updates the bottom sheet's peeking and content height. | |
| 552 */ | |
| 553 private void updateSheetDimensions() { | |
| 554 if (mContainerHeight <= 0) return; | |
| 555 | |
| 556 // Though mStateRatios is a static constant, the peeking ratio is comput
ed here because | |
| 557 // the correct toolbar height and container height are not know until th
ose views are | |
| 558 // inflated. | |
| 559 mStateRatios[0] = mToolbarHeight / mContainerHeight; | |
| 560 | |
| 561 // Compute the height that the content section of the bottom sheet. | |
| 562 float contentHeight = (mContainerHeight * getFullRatio()) - mToolbarHeig
ht; | |
| 563 | |
| 564 MarginLayoutParams sheetContentParams = | |
| 565 (MarginLayoutParams) mBottomSheetContentContainer.getLayoutParam
s(); | |
| 566 sheetContentParams.width = (int) mContainerWidth; | |
| 567 sheetContentParams.height = (int) contentHeight; | |
| 568 sheetContentParams.topMargin = (int) mToolbarHeight; | |
| 569 | |
| 570 MarginLayoutParams toolbarShadowParams = | |
| 571 (MarginLayoutParams) findViewById(R.id.toolbar_shadow).getLayout
Params(); | |
| 572 toolbarShadowParams.topMargin = (int) mToolbarHeight; | |
| 573 | |
| 574 mBottomSheetContentContainer.requestLayout(); | |
| 575 } | |
| 576 | |
| 577 /** | |
| 578 * Cancels and nulls the height animation if it exists. | |
| 579 */ | |
| 580 private void cancelAnimation() { | |
| 581 if (mSettleAnimator == null) return; | |
| 582 mSettleAnimator.cancel(); | |
| 583 mSettleAnimator = null; | |
| 584 } | |
| 585 | |
| 586 /** | |
| 587 * Creates the sheet's animation to a target state. | |
| 588 * @param targetState The target state. | |
| 589 */ | |
| 590 private void createSettleAnimation(@SheetState int targetState) { | |
| 591 mCurrentState = targetState; | |
| 592 mSettleAnimator = ValueAnimator.ofFloat(getSheetOffsetFromBottom(), | |
| 593 getSheetHeightForState(targetState)); | |
| 594 mSettleAnimator.setDuration(BASE_ANIMATION_DURATION_MS); | |
| 595 mSettleAnimator.setInterpolator(mInterpolator); | |
| 596 | |
| 597 // When the animation is canceled or ends, reset the handle to null. | |
| 598 mSettleAnimator.addListener(new AnimatorListenerAdapter() { | |
| 599 @Override | |
| 600 public void onAnimationEnd(Animator animator) { | |
| 601 mSettleAnimator = null; | |
| 602 } | |
| 603 }); | |
| 604 | |
| 605 mSettleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListen
er() { | |
| 606 @Override | |
| 607 public void onAnimationUpdate(ValueAnimator animator) { | |
| 608 setSheetOffsetFromBottom((Float) animator.getAnimatedValue()); | |
| 609 } | |
| 610 }); | |
| 611 | |
| 612 mSettleAnimator.start(); | |
| 613 } | |
| 614 | |
| 615 /** | |
| 616 * Gets the distance of a fling based on the velocity and the base animation
time. This formula | |
| 617 * assumes the deceleration curve is quadratic (t^2), hence the displacement
formula should be: | |
| 618 * displacement = initialVelocity * duration / 2. | |
| 619 * @param velocity The velocity of the fling. | |
| 620 * @return The distance the fling would cover. | |
| 621 */ | |
| 622 private float getFlingDistance(float velocity) { | |
| 623 // This includes conversion from seconds to ms. | |
| 624 return velocity * BASE_ANIMATION_DURATION_MS / 2000f; | |
| 625 } | |
| 626 | |
| 627 /** | |
| 628 * Gets the maximum offset of the bottom sheet. | |
| 629 * @return The max offset. | |
| 630 */ | |
| 631 private float getMaxOffset() { | |
| 632 return getFullRatio() * mContainerHeight; | |
| 633 } | |
| 634 | |
| 635 /** | |
| 636 * Sets the sheet's offset relative to the bottom of the screen. | |
| 637 * @param offset The offset that the sheet should be. | |
| 638 */ | |
| 639 private void setSheetOffsetFromBottom(float offset) { | |
| 640 if (MathUtils.areFloatsEqual(getSheetOffsetFromBottom(), getMinOffset()) | |
| 641 && offset > getMinOffset()) { | |
| 642 onSheetOpened(); | |
| 643 } else if (MathUtils.areFloatsEqual(offset, getMinOffset()) | |
| 644 && getSheetOffsetFromBottom() > getMinOffset()) { | |
| 645 onSheetClosed(); | |
| 646 } | |
| 647 | |
| 648 setTranslationY(mContainerHeight - offset); | |
| 649 sendOffsetChangeEvents(); | |
| 650 } | |
| 651 | |
| 652 /** | |
| 653 * This is the same as {@link #setSheetOffsetFromBottom(float)} but exclusiv
ely for testing. | |
| 654 * @param offset The offset to set the sheet to. | |
| 655 */ | |
| 656 @VisibleForTesting | |
| 657 public void setSheetOffsetFromBottomForTesting(float offset) { | |
| 658 setSheetOffsetFromBottom(offset); | |
| 659 } | |
| 660 | |
| 661 /** | |
| 662 * @return The ratio of the height of the screen that the peeking state is. | |
| 663 */ | |
| 664 @VisibleForTesting | |
| 665 public float getPeekRatio() { | |
| 666 return mStateRatios[0]; | |
| 667 } | |
| 668 | |
| 669 /** | |
| 670 * @return The ratio of the height of the screen that the half expanded stat
e is. | |
| 671 */ | |
| 672 @VisibleForTesting | |
| 673 public float getHalfRatio() { | |
| 674 return mStateRatios[1]; | |
| 675 } | |
| 676 | |
| 677 /** | |
| 678 * @return The ratio of the height of the screen that the fully expanded sta
te is. | |
| 679 */ | |
| 680 @VisibleForTesting | |
| 681 public float getFullRatio() { | |
| 682 return mStateRatios[2]; | |
| 683 } | |
| 684 | |
| 685 /** | |
| 686 * @return The height of the container that the bottom sheet exists in. | |
| 687 */ | |
| 688 @VisibleForTesting | |
| 689 public float getSheetContainerHeight() { | |
| 690 return mContainerHeight; | |
| 691 } | |
| 692 | |
| 693 /** | |
| 694 * Sends notifications if the sheet is transitioning from the peeking to hal
f expanded state and | |
| 695 * from the peeking to fully expanded state. The peek to half events are onl
y sent when the | |
| 696 * sheet is between the peeking and half states. | |
| 697 */ | |
| 698 private void sendOffsetChangeEvents() { | |
| 699 float screenRatio = | |
| 700 mContainerHeight > 0 ? getSheetOffsetFromBottom() / mContainerHe
ight : 0; | |
| 701 | |
| 702 // This ratio is relative to the peek and full positions of the sheet. | |
| 703 float peekFullRatio = MathUtils.clamp( | |
| 704 (screenRatio - getPeekRatio()) / (getFullRatio() - getPeekRatio(
)), 0, 1); | |
| 705 | |
| 706 for (BottomSheetObserver o : mObservers) { | |
| 707 o.onSheetOffsetChanged(MathUtils.areFloatsEqual(peekFullRatio, 0) ?
0 : peekFullRatio); | |
| 708 } | |
| 709 | |
| 710 // This ratio is relative to the peek and half positions of the sheet. | |
| 711 float peekHalfRatio = MathUtils.clamp( | |
| 712 (screenRatio - getPeekRatio()) / (getHalfRatio() - getPeekRatio(
)), 0, 1); | |
| 713 | |
| 714 // If the ratio is close enough to zero, just set it to zero. | |
| 715 if (MathUtils.areFloatsEqual(peekHalfRatio, 0f)) peekHalfRatio = 0f; | |
| 716 | |
| 717 if (mLastPeekToHalfRatioSent < 1f || peekHalfRatio < 1f) { | |
| 718 mLastPeekToHalfRatioSent = peekHalfRatio; | |
| 719 for (BottomSheetObserver o : mObservers) { | |
| 720 o.onTransitionPeekToHalf(peekHalfRatio); | |
| 721 } | |
| 722 } | |
| 723 } | |
| 724 | |
| 725 /** | |
| 726 * Moves the sheet to the provided state. | |
| 727 * @param state The state to move the panel to. | |
| 728 * @param animate If true, the sheet will animate to the provided state, oth
erwise it will | |
| 729 * move there instantly. | |
| 730 */ | |
| 731 public void setSheetState(@SheetState int state, boolean animate) { | |
| 732 mCurrentState = state; | |
| 733 | |
| 734 if (animate) { | |
| 735 createSettleAnimation(state); | |
| 736 } else { | |
| 737 setSheetOffsetFromBottom(getSheetHeightForState(state)); | |
| 738 } | |
| 739 } | |
| 740 | |
| 741 /** | |
| 742 * @return The current state of the bottom sheet. If the sheet is animating,
this will be the | |
| 743 * state the sheet is animating to. | |
| 744 */ | |
| 745 public int getSheetState() { | |
| 746 return mCurrentState; | |
| 747 } | |
| 748 | |
| 749 /** | |
| 750 * If the animation to settle the sheet in one of its states is running. | |
| 751 * @return True if the animation is running. | |
| 752 */ | |
| 753 public boolean isRunningSettleAnimation() { | |
| 754 return mSettleAnimator != null; | |
| 755 } | |
| 756 | |
| 757 @VisibleForTesting | |
| 758 public BottomSheetContent getCurrentSheetContent() { | |
| 759 return mSheetContent; | |
| 760 } | |
| 761 | |
| 762 /** | |
| 763 * Gets the height of the bottom sheet based on a provided state. | |
| 764 * @param state The state to get the height from. | |
| 765 * @return The height of the sheet at the provided state. | |
| 766 */ | |
| 767 private float getSheetHeightForState(@SheetState int state) { | |
| 768 return mStateRatios[state] * mContainerHeight; | |
| 769 } | |
| 770 | |
| 771 /** | |
| 772 * Adds an observer to the bottom sheet. | |
| 773 * @param observer The observer to add. | |
| 774 */ | |
| 775 public void addObserver(BottomSheetObserver observer) { | |
| 776 mObservers.addObserver(observer); | |
| 777 } | |
| 778 | |
| 779 /** | |
| 780 * Gets the target state of the sheet based on the sheet's height and veloci
ty. | |
| 781 * @param sheetHeight The current height of the sheet. | |
| 782 * @param yVelocity The current Y velocity of the sheet. This is only used f
or determining the | |
| 783 * scroll or fling direction. If this value is positive, th
e movement is from | |
| 784 * bottom to top. | |
| 785 * @return The target state of the bottom sheet. | |
| 786 */ | |
| 787 private int getTargetSheetState(float sheetHeight, float yVelocity) { | |
| 788 if (sheetHeight <= getMinOffset()) return SHEET_STATE_PEEK; | |
| 789 if (sheetHeight >= getMaxOffset()) return SHEET_STATE_FULL; | |
| 790 | |
| 791 // First, find the two states that the sheet height is between. | |
| 792 @SheetState | |
| 793 int nextState = sStates[0]; | |
| 794 | |
| 795 @SheetState | |
| 796 int prevState = nextState; | |
| 797 for (int i = 0; i < sStates.length; i++) { | |
| 798 prevState = nextState; | |
| 799 nextState = sStates[i]; | |
| 800 // The values in PanelState are ascending, they should be kept that
way in order for | |
| 801 // this to work. | |
| 802 if (sheetHeight >= getSheetHeightForState(prevState) | |
| 803 && sheetHeight < getSheetHeightForState(nextState)) { | |
| 804 break; | |
| 805 } | |
| 806 } | |
| 807 | |
| 808 // If the desired height is close enough to a certain state, depending o
n the direction of | |
| 809 // the velocity, move to that state. | |
| 810 float lowerBound = getSheetHeightForState(prevState); | |
| 811 float distance = getSheetHeightForState(nextState) - lowerBound; | |
| 812 | |
| 813 float inverseThreshold = 1.0f - THRESHOLD_TO_NEXT_STATE; | |
| 814 float thresholdToNextState = yVelocity < 0.0f ? THRESHOLD_TO_NEXT_STATE
: inverseThreshold; | |
| 815 | |
| 816 if ((sheetHeight - lowerBound) / distance > thresholdToNextState) { | |
| 817 return nextState; | |
| 818 } | |
| 819 return prevState; | |
| 820 } | |
| 821 | |
| 822 @Override | |
| 823 public void onFadingViewClick() { | |
| 824 setSheetState(SHEET_STATE_PEEK, true); | |
| 825 } | |
| 826 | |
| 827 @Override | |
| 828 public void onFadingViewVisibilityChanged(boolean visible) {} | |
| 829 | |
| 830 private boolean canMoveSheet() { | |
| 831 boolean isInOverviewMode = mTabModelSelector != null | |
| 832 && (mTabModelSelector.getCurrentTab() == null | |
| 833 || mTabModelSelector.getCurrentTab().getActivity().is
InOverviewMode()); | |
| 834 return !isToolbarAndroidViewHidden() && !isInOverviewMode; | |
| 835 } | |
| 836 } | |
| OLD | NEW |