OLD | NEW |
---|---|
(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 } | |
OLD | NEW |