1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
|
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.mediapicker;
import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.LinearLayout;
import com.android.messaging.R;
import com.android.messaging.ui.PagingAwareViewPager;
import com.android.messaging.util.Assert;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.UiUtils;
/**
* Custom layout panel which makes the MediaPicker animations seamless and synchronized
* Designed to be very specific to the MediaPicker's usage
*/
public class MediaPickerPanel extends ViewGroup {
/**
* The window of time in which we might to decide to reinterpret the intent of a gesture.
*/
private static final long TOUCH_RECAPTURE_WINDOW_MS = 500L;
// The two view components to layout
private LinearLayout mTabStrip;
private boolean mFullScreenOnly;
private PagingAwareViewPager mViewPager;
/**
* True if the MediaPicker is full screen or animating into it
*/
private boolean mFullScreen;
/**
* True if the MediaPicker is open at all
*/
private boolean mExpanded;
/**
* The current desired height of the MediaPicker. This property may be animated and the
* measure pass uses it to determine what size the components are.
*/
private int mCurrentDesiredHeight;
private final Handler mHandler = new Handler();
/**
* The media picker for dispatching events to the MediaPicker's listener
*/
private MediaPicker mMediaPicker;
/**
* The computed default "half-screen" height of the view pager in px
*/
private final int mDefaultViewPagerHeight;
/**
* The action bar height used to compute the padding on the view pager when it's full screen.
*/
private final int mActionBarHeight;
private TouchHandler mTouchHandler;
static final int PAGE_NOT_SET = -1;
public MediaPickerPanel(final Context context, final AttributeSet attrs) {
super(context, attrs);
// Cache the computed dimension
mDefaultViewPagerHeight = getResources().getDimensionPixelSize(
R.dimen.mediapicker_default_chooser_height);
mActionBarHeight = getResources().getDimensionPixelSize(R.dimen.action_bar_height);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTabStrip = (LinearLayout) findViewById(R.id.mediapicker_tabstrip);
mViewPager = (PagingAwareViewPager) findViewById(R.id.mediapicker_view_pager);
mTouchHandler = new TouchHandler();
setOnTouchListener(mTouchHandler);
mViewPager.setOnTouchListener(mTouchHandler);
// Make sure full screen mode is updated in landscape mode change when the panel is open.
addOnLayoutChangeListener(new OnLayoutChangeListener() {
private boolean mLandscapeMode = UiUtils.isLandscapeMode();
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
final boolean newLandscapeMode = UiUtils.isLandscapeMode();
if (mLandscapeMode != newLandscapeMode) {
mLandscapeMode = newLandscapeMode;
if (mExpanded) {
setExpanded(mExpanded, false /* animate */, mViewPager.getCurrentItem(),
true /* force */);
}
}
}
});
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
int requestedHeight = MeasureSpec.getSize(heightMeasureSpec);
if (mMediaPicker.getChooserShowsActionBarInFullScreen()) {
requestedHeight -= mActionBarHeight;
}
int desiredHeight = Math.min(mCurrentDesiredHeight, requestedHeight);
if (mExpanded && desiredHeight == 0) {
// If we want to be shown, we have to have a non-0 height. Returning a height of 0 will
// cause the framework to abort the animation from 0, so we must always have some
// height once we start expanding
desiredHeight = 1;
} else if (!mExpanded && desiredHeight == 0) {
mViewPager.setVisibility(View.GONE);
mViewPager.setAdapter(null);
}
measureChild(mTabStrip, widthMeasureSpec, heightMeasureSpec);
int tabStripHeight;
if (requiresFullScreen()) {
// Ensure that the tab strip is always visible, even in full screen.
tabStripHeight = mTabStrip.getMeasuredHeight();
} else {
// Slide out the tab strip at the end of the animation to full screen.
tabStripHeight = Math.min(mTabStrip.getMeasuredHeight(),
requestedHeight - desiredHeight);
}
// If we are animating and have an interim desired height, use the default height. We can't
// take the max here as on some devices the mDefaultViewPagerHeight may be too big in
// landscape mode after animation.
final int tabAdjustedDesiredHeight = desiredHeight - tabStripHeight;
final int viewPagerHeight =
tabAdjustedDesiredHeight <= 1 ? mDefaultViewPagerHeight : tabAdjustedDesiredHeight;
int viewPagerHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
viewPagerHeight, MeasureSpec.EXACTLY);
measureChild(mViewPager, widthMeasureSpec, viewPagerHeightMeasureSpec);
setMeasuredDimension(mViewPager.getMeasuredWidth(), desiredHeight);
}
@Override
protected void onLayout(final boolean changed, final int left, final int top, final int right,
final int bottom) {
int y = top;
final int width = right - left;
final int viewPagerHeight = mViewPager.getMeasuredHeight();
mViewPager.layout(0, y, width, y + viewPagerHeight);
y += viewPagerHeight;
mTabStrip.layout(0, y, width, y + mTabStrip.getMeasuredHeight());
}
void onChooserChanged() {
if (mFullScreen) {
setDesiredHeight(getDesiredHeight(), true);
}
}
void setFullScreenOnly(boolean fullScreenOnly) {
mFullScreenOnly = fullScreenOnly;
}
boolean isFullScreen() {
return mFullScreen;
}
void setMediaPicker(final MediaPicker mediaPicker) {
mMediaPicker = mediaPicker;
}
/**
* Get the desired height of the media picker panel for when the panel is not in motion (i.e.
* not being dragged by the user).
*/
private int getDesiredHeight() {
if (mFullScreen) {
int fullHeight = getContext().getResources().getDisplayMetrics().heightPixels;
if (OsUtil.isAtLeastKLP() && isAttachedToWindow()) {
// When we're attached to the window, we can get an accurate height, not necessary
// on older API level devices because they don't include the action bar height
View composeContainer =
getRootView().findViewById(R.id.conversation_and_compose_container);
if (composeContainer != null) {
// protect against composeContainer having been unloaded already
fullHeight -= UiUtils.getMeasuredBoundsOnScreen(composeContainer).top;
}
}
if (mMediaPicker.getChooserShowsActionBarInFullScreen()) {
return fullHeight - mActionBarHeight;
} else {
return fullHeight;
}
} else if (mExpanded) {
return LayoutParams.WRAP_CONTENT;
} else {
return 0;
}
}
private void setupViewPager(final int startingPage) {
mViewPager.setVisibility(View.VISIBLE);
if (startingPage >= 0 && startingPage < mMediaPicker.getPagerAdapter().getCount()) {
mViewPager.setAdapter(mMediaPicker.getPagerAdapter());
mViewPager.setCurrentItem(startingPage);
}
updateViewPager();
}
/**
* Expand the media picker panel. Since we always set the pager adapter to null when the panel
* is collapsed, we need to restore the adapter and the starting page.
* @param expanded expanded or collapsed
* @param animate need animation
* @param startingPage the desired selected page to start
*/
void setExpanded(final boolean expanded, final boolean animate, final int startingPage) {
setExpanded(expanded, animate, startingPage, false /* force */);
}
private void setExpanded(final boolean expanded, final boolean animate, final int startingPage,
final boolean force) {
if (expanded == mExpanded && !force) {
return;
}
mFullScreen = false;
mExpanded = expanded;
mHandler.post(new Runnable() {
@Override
public void run() {
setDesiredHeight(getDesiredHeight(), animate);
}
});
if (expanded) {
setupViewPager(startingPage);
mMediaPicker.dispatchOpened();
} else {
mMediaPicker.dispatchDismissed();
}
// Call setFullScreenView() when we are in landscape mode so it can go full screen as
// soon as it is expanded.
if (expanded && requiresFullScreen()) {
setFullScreenView(true, animate);
}
}
private boolean requiresFullScreen() {
return mFullScreenOnly || UiUtils.isLandscapeMode();
}
private void setDesiredHeight(int height, final boolean animate) {
final int startHeight = mCurrentDesiredHeight;
if (height == LayoutParams.WRAP_CONTENT) {
height = measureHeight();
}
clearAnimation();
if (animate) {
final int deltaHeight = height - startHeight;
final Animation animation = new Animation() {
@Override
protected void applyTransformation(final float interpolatedTime,
final Transformation t) {
mCurrentDesiredHeight = (int) (startHeight + deltaHeight * interpolatedTime);
requestLayout();
}
@Override
public boolean willChangeBounds() {
return true;
}
};
animation.setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION);
animation.setInterpolator(UiUtils.EASE_OUT_INTERPOLATOR);
startAnimation(animation);
} else {
mCurrentDesiredHeight = height;
}
requestLayout();
}
/**
* @return The minimum total height of the view
*/
private int measureHeight() {
final int measureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST);
measureChild(mTabStrip, measureSpec, measureSpec);
return mDefaultViewPagerHeight + mTabStrip.getMeasuredHeight();
}
/**
* Enters or leaves full screen view
*
* @param fullScreen True to enter full screen view, false to leave
* @param animate True to animate the transition
*/
void setFullScreenView(final boolean fullScreen, final boolean animate) {
if (fullScreen == mFullScreen) {
return;
}
if (requiresFullScreen() && !fullScreen) {
setExpanded(false /* expanded */, true /* animate */, PAGE_NOT_SET);
return;
}
mFullScreen = fullScreen;
setDesiredHeight(getDesiredHeight(), animate);
mMediaPicker.dispatchFullScreen(mFullScreen);
updateViewPager();
}
/**
* ViewPager should have its paging disabled when in full screen mode.
*/
private void updateViewPager() {
mViewPager.setPagingEnabled(!mFullScreen);
}
@Override
public boolean onInterceptTouchEvent(final MotionEvent ev) {
return mTouchHandler.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev);
}
/**
* Helper class to handle touch events and swipe gestures
*/
private class TouchHandler implements OnTouchListener {
/**
* The height of the view when the touch press started
*/
private int mDownHeight = -1;
/**
* True if the panel moved at all (changed height) during the drag
*/
private boolean mMoved = false;
// The threshold constants converted from DP to px
private final float mFlingThresholdPx;
private final float mBigFlingThresholdPx;
// The system defined pixel size to determine when a movement is considered a drag.
private final int mTouchSlop;
/**
* A copy of the MotionEvent that started the drag/swipe gesture
*/
private MotionEvent mDownEvent;
/**
* Whether we are currently moving down. We may not be able to move down in full screen
* mode when the child view can swipe down (such as a list view).
*/
private boolean mMovedDown = false;
/**
* Indicates whether the child view contained in the panel can swipe down at the beginning
* of the drag event (i.e. the initial down). The MediaPanel can contain
* scrollable children such as a list view / grid view. If the child view can swipe down,
* We want to let the child view handle the scroll first instead of handling it ourselves.
*/
private boolean mCanChildViewSwipeDown = false;
/**
* Necessary direction ratio for a fling to be considered in one direction this prevents
* horizontal swipes with small vertical components from triggering vertical swipe actions
*/
private static final float DIRECTION_RATIO = 1.1f;
TouchHandler() {
final Resources resources = getContext().getResources();
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mFlingThresholdPx = resources.getDimensionPixelSize(
R.dimen.mediapicker_fling_threshold);
mBigFlingThresholdPx = resources.getDimensionPixelSize(
R.dimen.mediapicker_big_fling_threshold);
mTouchSlop = configuration.getScaledTouchSlop();
}
/**
* The media picker panel may contain scrollable children such as a GridView, which eats
* all touch events before we get to it. Therefore, we'd like to intercept these events
* before the children to determine if we should handle swiping down in full screen mode.
* In non-full screen mode, we should handle all vertical scrolling events and leave
* horizontal scrolling to the view pager.
*/
public boolean onInterceptTouchEvent(final MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// Never capture the initial down, so that the children may handle it
// as well. Let the touch handler know about the down event as well.
mTouchHandler.onTouch(MediaPickerPanel.this, ev);
// Ask the MediaPicker whether the contained view can be swiped down.
// We record the value at the start of the drag to decide the swiping mode
// for the entire motion.
mCanChildViewSwipeDown = mMediaPicker.canSwipeDownChooser();
return false;
case MotionEvent.ACTION_MOVE: {
if (mMediaPicker.isChooserHandlingTouch()) {
if (shouldAllowRecaptureTouch(ev)) {
mMediaPicker.stopChooserTouchHandling();
mViewPager.setPagingEnabled(true);
return false;
}
// If the chooser is claiming ownership on all touch events, then we
// shouldn't try to handle them (neither should the view pager).
mViewPager.setPagingEnabled(false);
return false;
} else if (mCanChildViewSwipeDown) {
// Never capture event if the child view can swipe down.
return false;
} else if (!mFullScreen && mMoved) {
// When we are not fullscreen, we own any vertical drag motion.
return true;
} else if (mMovedDown) {
// We are currently handling the down swipe ourselves, so always
// capture this event.
return true;
} else {
// The current interaction mode is undetermined, so always let the
// touch handler know about this event. However, don't capture this
// event so the child may handle it as well.
mTouchHandler.onTouch(MediaPickerPanel.this, ev);
// Capture the touch event from now on if we are handling the drag.
return mFullScreen ? mMovedDown : mMoved;
}
}
}
return false;
}
/**
* Determine whether we think the user is actually trying to expand or slide despite the
* fact that they touched first on a chooser that captured the input.
*/
private boolean shouldAllowRecaptureTouch(MotionEvent ev) {
final long elapsedMs = ev.getEventTime() - ev.getDownTime();
if (mDownEvent == null || elapsedMs == 0 || elapsedMs > TOUCH_RECAPTURE_WINDOW_MS) {
// Either we don't have info to decide or it's been long enough that we no longer
// want to reinterpret user intent.
return false;
}
final float dx = ev.getRawX() - mDownEvent.getRawX();
final float dy = ev.getRawY() - mDownEvent.getRawY();
final float dt = elapsedMs / 1000.0f;
final float maxAbsDelta = Math.max(Math.abs(dx), Math.abs(dy));
final float velocity = maxAbsDelta / dt;
return velocity > mFlingThresholdPx;
}
@Override
public boolean onTouch(final View view, final MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_UP: {
if (!mMoved || mDownEvent == null) {
return false;
}
final float dx = motionEvent.getRawX() - mDownEvent.getRawX();
final float dy = motionEvent.getRawY() - mDownEvent.getRawY();
final float dt =
(motionEvent.getEventTime() - mDownEvent.getEventTime()) / 1000.0f;
final float yVelocity = dy / dt;
boolean handled = false;
// Vertical swipe occurred if the direction is as least mostly in the y
// component and has the required velocity (px/sec)
if ((dx == 0 || (Math.abs(dy) / Math.abs(dx)) > DIRECTION_RATIO) &&
Math.abs(yVelocity) > mFlingThresholdPx) {
if (yVelocity < 0 && mExpanded) {
setFullScreenView(true, true);
handled = true;
} else if (yVelocity > 0) {
if (mFullScreen && yVelocity < mBigFlingThresholdPx) {
setFullScreenView(false, true);
} else {
setExpanded(false, true, PAGE_NOT_SET);
}
handled = true;
}
}
if (!handled) {
// If they didn't swipe enough, animate back to resting state
setDesiredHeight(getDesiredHeight(), true);
}
resetState();
break;
}
case MotionEvent.ACTION_DOWN: {
mDownHeight = getHeight();
mDownEvent = MotionEvent.obtain(motionEvent);
// If we are here and care about the return value (i.e. this is not called
// from onInterceptTouchEvent), then presumably no children view in the panel
// handles the down event. We'd like to handle future ACTION_MOVE events, so
// always claim ownership on this event so it doesn't fall through and gets
// cancelled by the framework.
return true;
}
case MotionEvent.ACTION_MOVE: {
if (mDownEvent == null) {
return mMoved;
}
final float dx = mDownEvent.getRawX() - motionEvent.getRawX();
final float dy = mDownEvent.getRawY() - motionEvent.getRawY();
// Don't act if the move is mostly horizontal
if (Math.abs(dy) > mTouchSlop &&
(Math.abs(dy) / Math.abs(dx)) > DIRECTION_RATIO) {
setDesiredHeight((int) (mDownHeight + dy), false);
mMoved = true;
if (dy < -mTouchSlop) {
mMovedDown = true;
}
}
return mMoved;
}
}
return mMoved;
}
private void resetState() {
mDownEvent = null;
mDownHeight = -1;
mMoved = false;
mMovedDown = false;
mCanChildViewSwipeDown = false;
updateViewPager();
}
}
}
|