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 |