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