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

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