Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(678)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/widget/BottomSheet.java

Issue 2625923002: Introduce the bottom sheet class for Chrome Home (Closed)
Patch Set: Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java/src/org/chromium/chrome/browser/widget/BottomSheet.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/widget/BottomSheet.java b/chrome/android/java/src/org/chromium/chrome/browser/widget/BottomSheet.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0b7dc510640ec20f133586be20a6bc677150fdf
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/widget/BottomSheet.java
@@ -0,0 +1,385 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.LinearLayout;
+
+import org.chromium.chrome.R;
+
+/**
+ * This class defines the behavior of a bottom sheet that has multiple states and a persistently
+ * showing toolbar. Namely, the states are:
+ * - PEEK: Only the toolbar is visible at the bottom of the screen.
+ * - HALF: The sheet is expanded to consume around half of the screen.
+ * - FULL: The sheet is expanded to its full height.
+ *
+ * All the computation in this file is based off of the bottom of the screen instead of the top
+ * for simplicity. This means that the bottom of the screen is 0 on the Y axis.
+ */
+public class BottomSheet extends LinearLayout {
+ /** The different states that the bottom sheet can have. */
+ private enum SheetState { PEEK, HALF, FULL }
Ian Wen 2017/01/11 19:46:54 Nit: use int instead? Enums are slow on Android.
mdjones 2017/01/12 21:26:57 Done.
+
+ /** The base duration of the settling animation of the sheet. */
+ private static final long BASE_ANIMATION_DURATION_MS = 218;
+
+ /**
+ * The fraction of the way to the next state the sheet must be swiped to animate there when
+ * released. A smaller value here means a smaller swipe is needed to move the sheet around.
+ */
+ private static final float THRESHOLD_TO_NEXT_STATE = 0.5f;
+
+ /** The minimum y/x ratio that a scroll must have to be considered vertical. */
+ private static final float MIN_VERTICAL_SCROLL_SLOPE = 2.0f;
+
+ /** For detecting scroll and fling events on the bottom sheet. */
+ private GestureDetector mGestureDetector;
+
+ /** Whether or not the user is scrolling the bottom sheet. */
+ private boolean mIsScrolling;
+
+ /** Handle to the views that make up the bottom sheet. */
+ private ViewGroup mSheetContentWrapper;
+
+ /** Track the velocity of the user's scrolls to determine up or down direction. */
+ private VelocityTracker mVelocityTracker;
+
+ /** The animator used to move the sheet to a fixed state when released by the user. */
+ private ObjectAnimator mHeightAnimator;
Ian Wen 2017/01/11 19:46:54 Nit: how about mSettleAnimator? You mentioned belo
mdjones 2017/01/12 21:26:57 Done.
+
+ /** The interpolator that the height animator uses. */
+ private Interpolator mInterpolator;
Ian Wen 2017/01/11 19:46:54 Nit: inline the assignment here.
mdjones 2017/01/12 21:26:57 Done.
+
+ /** The height of the toolbar. */
+ private float mToolbarHeight;
+
+ /** The height of the view that contains the bottom sheet. */
+ private float mContainerHeight;
+
+ /** The current sheet state. If the sheet is moving, this will be the target state. */
+ private SheetState mCurrentState;
+
+ /**
+ * Information about the different scroll states of the sheet. Order is important for these,
+ * they go from smallest to largest.
+ */
+ private float[] mStateRatios;
+
+ public BottomSheet(Context context, AttributeSet atts) {
+ super(context, atts);
+
+ mStateRatios = new float[] {0.0f, 0.55f, 0.95f};
+
+ mVelocityTracker = VelocityTracker.obtain();
+
+ mInterpolator = new DecelerateInterpolator(1.0f);
+
+ mGestureDetector =
+ new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
Ian Wen 2017/01/11 19:46:54 Nit: You might want to move SimpleOnGestureListene
mdjones 2017/01/12 21:26:57 Done.
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ float distanceY) {
+ // Only start scrolling if the scroll is up or down. If the user is already
+ // scrolling, continue moving the sheet.
+ if (!mIsScrolling && (distanceX == 0
+ || Math.abs(distanceY) / Math.abs(distanceX)
Ian Wen 2017/01/11 19:46:54 distanceX might be 0 here. Also I think you need t
mdjones 2017/01/12 21:26:57 Done.
+ < MIN_VERTICAL_SCROLL_SLOPE)) {
+ mVelocityTracker.clear();
+ return false;
+ }
+
+ // Cancel the settling animation if it is running so it doesn't conflict
+ // with where the user wants to move the sheet.
+ cancelAnimation();
+
+ mVelocityTracker.addMovement(e2);
+
+ float currentShownRatio = getSheetOffsetFromBottom() / mContainerHeight;
+
+ // If the sheet is in the max position, don't move if the scroll is upward.
+ if (currentShownRatio >= mStateRatios[mStateRatios.length - 1]
+ && distanceY > 0) {
+ mIsScrolling = false;
+ return false;
+ }
+
+ // Similarly, if the sheet is in the min position, don't move if the scroll
+ // is downward.
+ if (currentShownRatio <= mStateRatios[0] && distanceY < 0) {
+ mIsScrolling = false;
+ return false;
+ }
+
+ setSheetOffsetFromBottom(Math.max(getMinOffset(),
Ian Wen 2017/01/11 19:46:53 Nit: maybe write a clamp function below, instead o
mdjones 2017/01/12 21:26:57 Done. Used MathUtils.clamp(...)
+ Math.min(getMaxOffset(), getSheetOffsetFromBottom() + distanceY)));
+
+ mIsScrolling = true;
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ cancelAnimation();
+
+ // Figure out the projected state of the sheet and animate there.
+ // Note that a swipe up will have a negative velocity, swipe down will have
+ // a positive velocity. Negate this values so that the logic is more
+ // intuitive.
+ SheetState targetState = getTargetSheetState(
+ getSheetOffsetFromBottom() + getFlingDistance(-velocityY),
+ -velocityY);
+ setSheetState(targetState, true);
+
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent e) {
+ // Don't trust incoming motion events for the gesture detector.
Ian Wen 2017/01/11 19:46:54 Hmmm "trust" is ambiguous here. Are you trying to
mdjones 2017/01/12 21:26:57 The incoming event may have been adjusted. I updat
+ MotionEvent rawEvent = MotionEvent.obtain(e);
+ rawEvent.setLocation(e.getRawX(), e.getRawY());
+ mGestureDetector.onTouchEvent(rawEvent);
+
+ // If the user is scrolling and the event is a cancel or up action, update scroll state
+ // and return.
+ if (mIsScrolling && (e.getActionMasked() == MotionEvent.ACTION_UP
+ || e.getActionMasked() == MotionEvent.ACTION_CANCEL)) {
+ mIsScrolling = false;
+
+ mVelocityTracker.computeCurrentVelocity(1000);
+
+ // If an animation was not created to settle the sheet at some state, do it now.
+ if (mHeightAnimator == null) {
+ // Negate velocity so a positive number indicates a swipe up.
+ float currentVelocity = -mVelocityTracker.getYVelocity();
+ SheetState targetState =
+ getTargetSheetState(getSheetOffsetFromBottom(), currentVelocity);
+
+ setSheetState(targetState, true);
+ }
+
+ return true;
+ }
+
+ // Send a cancel event to super if the sheet is scrolling.
+ if (mIsScrolling) {
+ MotionEvent cancel = MotionEvent.obtain(e);
+ cancel.setAction(MotionEvent.ACTION_CANCEL);
Ian Wen 2017/01/11 19:46:54 You only need to send cancel once. IIUC you are se
mdjones 2017/01/12 21:26:57 Removed the need for this with the code added to o
+ mSheetContentWrapper.dispatchTouchEvent(cancel);
+ return true;
+ }
+
+ mSheetContentWrapper.dispatchTouchEvent(e);
+
+ return true;
+ }
+
+ /**
+ * Add layout change listeners to the views that the bottom sheet depends on. Namely the
+ * heights of the root view and control container are important as they are used in many of the
+ * calculations in this class.
+ * @param root The container of the bottom sheet.
+ * @param controlContainer The container for the toolbar.
+ */
+ public void init(View root, View controlContainer) {
+ mSheetContentWrapper = (ViewGroup) findViewById(R.id.bottom_sheet_content_wrapper);
Ian Wen 2017/01/11 19:46:53 Let's unify the name to either wrapper or containe
mdjones 2017/01/12 21:26:58 Removed the need for this.
+ mToolbarHeight = controlContainer.getHeight();
+ mCurrentState = SheetState.PEEK;
+
+ // Listen to height changes on the root.
+ root.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mContainerHeight = bottom - top;
+ updateSheetPeekHeight(mToolbarHeight, mContainerHeight);
+ setSheetState(mCurrentState, false);
+ }
+ });
+
+ // Listen to height changes on the toolbar.
+ controlContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mToolbarHeight = bottom - top;
+ updateSheetPeekHeight(mToolbarHeight, mContainerHeight);
+ setSheetState(mCurrentState, false);
+ }
+ });
+ }
+
+ /**
+ * Update the bottom sheet's peeking height.
+ * @param toolbarHeight The height of the toolbar control container.
+ * @param containerHeight The height of the bottom sheet's container.
+ */
+ private void updateSheetPeekHeight(float toolbarHeight, float containerHeight) {
+ if (containerHeight <= 0) return;
+
+ mStateRatios[0] = toolbarHeight / containerHeight;
+ }
+
+ /**
+ * Cancel and null the height animation if it exists.
+ */
+ private void cancelAnimation() {
+ if (mHeightAnimator == null) return;
+ mHeightAnimator.cancel();
+ mHeightAnimator = null;
+ }
+
+ /**
+ * Create the sheet's animation to a target state.
+ * @param targetState The target state.
+ */
+ private void createYAnimation(SheetState targetState) {
Ian Wen 2017/01/11 19:46:53 Similarly, let's unify the animations' name as wel
mdjones 2017/01/12 21:26:57 Done.
+ mCurrentState = targetState;
+ mHeightAnimator = ObjectAnimator.ofFloat(
+ this, View.TRANSLATION_Y, mContainerHeight - getSheetHeightForState(targetState));
+ mHeightAnimator.setDuration(BASE_ANIMATION_DURATION_MS);
+ mHeightAnimator.setInterpolator(mInterpolator);
+
+ // When the animation is canceled or ends, reset the handle to null.
+ mHeightAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mHeightAnimator = null;
+ }
+ });
+
+ mHeightAnimator.start();
+ }
+
+ /**
+ * Get the distance of a fling based on the velocity and the base animation time. This formula
+ * assumes the deceleration curve is quadratic (t^2), hence the displacement formula should be:
+ * displacement = initialVelocity * duration / 2.
+ * @param velocity The velocity of the fling.
+ * @return The distance the fling would cover.
+ */
+ private float getFlingDistance(float velocity) {
+ // This includes conversion from seconds to ms.
+ return velocity * BASE_ANIMATION_DURATION_MS / 2000f;
+ }
+
+ /**
+ * Get the maximum offset of the bottom sheet.
+ * @return The max offset.
+ */
+ private float getMaxOffset() {
+ return mStateRatios[mStateRatios.length - 1] * mContainerHeight;
+ }
+
+ /**
+ * Get the minimum offset of the bottom sheet.
+ * @return The min offset.
+ */
+ private float getMinOffset() {
+ return mStateRatios[0] * mContainerHeight;
+ }
+
+ /**
+ * Get the sheet's offset from the bottom of the screen.
+ * @return The sheet's distance from the bottom of the screen.
+ */
+ private float getSheetOffsetFromBottom() {
+ return mContainerHeight - getTranslationY();
+ }
+
+ /**
+ * Set the sheet's offset relative to the bottom of the screen.
+ * @param offset The offset that the sheet should be.
+ */
+ private void setSheetOffsetFromBottom(float offset) {
+ setTranslationY(mContainerHeight - offset);
+ }
+
+ /**
+ * Move the sheet to the provided state.
+ * @param state The state to move the panel to.
+ * @param animate If true, the sheet will animate to the provided state, otherwise it will
+ * move there instantly.
+ */
+ private void setSheetState(SheetState state, boolean animate) {
+ mCurrentState = state;
+
+ if (animate) {
+ createYAnimation(state);
+ } else {
+ setSheetOffsetFromBottom(getSheetHeightForState(state));
+ }
+ }
+
+ /**
+ * Get the height of the bottom sheet based on a provided state.
+ * @param state The state to get the height from.
+ * @return The height of the sheet at the provided state.
+ */
+ private float getSheetHeightForState(SheetState state) {
+ return mStateRatios[state.ordinal()] * mContainerHeight;
+ }
+
+ /**
+ * Get the target state of the sheet based on the sheet's height and velocity.
+ * @param sheetHeight The current height of the sheet.
+ * @param yVelocity The current Y velocity of the sheet. This is only used for determining the
Ian Wen 2017/01/11 19:46:53 Please document if yVolocity is positive, the dire
mdjones 2017/01/12 21:26:58 Done.
+ * scroll or fling direction.
+ * @return The target state of the bottom sheet.
+ */
+ private SheetState getTargetSheetState(float sheetHeight, float yVelocity) {
+ if (sheetHeight <= getMinOffset()) return SheetState.PEEK;
+ if (sheetHeight >= getMaxOffset()) return SheetState.FULL;
+
+ // First, find the two states that the sheet height is between.
+ SheetState nextState = SheetState.values()[0];
+ SheetState prevState = nextState;
+ for (SheetState state : SheetState.values()) {
+ prevState = nextState;
+ nextState = state;
+ // The values in PanelState are ascending, they should be kept that way in order for
+ // this to work.
+ if (sheetHeight >= getSheetHeightForState(prevState)
+ && sheetHeight < getSheetHeightForState(nextState)) {
+ break;
+ }
+ }
+
+ // If the desired height is close enough to a certain state, depending on the direction of
+ // the velocity, move to that state.
+ float lowerBound = getSheetHeightForState(prevState);
+ float distance = getSheetHeightForState(nextState) - lowerBound;
+ float thresholdToNextState =
+ yVelocity < 0.0f ? THRESHOLD_TO_NEXT_STATE : 1.0f - THRESHOLD_TO_NEXT_STATE;
+ if ((sheetHeight - lowerBound) / distance > thresholdToNextState) {
+ return nextState;
+ } else {
+ return prevState;
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698