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

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

Powered by Google App Engine
This is Rietveld 408576698