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

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: 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.util.AttributeSet;
12 import android.view.GestureDetector;
13 import android.view.MotionEvent;
14 import android.view.VelocityTracker;
15 import android.view.View;
16 import android.view.ViewGroup;
17 import android.view.animation.DecelerateInterpolator;
18 import android.view.animation.Interpolator;
19 import android.widget.LinearLayout;
20
21 import org.chromium.chrome.R;
22
23 /**
24 * This class defines the behavior of a bottom sheet that has multiple states an d a persistently
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 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.
36
37 /** The base duration of the settling animation of the sheet. */
38 private static final long BASE_ANIMATION_DURATION_MS = 218;
39
40 /**
41 * The fraction of the way to the next state the sheet must be swiped to ani mate there when
42 * released. A smaller value here means a smaller swipe is needed to move th e sheet around.
43 */
44 private static final float THRESHOLD_TO_NEXT_STATE = 0.5f;
45
46 /** The minimum y/x ratio that a scroll must have to be considered vertical. */
47 private static final float MIN_VERTICAL_SCROLL_SLOPE = 2.0f;
48
49 /** For detecting scroll and fling events on the bottom sheet. */
50 private GestureDetector mGestureDetector;
51
52 /** Whether or not the user is scrolling the bottom sheet. */
53 private boolean mIsScrolling;
54
55 /** Handle to the views that make up the bottom sheet. */
56 private ViewGroup mSheetContentWrapper;
57
58 /** Track the velocity of the user's scrolls to determine up or down directi on. */
59 private VelocityTracker mVelocityTracker;
60
61 /** The animator used to move the sheet to a fixed state when released by th e user. */
62 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.
63
64 /** The interpolator that the height animator uses. */
65 private Interpolator mInterpolator;
Ian Wen 2017/01/11 19:46:54 Nit: inline the assignment here.
mdjones 2017/01/12 21:26:57 Done.
66
67 /** The height of the toolbar. */
68 private float mToolbarHeight;
69
70 /** The height of the view that contains the bottom sheet. */
71 private float mContainerHeight;
72
73 /** The current sheet state. If the sheet is moving, this will be the target state. */
74 private SheetState mCurrentState;
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 float[] mStateRatios;
81
82 public BottomSheet(Context context, AttributeSet atts) {
83 super(context, atts);
84
85 mStateRatios = new float[] {0.0f, 0.55f, 0.95f};
86
87 mVelocityTracker = VelocityTracker.obtain();
88
89 mInterpolator = new DecelerateInterpolator(1.0f);
90
91 mGestureDetector =
92 new GestureDetector(context, new GestureDetector.SimpleOnGesture Listener() {
Ian Wen 2017/01/11 19:46:54 Nit: You might want to move SimpleOnGestureListene
mdjones 2017/01/12 21:26:57 Done.
93 @Override
94 public boolean onDown(MotionEvent e) {
95 return true;
96 }
97
98 @Override
99 public boolean onScroll(MotionEvent e1, MotionEvent e2, floa t distanceX,
100 float distanceY) {
101 // Only start scrolling if the scroll is up or down. If the user is already
102 // scrolling, continue moving the sheet.
103 if (!mIsScrolling && (distanceX == 0
104 || Math.abs(distanceY) / Ma th.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.
105 < MIN_VERTICAL_SCRO LL_SLOPE)) {
106 mVelocityTracker.clear();
107 return false;
108 }
109
110 // Cancel the settling animation if it is running so it doesn't conflict
111 // with where the user wants to move the sheet.
112 cancelAnimation();
113
114 mVelocityTracker.addMovement(e2);
115
116 float currentShownRatio = getSheetOffsetFromBottom() / m ContainerHeight;
117
118 // If the sheet is in the max position, don't move if th e scroll is upward.
119 if (currentShownRatio >= mStateRatios[mStateRatios.lengt h - 1]
120 && distanceY > 0) {
121 mIsScrolling = false;
122 return false;
123 }
124
125 // Similarly, if the sheet is in the min position, don't move if the scroll
126 // is downward.
127 if (currentShownRatio <= mStateRatios[0] && distanceY < 0) {
128 mIsScrolling = false;
129 return false;
130 }
131
132 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(...)
133 Math.min(getMaxOffset(), getSheetOffsetFromBotto m() + distanceY)));
134
135 mIsScrolling = true;
136 return true;
137 }
138
139 @Override
140 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
141 float velocityY) {
142 cancelAnimation();
143
144 // Figure out the projected state of the sheet and anima te there.
145 // Note that a swipe up will have a negative velocity, s wipe down will have
146 // a positive velocity. Negate this values so that the l ogic is more
147 // intuitive.
148 SheetState targetState = getTargetSheetState(
149 getSheetOffsetFromBottom() + getFlingDistance(-v elocityY),
150 -velocityY);
151 setSheetState(targetState, true);
152
153 return true;
154 }
155 });
156 }
157
158 @Override
159 public boolean onInterceptTouchEvent(MotionEvent e) {
160 return true;
161 }
162
163 @Override
164 public boolean onTouchEvent(MotionEvent e) {
165 // 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
166 MotionEvent rawEvent = MotionEvent.obtain(e);
167 rawEvent.setLocation(e.getRawX(), e.getRawY());
168 mGestureDetector.onTouchEvent(rawEvent);
169
170 // If the user is scrolling and the event is a cancel or up action, upda te scroll state
171 // and return.
172 if (mIsScrolling && (e.getActionMasked() == MotionEvent.ACTION_UP
173 || e.getActionMasked() == MotionEvent.ACTION _CANCEL)) {
174 mIsScrolling = false;
175
176 mVelocityTracker.computeCurrentVelocity(1000);
177
178 // If an animation was not created to settle the sheet at some state , do it now.
179 if (mHeightAnimator == null) {
180 // Negate velocity so a positive number indicates a swipe up.
181 float currentVelocity = -mVelocityTracker.getYVelocity();
182 SheetState targetState =
183 getTargetSheetState(getSheetOffsetFromBottom(), currentV elocity);
184
185 setSheetState(targetState, true);
186 }
187
188 return true;
189 }
190
191 // Send a cancel event to super if the sheet is scrolling.
192 if (mIsScrolling) {
193 MotionEvent cancel = MotionEvent.obtain(e);
194 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
195 mSheetContentWrapper.dispatchTouchEvent(cancel);
196 return true;
197 }
198
199 mSheetContentWrapper.dispatchTouchEvent(e);
200
201 return true;
202 }
203
204 /**
205 * Add layout change listeners to the views that the bottom sheet depends on . Namely the
206 * heights of the root view and control container are important as they are used in many of the
207 * calculations in this class.
208 * @param root The container of the bottom sheet.
209 * @param controlContainer The container for the toolbar.
210 */
211 public void init(View root, View controlContainer) {
212 mSheetContentWrapper = (ViewGroup) findViewById(R.id.bottom_sheet_conten t_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.
213 mToolbarHeight = controlContainer.getHeight();
214 mCurrentState = SheetState.PEEK;
215
216 // Listen to height changes on the root.
217 root.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
218 public void onLayoutChange(View v, int left, int top, int right, int bottom,
219 int oldLeft, int oldTop, int oldRight, int oldBottom) {
220 mContainerHeight = bottom - top;
221 updateSheetPeekHeight(mToolbarHeight, mContainerHeight);
222 setSheetState(mCurrentState, false);
223 }
224 });
225
226 // Listen to height changes on the toolbar.
227 controlContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListen er() {
228 public void onLayoutChange(View v, int left, int top, int right, int bottom,
229 int oldLeft, int oldTop, int oldRight, int oldBottom) {
230 mToolbarHeight = bottom - top;
231 updateSheetPeekHeight(mToolbarHeight, mContainerHeight);
232 setSheetState(mCurrentState, false);
233 }
234 });
235 }
236
237 /**
238 * Update the bottom sheet's peeking height.
239 * @param toolbarHeight The height of the toolbar control container.
240 * @param containerHeight The height of the bottom sheet's container.
241 */
242 private void updateSheetPeekHeight(float toolbarHeight, float containerHeigh t) {
243 if (containerHeight <= 0) return;
244
245 mStateRatios[0] = toolbarHeight / containerHeight;
246 }
247
248 /**
249 * Cancel and null the height animation if it exists.
250 */
251 private void cancelAnimation() {
252 if (mHeightAnimator == null) return;
253 mHeightAnimator.cancel();
254 mHeightAnimator = null;
255 }
256
257 /**
258 * Create the sheet's animation to a target state.
259 * @param targetState The target state.
260 */
261 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.
262 mCurrentState = targetState;
263 mHeightAnimator = ObjectAnimator.ofFloat(
264 this, View.TRANSLATION_Y, mContainerHeight - getSheetHeightForSt ate(targetState));
265 mHeightAnimator.setDuration(BASE_ANIMATION_DURATION_MS);
266 mHeightAnimator.setInterpolator(mInterpolator);
267
268 // When the animation is canceled or ends, reset the handle to null.
269 mHeightAnimator.addListener(new AnimatorListenerAdapter() {
270 @Override
271 public void onAnimationEnd(Animator animator) {
272 mHeightAnimator = null;
273 }
274 });
275
276 mHeightAnimator.start();
277 }
278
279 /**
280 * Get the distance of a fling based on the velocity and the base animation time. This formula
281 * assumes the deceleration curve is quadratic (t^2), hence the displacement formula should be:
282 * displacement = initialVelocity * duration / 2.
283 * @param velocity The velocity of the fling.
284 * @return The distance the fling would cover.
285 */
286 private float getFlingDistance(float velocity) {
287 // This includes conversion from seconds to ms.
288 return velocity * BASE_ANIMATION_DURATION_MS / 2000f;
289 }
290
291 /**
292 * Get the maximum offset of the bottom sheet.
293 * @return The max offset.
294 */
295 private float getMaxOffset() {
296 return mStateRatios[mStateRatios.length - 1] * mContainerHeight;
297 }
298
299 /**
300 * Get the minimum offset of the bottom sheet.
301 * @return The min offset.
302 */
303 private float getMinOffset() {
304 return mStateRatios[0] * mContainerHeight;
305 }
306
307 /**
308 * Get the sheet's offset from the bottom of the screen.
309 * @return The sheet's distance from the bottom of the screen.
310 */
311 private float getSheetOffsetFromBottom() {
312 return mContainerHeight - getTranslationY();
313 }
314
315 /**
316 * Set the sheet's offset relative to the bottom of the screen.
317 * @param offset The offset that the sheet should be.
318 */
319 private void setSheetOffsetFromBottom(float offset) {
320 setTranslationY(mContainerHeight - offset);
321 }
322
323 /**
324 * Move the sheet to the provided state.
325 * @param state The state to move the panel to.
326 * @param animate If true, the sheet will animate to the provided state, oth erwise it will
327 * move there instantly.
328 */
329 private void setSheetState(SheetState state, boolean animate) {
330 mCurrentState = state;
331
332 if (animate) {
333 createYAnimation(state);
334 } else {
335 setSheetOffsetFromBottom(getSheetHeightForState(state));
336 }
337 }
338
339 /**
340 * Get the height of the bottom sheet based on a provided state.
341 * @param state The state to get the height from.
342 * @return The height of the sheet at the provided state.
343 */
344 private float getSheetHeightForState(SheetState state) {
345 return mStateRatios[state.ordinal()] * mContainerHeight;
346 }
347
348 /**
349 * Get the target state of the sheet based on the sheet's height and velocit y.
350 * @param sheetHeight The current height of the sheet.
351 * @param yVelocity The current Y velocity of the sheet. This is only used f or 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.
352 * scroll or fling direction.
353 * @return The target state of the bottom sheet.
354 */
355 private SheetState getTargetSheetState(float sheetHeight, float yVelocity) {
356 if (sheetHeight <= getMinOffset()) return SheetState.PEEK;
357 if (sheetHeight >= getMaxOffset()) return SheetState.FULL;
358
359 // First, find the two states that the sheet height is between.
360 SheetState nextState = SheetState.values()[0];
361 SheetState prevState = nextState;
362 for (SheetState state : SheetState.values()) {
363 prevState = nextState;
364 nextState = state;
365 // The values in PanelState are ascending, they should be kept that way in order for
366 // this to work.
367 if (sheetHeight >= getSheetHeightForState(prevState)
368 && sheetHeight < getSheetHeightForState(nextState)) {
369 break;
370 }
371 }
372
373 // If the desired height is close enough to a certain state, depending o n the direction of
374 // the velocity, move to that state.
375 float lowerBound = getSheetHeightForState(prevState);
376 float distance = getSheetHeightForState(nextState) - lowerBound;
377 float thresholdToNextState =
378 yVelocity < 0.0f ? THRESHOLD_TO_NEXT_STATE : 1.0f - THRESHOLD_TO _NEXT_STATE;
379 if ((sheetHeight - lowerBound) / distance > thresholdToNextState) {
380 return nextState;
381 } else {
382 return prevState;
383 }
384 }
385 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698