summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/ui/mediapicker/MediaPickerPanel.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/messaging/ui/mediapicker/MediaPickerPanel.java')
-rw-r--r--src/com/android/messaging/ui/mediapicker/MediaPickerPanel.java563
1 files changed, 563 insertions, 0 deletions
diff --git a/src/com/android/messaging/ui/mediapicker/MediaPickerPanel.java b/src/com/android/messaging/ui/mediapicker/MediaPickerPanel.java
new file mode 100644
index 0000000..56b0a03
--- /dev/null
+++ b/src/com/android/messaging/ui/mediapicker/MediaPickerPanel.java
@@ -0,0 +1,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();
+ }
+ }
+}
+