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

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: javadoc 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 bottom sheet that has multiple states and a persistent ly showing toolbar.
25 * 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;
gone 2017/01/19 18:00:42 nit: Use @IntDef?
mdjones 2017/01/19 18:52:50 Done.
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;
gone 2017/01/19 18:00:42 This number seems entirely arbitrary. Why not pic
mdjones 2017/01/19 18:52:50 It's the same number used by the overlay panels; i
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} ;
gone 2017/01/19 18:00:42 Why aren't these static? They can't change.
mdjones 2017/01/19 18:52:50 Done.
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
104 // user wants to move the sheet.
105 cancelAnimation();
106
107 mVelocityTracker.addMovement(e2);
108
109 float currentShownRatio =
110 mContainerHeight > 0 ? getSheetOffsetFromBottom() / mContain erHeight : 0;
111
112 // If the sheet is in the max position, don't move if the scroll is upward.
113 if (currentShownRatio >= mStateRatios[mStateRatios.length - 1]
114 && distanceY > 0) {
115 mIsScrolling = false;
116 return false;
117 }
118
119 // Similarly, if the sheet is in the min position, don't move if the scroll is downward.
120 if (currentShownRatio <= mStateRatios[0] && distanceY < 0) {
121 mIsScrolling = false;
122 return false;
123 }
124
125 float newOffset = getSheetOffsetFromBottom() + distanceY;
126 setSheetOffsetFromBottom(MathUtils.clamp(newOffset, getMinOffset(), getMaxOffset()));
127
128 mIsScrolling = true;
129 return true;
130 }
131
132 @Override
133 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
134 float velocityY) {
135 cancelAnimation();
136
137 // Figure out the projected state of the sheet and animate there. No te that a swipe up
138 // will have a negative velocity, swipe down will have a positive ve locity. Negate this
139 // values so that the logic is more intuitive.
140 int targetState = getTargetSheetState(
141 getSheetOffsetFromBottom() + getFlingDistance(-velocityY), - velocityY);
142 setSheetState(targetState, true);
143 mIsScrolling = false;
144
145 return true;
146 }
147 }
148
149 /**
150 * Constructor for inflation from XML.
151 * @param context An Android context.
152 * @param atts The XML attributes.
153 */
154 public BottomSheet(Context context, AttributeSet atts) {
155 super(context, atts);
156
157 mVelocityTracker = VelocityTracker.obtain();
158
159 mGestureDetector = new GestureDetector(context, new BottomSheetSwipeDete ctor());
160 mGestureDetector.setIsLongpressEnabled(false);
161 }
162
163 @Override
164 public boolean onInterceptTouchEvent(MotionEvent e) {
165 // The incoming motion event may have been adjusted by the view sending it down. Create a
166 // motion event with the raw (x, y) coordinates of the original so the g esture detector
167 // functions properly.
168 mGestureDetector.onTouchEvent(createRawMotionEvent(e));
169 return mIsScrolling;
170 }
171
172 @Override
173 public boolean onTouchEvent(MotionEvent e) {
174 // The down event is interpreted above in onInterceptTouchEvent, it does not need to be
175 // interpreted a second time.
176 if (e.getActionMasked() != MotionEvent.ACTION_DOWN) {
177 mGestureDetector.onTouchEvent(createRawMotionEvent(e));
178 }
179
180 // If the user is scrolling and the event is a cancel or up action, upda te scroll state
181 // and return.
182 if (e.getActionMasked() == MotionEvent.ACTION_UP
183 || e.getActionMasked() == MotionEvent.ACTION_CANCEL) {
184 mIsScrolling = false;
185
186 mVelocityTracker.computeCurrentVelocity(1000);
187
188 // If an animation was not created to settle the sheet at some state , do it now.
189 if (mSettleAnimator == null) {
190 // Negate velocity so a positive number indicates a swipe up.
191 float currentVelocity = -mVelocityTracker.getYVelocity();
192 int targetState = getTargetSheetState(getSheetOffsetFromBottom() , currentVelocity);
193
194 setSheetState(targetState, true);
195 }
196 }
197
198 return true;
199 }
200
201 @Override
202 public boolean gatherTransparentRegion(Region region) {
203 // TODO(mdjones): Figure out what this should actually be set to since t he view animates
204 // without necessarily calling this method again.
205 region.setEmpty();
206 return true;
207 }
208
209 /**
210 * Adds layout change listeners to the views that the bottom sheet depends o n. Namely the
211 * heights of the root view and control container are important as they are used in many of the
212 * calculations in this class.
213 * @param root The container of the bottom sheet.
214 * @param controlContainer The container for the toolbar.
215 */
216 public void init(View root, View controlContainer) {
217 mToolbarHeight = controlContainer.getHeight();
218 mCurrentState = STATE_PEEK;
219
220 // Listen to height changes on the root.
221 root.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
222 public void onLayoutChange(View v, int left, int top, int right, int bottom,
223 int oldLeft, int oldTop, int oldRight, int oldBottom) {
224 mContainerHeight = bottom - top;
225 updateSheetPeekHeight(mToolbarHeight, mContainerHeight);
226 setSheetState(mCurrentState, false);
227 }
228 });
229
230 // Listen to height changes on the toolbar.
231 controlContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListen er() {
232 public void onLayoutChange(View v, int left, int top, int right, int bottom,
233 int oldLeft, int oldTop, int oldRight, int oldBottom) {
234 mToolbarHeight = bottom - top;
235 updateSheetPeekHeight(mToolbarHeight, mContainerHeight);
236 setSheetState(mCurrentState, false);
237 }
238 });
239 }
240
241 /**
242 * Creates an unadjusted version of a MotionEvent.
243 * @param e The original event.
244 * @return The unadjusted version of the event.
245 */
246 private MotionEvent createRawMotionEvent(MotionEvent e) {
247 MotionEvent rawEvent = MotionEvent.obtain(e);
248 rawEvent.setLocation(e.getRawX(), e.getRawY());
249 return rawEvent;
250 }
251
252 /**
253 * Updates the bottom sheet's peeking height.
254 * @param toolbarHeight The height of the toolbar control container.
255 * @param containerHeight The height of the bottom sheet's container.
256 */
257 private void updateSheetPeekHeight(float toolbarHeight, float containerHeigh t) {
gone 2017/01/19 18:00:42 You always pass in member fields when calling this
mdjones 2017/01/19 18:52:50 Done.
258 if (containerHeight <= 0) return;
259
260 mStateRatios[0] = toolbarHeight / containerHeight;
261 }
262
263 /**
264 * Cancels and nulls the height animation if it exists.
265 */
266 private void cancelAnimation() {
267 if (mSettleAnimator == null) return;
268 mSettleAnimator.cancel();
269 mSettleAnimator = null;
270 }
271
272 /**
273 * Creates the sheet's animation to a target state.
274 * @param targetState The target state.
275 */
276 private void createSettleAnimation(int targetState) {
277 mCurrentState = targetState;
278 mSettleAnimator = ObjectAnimator.ofFloat(
279 this, View.TRANSLATION_Y, mContainerHeight - getSheetHeightForSt ate(targetState));
280 mSettleAnimator.setDuration(BASE_ANIMATION_DURATION_MS);
281 mSettleAnimator.setInterpolator(mInterpolator);
282
283 // When the animation is canceled or ends, reset the handle to null.
284 mSettleAnimator.addListener(new AnimatorListenerAdapter() {
285 @Override
286 public void onAnimationEnd(Animator animator) {
287 mSettleAnimator = null;
288 }
289 });
290
291 mSettleAnimator.start();
292 }
293
294 /**
295 * Gets the distance of a fling based on the velocity and the base animation time. This formula
296 * assumes the deceleration curve is quadratic (t^2), hence the displacement formula should be:
297 * displacement = initialVelocity * duration / 2.
298 * @param velocity The velocity of the fling.
299 * @return The distance the fling would cover.
300 */
301 private float getFlingDistance(float velocity) {
302 // This includes conversion from seconds to ms.
303 return velocity * BASE_ANIMATION_DURATION_MS / 2000f;
304 }
305
306 /**
307 * Gets the maximum offset of the bottom sheet.
308 * @return The max offset.
309 */
310 private float getMaxOffset() {
311 return mStateRatios[mStateRatios.length - 1] * mContainerHeight;
312 }
313
314 /**
315 * Gets the minimum offset of the bottom sheet.
316 * @return The min offset.
317 */
318 private float getMinOffset() {
319 return mStateRatios[0] * mContainerHeight;
320 }
321
322 /**
323 * Gets the sheet's offset from the bottom of the screen.
324 * @return The sheet's distance from the bottom of the screen.
325 */
326 private float getSheetOffsetFromBottom() {
327 return mContainerHeight - getTranslationY();
328 }
329
330 /**
331 * Sets the sheet's offset relative to the bottom of the screen.
332 * @param offset The offset that the sheet should be.
333 */
334 private void setSheetOffsetFromBottom(float offset) {
335 setTranslationY(mContainerHeight - offset);
336 }
337
338 /**
339 * Moves the sheet to the provided state.
340 * @param state The state to move the panel to.
341 * @param animate If true, the sheet will animate to the provided state, oth erwise it will
342 * move there instantly.
343 */
344 private void setSheetState(int state, boolean animate) {
345 mCurrentState = state;
346
347 if (animate) {
348 createSettleAnimation(state);
349 } else {
350 setSheetOffsetFromBottom(getSheetHeightForState(state));
351 }
352 }
353
354 /**
355 * Gets the height of the bottom sheet based on a provided state.
356 * @param state The state to get the height from.
357 * @return The height of the sheet at the provided state.
358 */
359 private float getSheetHeightForState(int state) {
360 return mStateRatios[state] * mContainerHeight;
361 }
362
363 /**
364 * Gets the target state of the sheet based on the sheet's height and veloci ty.
365 * @param sheetHeight The current height of the sheet.
366 * @param yVelocity The current Y velocity of the sheet. This is only used f or determining the
367 * scroll or fling direction. If this value is positive, th e movement is from
368 * bottom to top.
369 * @return The target state of the bottom sheet.
370 */
371 private int getTargetSheetState(float sheetHeight, float yVelocity) {
372 if (sheetHeight <= getMinOffset()) return STATE_PEEK;
373 if (sheetHeight >= getMaxOffset()) return STATE_FULL;
374
375 // First, find the two states that the sheet height is between.
376 int nextState = mStates[0];
377 int prevState = nextState;
378 for (int i = 0; i < mStates.length; i++) {
379 prevState = nextState;
380 nextState = mStates[i];
381 // The values in PanelState are ascending, they should be kept that way in order for
382 // this to work.
383 if (sheetHeight >= getSheetHeightForState(prevState)
384 && sheetHeight < getSheetHeightForState(nextState)) {
385 break;
386 }
387 }
388
389 // If the desired height is close enough to a certain state, depending o n the direction of
390 // the velocity, move to that state.
391 float lowerBound = getSheetHeightForState(prevState);
392 float distance = getSheetHeightForState(nextState) - lowerBound;
393 float thresholdToNextState =
394 yVelocity < 0.0f ? THRESHOLD_TO_NEXT_STATE : 1.0f - THRESHOLD_TO _NEXT_STATE;
395 if ((sheetHeight - lowerBound) / distance > thresholdToNextState) {
396 return nextState;
397 } else {
398 return prevState;
399 }
400 }
401 }
OLDNEW
« no previous file with comments | « chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java ('k') | chrome/android/java_sources.gni » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698