OSDN Git Service

Adding experiment for minimized pinned stack.
authorWinson Chung <winsonc@google.com>
Tue, 8 Nov 2016 23:45:10 +0000 (15:45 -0800)
committerWinson Chung <winsonc@google.com>
Thu, 10 Nov 2016 23:09:17 +0000 (23:09 +0000)
- Also refactoring the PIP touch handling to be independent gestures

Test: Enable the setting in SystemUI tuner, then drag the PIP slightly
      offscreen. This is only experimental behaviour, and
      android.server.cts.ActivityManagerPinnedStackTests will be updated
      accordingly if we keep this behavior.

Change-Id: I5834971fcbbb127526339e764e7d76b5d22d4707

core/java/android/view/IPinnedStackController.aidl
core/java/com/android/internal/policy/PipSnapAlgorithm.java
packages/SystemUI/res/values/strings.xml
packages/SystemUI/res/xml/tuner_prefs.xml
packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchGesture.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java [new file with mode: 0644]
services/core/java/com/android/server/wm/PinnedStackController.java

index a81eef8..d59be02 100644 (file)
@@ -32,6 +32,11 @@ interface IPinnedStackController {
     oneway void setInInteractiveMode(boolean inInteractiveMode);
 
     /**
+     * Notifies the controller that the PIP is currently minimized.
+     */
+    oneway void setIsMinimized(boolean isMinimized);
+
+    /**
      * Notifies the controller that the desired snap mode is to the closest edge.
      */
     oneway void setSnapToEdge(boolean snapToEdge);
index cbacf26..1e2a53b 100644 (file)
@@ -208,15 +208,19 @@ public class PipSnapAlgorithm {
         final int fromTop = Math.abs(stackBounds.top - movementBounds.top);
         final int fromRight = Math.abs(movementBounds.right - stackBounds.left);
         final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top);
+        final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right,
+                stackBounds.left));
+        final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom,
+                stackBounds.top));
         boundsOut.set(stackBounds);
         if (fromLeft <= fromTop && fromLeft <= fromRight && fromLeft <= fromBottom) {
-            boundsOut.offsetTo(movementBounds.left, stackBounds.top);
+            boundsOut.offsetTo(movementBounds.left, boundedTop);
         } else if (fromTop <= fromLeft && fromTop <= fromRight && fromTop <= fromBottom) {
-            boundsOut.offsetTo(stackBounds.left, movementBounds.top);
+            boundsOut.offsetTo(boundedLeft, movementBounds.top);
         } else if (fromRight < fromLeft && fromRight < fromTop && fromRight < fromBottom) {
-            boundsOut.offsetTo(movementBounds.right, stackBounds.top);
+            boundsOut.offsetTo(movementBounds.right, boundedTop);
         } else {
-            boundsOut.offsetTo(stackBounds.left, movementBounds.bottom);
+            boundsOut.offsetTo(boundedLeft, movementBounds.bottom);
         }
     }
 
index 37a7e38..b1d81ca 100644 (file)
         not appear on production builds ever. -->
     <string name="pip_drag_to_dismiss_summary" translatable="false">Drag to the dismiss target at the bottom of the screen to close the PIP</string>
 
-    <!-- PIP tap once to break through to the activity. Non-translatable since it should
+    <!-- PIP tap once to break through to the activity title. Non-translatable since it should
         not appear on production builds ever. -->
     <string name="pip_tap_through_title" translatable="false">Tap to interact</string>
 
-    <!-- PIP tap once to break through to the activity. Non-translatable since it should
+    <!-- PIP tap once to break through to the activity description. Non-translatable since it should
         not appear on production builds ever. -->
     <string name="pip_tap_through_summary" translatable="false">Tap once to interact with the activity</string>
 
-    <!-- PIP snap to closest edge. Non-translatable since it should
+    <!-- PIP snap to closest edge title. Non-translatable since it should
         not appear on production builds ever. -->
     <string name="pip_snap_mode_edge_title" translatable="false">Snap to closest edge</string>
 
-    <!-- PIP snap to closest edge. Non-translatable since it should
+    <!-- PIP snap to closest edge description. Non-translatable since it should
         not appear on production builds ever. -->
     <string name="pip_snap_mode_edge_summary" translatable="false">Snap to the closest edge</string>
 
+    <!-- PIP allow minimize title. Non-translatable since it should
+        not appear on production builds ever. -->
+    <string name="pip_allow_minimize_title" translatable="false">Allow PIP to minimize</string>
+
+    <!-- PIP allow minimize description. Non-translatable since it should
+        not appear on production builds ever. -->
+    <string name="pip_allow_minimize_summary" translatable="false">Allow PIP to minimize slightly offscreen</string>
+
 </resources>
index f09d6e9..74d5d6c 100644 (file)
             android:summary="@string/pip_snap_mode_edge_summary"
             sysui:defValue="false" />
 
+        <com.android.systemui.tuner.TunerSwitch
+            android:key="pip_allow_minimize"
+            android:title="@string/pip_allow_minimize_title"
+            android:summary="@string/pip_allow_minimize_summary"
+            sysui:defValue="false" />
+
     </PreferenceScreen>
 
     <PreferenceScreen
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchGesture.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchGesture.java
new file mode 100644 (file)
index 0000000..e8e8a4d
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 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.systemui.pip.phone;
+
+/**
+ * A generic interface for a touch gesture.
+ */
+public abstract class PipTouchGesture {
+
+    /**
+     * Handle the touch down.
+     */
+    void onDown(PipTouchState touchState) {}
+
+    /**
+     * Handle the touch move, and return whether the event was consumed.
+     */
+    boolean onMove(PipTouchState touchState) {
+        return false;
+    }
+
+    /**
+     * Handle the touch up, and return whether the gesture was consumed.
+     */
+    boolean onUp(PipTouchState touchState) {
+        return false;
+    }
+}
index a359380..b24d199 100644 (file)
@@ -22,6 +22,7 @@ import static android.view.WindowManager.INPUT_CONSUMER_PIP;
 
 import static com.android.systemui.Interpolators.FAST_OUT_LINEAR_IN;
 import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.systemui.Interpolators.LINEAR_OUT_SLOW_IN;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -30,9 +31,9 @@ import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.app.ActivityManager.StackInfo;
 import android.app.IActivityManager;
 import android.content.Context;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.util.Log;
@@ -43,7 +44,6 @@ import android.view.InputChannel;
 import android.view.InputEvent;
 import android.view.InputEventReceiver;
 import android.view.MotionEvent;
-import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 
 import com.android.internal.os.BackgroundThread;
@@ -64,10 +64,19 @@ public class PipTouchHandler implements TunerService.Tunable {
     private static final String TUNER_KEY_DRAG_TO_DISMISS = "pip_drag_to_dismiss";
     private static final String TUNER_KEY_TAP_THROUGH = "pip_tap_through";
     private static final String TUNER_KEY_SNAP_MODE_EDGE = "pip_snap_mode_edge";
+    private static final String TUNER_KEY_ALLOW_MINIMIZE = "pip_allow_minimize";
 
     private static final int SNAP_STACK_DURATION = 225;
     private static final int DISMISS_STACK_DURATION = 375;
     private static final int EXPAND_STACK_DURATION = 225;
+    private static final int MINIMIZE_STACK_MAX_DURATION = 200;
+
+    // The fraction of the stack width to show when minimized
+    private static final float MINIMIZED_VISIBLE_FRACTION = 0.25f;
+    // The fraction of the stack width that the user has to drag offscreen to minimize the PIP
+    private static final float MINIMIZE_OFFSCREEN_FRACTION = 0.15f;
+    // The fraction of the stack width that the user has to move when flinging to dismiss the PIP
+    private static final float DISMISS_FLING_DISTANCE_FRACTION = 0.3f;
 
     private final Context mContext;
     private final IActivityManager mActivityManager;
@@ -83,10 +92,16 @@ public class PipTouchHandler implements TunerService.Tunable {
     private final PipSnapAlgorithm mSnapAlgorithm;
     private PipMotionHelper mMotionHelper;
 
+    // Allow swiping offscreen to dismiss the PIP
     private boolean mEnableSwipeToDismiss = true;
+    // Allow dragging the PIP to a location to close it
     private boolean mEnableDragToDismiss = true;
+    // Allow tapping on the PIP to show additional controls
     private boolean mEnableTapThrough = false;
+    // Allow snapping the PIP to the closest edge and not the corners of the screen
     private boolean mEnableSnapToEdge = false;
+    // Allow the PIP to be "docked" slightly offscreen
+    private boolean mEnableMinimizing = false;
 
     private final Rect mPinnedStackBounds = new Rect();
     private final Rect mBoundedPinnedStackBounds = new Rect();
@@ -99,16 +114,16 @@ public class PipTouchHandler implements TunerService.Tunable {
         }
     };
 
-    private final PointF mDownTouch = new PointF();
-    private final PointF mLastTouch = new PointF();
-    private boolean mIsDragging;
-    private boolean mIsSwipingToDismiss;
+    // Behaviour states
     private boolean mIsTappingThrough;
-    private int mActivePointerId;
+    private boolean mIsMinimized;
 
+    // Touch state
+    private final PipTouchState mTouchState;
     private final FlingAnimationUtils mFlingAnimationUtils;
-    private VelocityTracker mVelocityTracker;
+    private final PipTouchGesture[] mGestures;
 
+    // Temporary vars
     private final Rect mTmpBounds = new Rect();
 
     /**
@@ -183,13 +198,19 @@ public class PipTouchHandler implements TunerService.Tunable {
         mMenuController.addListener(mMenuListener);
         mDismissViewController = new PipDismissViewController(context);
         mSnapAlgorithm = new PipSnapAlgorithm(mContext);
+        mTouchState = new PipTouchState(mViewConfig);
         mFlingAnimationUtils = new FlingAnimationUtils(context, 2f);
+        mGestures = new PipTouchGesture[]{
+                mDragToDismissGesture, mSwipeToDismissGesture, mTapThroughGesture, mMinimizeGesture,
+                mDefaultMovementGesture
+        };
         mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
         registerInputConsumer();
 
         // Register any tuner settings changes
         TunerService.get(context).addTunable(this, TUNER_KEY_SWIPE_TO_DISMISS,
-            TUNER_KEY_DRAG_TO_DISMISS, TUNER_KEY_TAP_THROUGH, TUNER_KEY_SNAP_MODE_EDGE);
+            TUNER_KEY_DRAG_TO_DISMISS, TUNER_KEY_TAP_THROUGH, TUNER_KEY_SNAP_MODE_EDGE,
+                TUNER_KEY_ALLOW_MINIMIZE);
     }
 
     @Override
@@ -198,6 +219,8 @@ public class PipTouchHandler implements TunerService.Tunable {
             // Reset back to default
             mEnableSwipeToDismiss = true;
             mEnableDragToDismiss = true;
+            mEnableMinimizing = false;
+            setMinimizedState(false);
             mEnableTapThrough = false;
             mIsTappingThrough = false;
             mEnableSnapToEdge = false;
@@ -211,6 +234,9 @@ public class PipTouchHandler implements TunerService.Tunable {
             case TUNER_KEY_DRAG_TO_DISMISS:
                 mEnableDragToDismiss = Integer.parseInt(newValue) != 0;
                 break;
+            case TUNER_KEY_ALLOW_MINIMIZE:
+                mEnableMinimizing = Integer.parseInt(newValue) != 0;
+                break;
             case TUNER_KEY_TAP_THROUGH:
                 mEnableTapThrough = Integer.parseInt(newValue) != 0;
                 mIsTappingThrough = false;
@@ -233,6 +259,9 @@ public class PipTouchHandler implements TunerService.Tunable {
             return true;
         }
 
+        // Update the touch state
+        mTouchState.onTouchEvent(ev);
+
         switch (ev.getAction()) {
             case MotionEvent.ACTION_DOWN: {
                 // Cancel any existing animations on the pinned stack
@@ -241,173 +270,58 @@ public class PipTouchHandler implements TunerService.Tunable {
                 }
 
                 updateBoundedPinnedStackBounds(true /* updatePinnedStackBounds */);
-                initOrResetVelocityTracker();
-                mVelocityTracker.addMovement(ev);
-                mActivePointerId = ev.getPointerId(0);
-                mLastTouch.set(ev.getX(), ev.getY());
-                mDownTouch.set(mLastTouch);
-                mIsDragging = false;
+                for (PipTouchGesture gesture : mGestures) {
+                    gesture.onDown(mTouchState);
+                }
                 try {
                     mPinnedStackController.setInInteractiveMode(true);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Could not set dragging state", e);
                 }
-                if (mEnableDragToDismiss) {
-                    // TODO: Consider setting a timer such at after X time, we show the dismiss
-                    //       target if the user hasn't already dragged some distance
-                    mDismissViewController.createDismissTarget();
-                }
                 break;
             }
             case MotionEvent.ACTION_MOVE: {
-                // Update the velocity tracker
-                mVelocityTracker.addMovement(ev);
-
-                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
-                float x = ev.getX(activePointerIndex);
-                float y = ev.getY(activePointerIndex);
-                float left = mPinnedStackBounds.left + (x - mLastTouch.x);
-                float top = mPinnedStackBounds.top + (y - mLastTouch.y);
-
-                if (!mIsDragging) {
-                    // Check if the pointer has moved far enough
-                    float movement = PointF.length(mDownTouch.x - x, mDownTouch.y - y);
-                    if (movement > mViewConfig.getScaledTouchSlop()) {
-                        mIsDragging = true;
-                        mIsTappingThrough = false;
-                        mMenuController.hideMenu();
-                        if (mEnableSwipeToDismiss) {
-                            // TODO: this check can have some buffer so that we only start swiping
-                            //       after a significant move out of bounds
-                            mIsSwipingToDismiss = !(mBoundedPinnedStackBounds.left <= left &&
-                                    left <= mBoundedPinnedStackBounds.right) &&
-                                    Math.abs(mDownTouch.x - x) > Math.abs(y - mLastTouch.y);
-                        }
-                        if (mEnableDragToDismiss) {
-                            mDismissViewController.showDismissTarget();
-                        }
+                for (PipTouchGesture gesture : mGestures) {
+                    if (gesture.onMove(mTouchState)) {
+                        break;
                     }
                 }
-
-                if (mIsSwipingToDismiss) {
-                    // Ignore the vertical movement
-                    mTmpBounds.set(mPinnedStackBounds);
-                    mTmpBounds.offsetTo((int) left, mPinnedStackBounds.top);
-                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
-                        mPinnedStackBounds.set(mTmpBounds);
-                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
-                    }
-                } else if (mIsDragging) {
-                    // Move the pinned stack
-                    if (!DEBUG_ALLOW_OUT_OF_BOUNDS_STACK) {
-                        left = Math.max(mBoundedPinnedStackBounds.left, Math.min(
-                                mBoundedPinnedStackBounds.right, left));
-                        top = Math.max(mBoundedPinnedStackBounds.top, Math.min(
-                                mBoundedPinnedStackBounds.bottom, top));
-                    }
-                    mTmpBounds.set(mPinnedStackBounds);
-                    mTmpBounds.offsetTo((int) left, (int) top);
-                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
-                        mPinnedStackBounds.set(mTmpBounds);
-                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
-                    }
-                }
-                mLastTouch.set(ev.getX(), ev.getY());
-                break;
-            }
-            case MotionEvent.ACTION_POINTER_UP: {
-                // Update the velocity tracker
-                mVelocityTracker.addMovement(ev);
-
-                int pointerIndex = ev.getActionIndex();
-                int pointerId = ev.getPointerId(pointerIndex);
-                if (pointerId == mActivePointerId) {
-                    // Select a new active pointer id and reset the movement state
-                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
-                    mActivePointerId = ev.getPointerId(newPointerIndex);
-                    mLastTouch.set(ev.getX(newPointerIndex), ev.getY(newPointerIndex));
-                }
                 break;
             }
             case MotionEvent.ACTION_UP: {
-                // Update the velocity tracker
-                mVelocityTracker.addMovement(ev);
-                mVelocityTracker.computeCurrentVelocity(1000,
-                    ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
-                float velocityX = mVelocityTracker.getXVelocity();
-                float velocityY = mVelocityTracker.getYVelocity();
-                float velocity = PointF.length(velocityX, velocityY);
-
                 // Update the movement bounds again if the state has changed since the user started
                 // dragging (ie. when the IME shows)
                 updateBoundedPinnedStackBounds(false /* updatePinnedStackBounds */);
 
-                if (mIsSwipingToDismiss) {
-                    if (Math.abs(velocityX) > mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
-                        flingToDismiss(velocityX);
-                    } else {
-                        animateToClosestSnapTarget();
-                    }
-                } else if (mIsDragging) {
-                    if (velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
-                        flingToSnapTarget(velocity, velocityX, velocityY);
-                    } else {
-                        int activePointerIndex = ev.findPointerIndex(mActivePointerId);
-                        int x = (int) ev.getX(activePointerIndex);
-                        int y = (int) ev.getY(activePointerIndex);
-                        Rect dismissBounds = mEnableDragToDismiss
-                                ? mDismissViewController.getDismissBounds()
-                                : null;
-                        if (dismissBounds != null && dismissBounds.contains(x, y)) {
-                            animateDismissPinnedStack(dismissBounds);
-                        } else {
-                            animateToClosestSnapTarget();
-                        }
+                for (PipTouchGesture gesture : mGestures) {
+                    if (gesture.onUp(mTouchState)) {
+                        break;
                     }
-                } else {
-                    if (mEnableTapThrough) {
-                        if (!mIsTappingThrough) {
-                            mMenuController.showMenu();
-                            mIsTappingThrough = true;
-                        }
-                    } else {
-                        expandPinnedStackToFullscreen();
-                    }
-                }
-                if (mEnableDragToDismiss) {
-                    mDismissViewController.destroyDismissTarget();
                 }
 
                 // Fall through to clean up
             }
             case MotionEvent.ACTION_CANCEL: {
-                mIsDragging = false;
-                mIsSwipingToDismiss = false;
                 try {
                     mPinnedStackController.setInInteractiveMode(false);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Could not set dragging state", e);
                 }
-                recycleVelocityTracker();
                 break;
             }
         }
         return !mIsTappingThrough;
     }
 
-    private void initOrResetVelocityTracker() {
-        if (mVelocityTracker == null) {
-            mVelocityTracker = VelocityTracker.obtain();
-        } else {
-            mVelocityTracker.clear();
-        }
-    }
-
-    private void recycleVelocityTracker() {
-        if (mVelocityTracker != null) {
-            mVelocityTracker.recycle();
-            mVelocityTracker = null;
-        }
+    /**
+     * @return whether the current touch state is a horizontal drag offscreen.
+     */
+    private boolean isDraggingOffscreen(PipTouchState touchState) {
+        PointF lastDelta = touchState.getLastTouchDelta();
+        PointF downDelta = touchState.getDownTouchDelta();
+        float left = mPinnedStackBounds.left + lastDelta.x;
+        return !(mBoundedPinnedStackBounds.left <= left && left <= mBoundedPinnedStackBounds.right)
+                && Math.abs(downDelta.x) > Math.abs(downDelta.y);
     }
 
     /**
@@ -449,6 +363,74 @@ public class PipTouchHandler implements TunerService.Tunable {
     }
 
     /**
+     * Sets the minimized state and notifies the controller.
+     */
+    private void setMinimizedState(boolean isMinimized) {
+        mIsMinimized = isMinimized;
+        try {
+            mPinnedStackController.setIsMinimized(isMinimized);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not set minimized state", e);
+        }
+    }
+
+    /**
+     * @return whether the given {@param pinnedStackBounds} indicates the PIP should be minimized.
+     */
+    private boolean shouldMinimizedPinnedStack() {
+        Point displaySize = new Point();
+        mContext.getDisplay().getRealSize(displaySize);
+        if (mPinnedStackBounds.left < 0) {
+            float offscreenFraction = (float) -mPinnedStackBounds.left / mPinnedStackBounds.width();
+            return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION;
+        } else if (mPinnedStackBounds.right > displaySize.x) {
+            float offscreenFraction = (float) (mPinnedStackBounds.right - displaySize.x) /
+                    mPinnedStackBounds.width();
+            return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Flings the minimized PIP to the closest minimized snap target.
+     */
+    private void flingToMinimizedSnapTarget(float velocityY) {
+        Rect movementBounds = new Rect(mPinnedStackBounds.left, mBoundedPinnedStackBounds.top,
+                mPinnedStackBounds.left, mBoundedPinnedStackBounds.bottom);
+        Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mPinnedStackBounds,
+                0 /* velocityX */, velocityY);
+        if (!mPinnedStackBounds.equals(toBounds)) {
+            mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
+                    toBounds, 0, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener);
+            mFlingAnimationUtils.apply(mPinnedStackBoundsAnimator, 0,
+                    distanceBetweenRectOffsets(mPinnedStackBounds, toBounds),
+                    velocityY);
+            mPinnedStackBoundsAnimator.start();
+        }
+    }
+
+    /**
+     * Animates the PIP to the minimized state, slightly offscreen.
+     */
+    private void animateToClosestMinimizedTarget() {
+        Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds,
+                mPinnedStackBounds);
+        Point displaySize = new Point();
+        mContext.getDisplay().getRealSize(displaySize);
+        int visibleWidth = (int) (MINIMIZED_VISIBLE_FRACTION * mPinnedStackBounds.width());
+        if (mPinnedStackBounds.left < 0) {
+            toBounds.offsetTo(-toBounds.width() + visibleWidth, toBounds.top);
+        } else if (mPinnedStackBounds.right > displaySize.x) {
+            toBounds.offsetTo(displaySize.x - visibleWidth, toBounds.top);
+        }
+        mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
+                toBounds, MINIMIZE_STACK_MAX_DURATION, LINEAR_OUT_SLOW_IN,
+                mUpdatePinnedStackBoundsListener);
+        mPinnedStackBoundsAnimator.start();
+    }
+
+    /**
      * Flings the PIP to the closest snap target.
      */
     private void flingToSnapTarget(float velocity, float velocityX, float velocityY) {
@@ -478,12 +460,26 @@ public class PipTouchHandler implements TunerService.Tunable {
     }
 
     /**
+     * @return whether the velocity is coincident with the current pinned stack bounds to be
+     *         considered a fling to dismiss.
+     */
+    private boolean isFlingToDismiss(float velocityX) {
+        Point displaySize = new Point();
+        mContext.getDisplay().getRealSize(displaySize);
+        return (mPinnedStackBounds.right > displaySize.x && velocityX > 0) ||
+                (mPinnedStackBounds.left < 0 && velocityX < 0);
+    }
+
+    /**
      * Flings the PIP to dismiss it offscreen.
      */
     private void flingToDismiss(float velocityX) {
+        Point displaySize = new Point();
+        mContext.getDisplay().getRealSize(displaySize);
         float offsetX = velocityX > 0
-            ? mBoundedPinnedStackBounds.right + 2 * mPinnedStackBounds.width()
-            : mBoundedPinnedStackBounds.left - 2 * mPinnedStackBounds.width();
+                ? displaySize.x + mPinnedStackBounds.width()
+                : -mPinnedStackBounds.width();
+
         Rect toBounds = new Rect(mPinnedStackBounds);
         toBounds.offsetTo((int) offsetX, toBounds.top);
         if (!mPinnedStackBounds.equals(toBounds)) {
@@ -495,13 +491,7 @@ public class PipTouchHandler implements TunerService.Tunable {
             mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    BackgroundThread.getHandler().post(() -> {
-                        try {
-                            mActivityManager.removeStack(PINNED_STACK_ID);
-                        } catch (RemoteException e) {
-                            Log.e(TAG, "Failed to remove PIP", e);
-                        }
-                    });
+                    BackgroundThread.getHandler().post(PipTouchHandler.this::dismissPinnedStack);
                 }
             });
             mPinnedStackBoundsAnimator.start();
@@ -521,13 +511,7 @@ public class PipTouchHandler implements TunerService.Tunable {
         mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                BackgroundThread.getHandler().post(() -> {
-                    try {
-                        mActivityManager.removeStack(PINNED_STACK_ID);
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Failed to remove PIP", e);
-                    }
-                });
+                BackgroundThread.getHandler().post(PipTouchHandler.this::dismissPinnedStack);
             }
         });
         mPinnedStackBoundsAnimator.start();
@@ -549,6 +533,27 @@ public class PipTouchHandler implements TunerService.Tunable {
     }
 
     /**
+     * Tries to the move the pinned stack to the given {@param bounds}.
+     */
+    private void movePinnedStack(Rect bounds) {
+        if (!bounds.equals(mPinnedStackBounds)) {
+            mPinnedStackBounds.set(bounds);
+            mMotionHelper.resizeToBounds(mPinnedStackBounds);
+        }
+    }
+
+    /**
+     * Dismisses the pinned stack.
+     */
+    private void dismissPinnedStack() {
+        try {
+            mActivityManager.removeStack(PINNED_STACK_ID);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to remove PIP", e);
+        }
+    }
+
+    /**
      * Updates the movement bounds of the pinned stack.
      */
     private void updateBoundedPinnedStackBounds(boolean updatePinnedStackBounds) {
@@ -572,4 +577,231 @@ public class PipTouchHandler implements TunerService.Tunable {
     private float distanceBetweenRectOffsets(Rect r1, Rect r2) {
         return PointF.length(r1.left - r2.left, r1.top - r2.top);
     }
+
+    /**
+     * Gesture controlling dragging over a target to dismiss the PIP.
+     */
+    private PipTouchGesture mDragToDismissGesture = new PipTouchGesture() {
+        @Override
+        public void onDown(PipTouchState touchState) {
+            if (mEnableDragToDismiss) {
+                // TODO: Consider setting a timer such at after X time, we show the dismiss
+                //       target if the user hasn't already dragged some distance
+                mDismissViewController.createDismissTarget();
+            }
+        }
+
+        @Override
+        boolean onMove(PipTouchState touchState) {
+            if (mEnableDragToDismiss && touchState.startedDragging()) {
+                mDismissViewController.showDismissTarget();
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onUp(PipTouchState touchState) {
+            if (mEnableDragToDismiss) {
+                try {
+                    if (touchState.isDragging()) {
+                        Rect dismissBounds = mDismissViewController.getDismissBounds();
+                        PointF lastTouch = touchState.getLastTouchPosition();
+                        if (dismissBounds.contains((int) lastTouch.x, (int) lastTouch.y)) {
+                            animateDismissPinnedStack(dismissBounds);
+                            return true;
+                        }
+                    }
+                } finally {
+                    mDismissViewController.destroyDismissTarget();
+                }
+            }
+            return false;
+        }
+    };
+
+    /**** Gestures ****/
+
+    /**
+     * Gesture controlling swiping offscreen to dismiss the PIP.
+     */
+    private PipTouchGesture mSwipeToDismissGesture = new PipTouchGesture() {
+        @Override
+        boolean onMove(PipTouchState touchState) {
+            if (mEnableSwipeToDismiss) {
+                boolean isDraggingOffscreen = isDraggingOffscreen(touchState);
+
+                if (touchState.startedDragging() && isDraggingOffscreen) {
+                    // Reset the minimized state once we drag horizontally
+                    setMinimizedState(false);
+                }
+
+                if (isDraggingOffscreen) {
+                    // Move the pinned stack, but ignore the vertical movement
+                    float left = mPinnedStackBounds.left + touchState.getLastTouchDelta().x;
+                    mTmpBounds.set(mPinnedStackBounds);
+                    mTmpBounds.offsetTo((int) left, mPinnedStackBounds.top);
+                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
+                        mPinnedStackBounds.set(mTmpBounds);
+                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
+                    }
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onUp(PipTouchState touchState) {
+            if (mEnableSwipeToDismiss && touchState.isDragging()) {
+                PointF vel = touchState.getVelocity();
+                PointF downDelta = touchState.getDownTouchDelta();
+                float minFlingVel = mFlingAnimationUtils.getMinVelocityPxPerSecond();
+                float flingVelScale = mEnableMinimizing ? 3f : 2f;
+                if (Math.abs(vel.x) > (flingVelScale * minFlingVel)) {
+                    // Determine if this gesture is actually a fling to dismiss
+                    if (isFlingToDismiss(vel.x) && Math.abs(downDelta.x) >=
+                            (DISMISS_FLING_DISTANCE_FRACTION * mPinnedStackBounds.width())) {
+                        flingToDismiss(vel.x);
+                    } else {
+                        flingToSnapTarget(vel.length(), vel.x, vel.y);
+                    }
+                    return true;
+                }
+            }
+            return false;
+        }
+    };
+
+    /**
+     * Gesture controlling dragging the PIP slightly offscreen to minimize it.
+     */
+    private PipTouchGesture mMinimizeGesture = new PipTouchGesture() {
+        @Override
+        boolean onMove(PipTouchState touchState) {
+            if (mEnableMinimizing) {
+                boolean isDraggingOffscreen = isDraggingOffscreen(touchState);
+                if (touchState.startedDragging() && isDraggingOffscreen) {
+                    // Reset the minimized state once we drag horizontally
+                    setMinimizedState(false);
+                }
+
+                if (isDraggingOffscreen) {
+                    // Move the pinned stack, but ignore the vertical movement
+                    float left = mPinnedStackBounds.left + touchState.getLastTouchDelta().x;
+                    mTmpBounds.set(mPinnedStackBounds);
+                    mTmpBounds.offsetTo((int) left, mPinnedStackBounds.top);
+                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
+                        mPinnedStackBounds.set(mTmpBounds);
+                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
+                    }
+                    return true;
+                } else if (mIsMinimized && touchState.isDragging()) {
+                    // Move the pinned stack, but ignore the horizontal movement
+                    PointF lastDelta = touchState.getLastTouchDelta();
+                    float top = mPinnedStackBounds.top + lastDelta.y;
+                    top = Math.max(mBoundedPinnedStackBounds.top, Math.min(
+                            mBoundedPinnedStackBounds.bottom, top));
+                    mTmpBounds.set(mPinnedStackBounds);
+                    mTmpBounds.offsetTo(mPinnedStackBounds.left, (int) top);
+                    movePinnedStack(mTmpBounds);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onUp(PipTouchState touchState) {
+            if (mEnableMinimizing) {
+                if (touchState.isDragging()) {
+                    if (isDraggingOffscreen(touchState)) {
+                        if (shouldMinimizedPinnedStack()) {
+                            setMinimizedState(true);
+                            animateToClosestMinimizedTarget();
+                            return true;
+                        }
+                    } else if (mIsMinimized) {
+                        PointF vel = touchState.getVelocity();
+                        if (vel.length() > mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+                            flingToMinimizedSnapTarget(vel.y);
+                        } else {
+                            animateToClosestMinimizedTarget();
+                        }
+                        return true;
+                    }
+                } else if (mIsMinimized) {
+                    setMinimizedState(false);
+                    animateToClosestSnapTarget();
+                    return true;
+                }
+            }
+            return false;
+        }
+    };
+
+    /**
+     * Gesture controlling tapping on the PIP to show an overlay.
+     */
+    private PipTouchGesture mTapThroughGesture = new PipTouchGesture() {
+        @Override
+        boolean onMove(PipTouchState touchState) {
+            if (mEnableTapThrough && touchState.startedDragging()) {
+                mIsTappingThrough = false;
+                mMenuController.hideMenu();
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onUp(PipTouchState touchState) {
+            if (mEnableTapThrough && !touchState.isDragging() && !mIsTappingThrough) {
+                mMenuController.showMenu();
+                mIsTappingThrough = true;
+                return true;
+            }
+            return false;
+        }
+    };
+
+    /**
+     * Gesture controlling normal movement of the PIP.
+     */
+    private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() {
+        @Override
+        boolean onMove(PipTouchState touchState) {
+            if (touchState.isDragging()) {
+                // Move the pinned stack freely
+                PointF lastDelta = touchState.getLastTouchDelta();
+                float left = mPinnedStackBounds.left + lastDelta.x;
+                float top = mPinnedStackBounds.top + lastDelta.y;
+                if (!DEBUG_ALLOW_OUT_OF_BOUNDS_STACK) {
+                    left = Math.max(mBoundedPinnedStackBounds.left, Math.min(
+                            mBoundedPinnedStackBounds.right, left));
+                    top = Math.max(mBoundedPinnedStackBounds.top, Math.min(
+                            mBoundedPinnedStackBounds.bottom, top));
+                }
+                mTmpBounds.set(mPinnedStackBounds);
+                mTmpBounds.offsetTo((int) left, (int) top);
+                movePinnedStack(mTmpBounds);
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onUp(PipTouchState touchState) {
+            if (touchState.isDragging()) {
+                PointF vel = mTouchState.getVelocity();
+                float velocity = PointF.length(vel.x, vel.y);
+                if (velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+                    flingToSnapTarget(velocity, vel.x, vel.y);
+                } else {
+                    animateToClosestSnapTarget();
+                }
+            } else {
+                expandPinnedStackToFullscreen();
+            }
+            return true;
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java
new file mode 100644 (file)
index 0000000..80af5a6
--- /dev/null
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2016 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.systemui.pip.phone;
+
+import android.app.IActivityManager;
+import android.graphics.PointF;
+import android.view.IPinnedStackController;
+import android.view.IPinnedStackListener;
+import android.view.IWindowManager;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+/**
+ * This keeps track of the touch state throughout the current touch gesture.
+ */
+public class PipTouchState {
+
+    private ViewConfiguration mViewConfig;
+
+    private VelocityTracker mVelocityTracker;
+    private final PointF mDownTouch = new PointF();
+    private final PointF mDownDelta = new PointF();
+    private final PointF mLastTouch = new PointF();
+    private final PointF mLastDelta = new PointF();
+    private final PointF mVelocity = new PointF();
+    private boolean mIsDragging = false;
+    private boolean mStartedDragging = false;
+    private int mActivePointerId;
+
+    public PipTouchState(ViewConfiguration viewConfig) {
+        mViewConfig = viewConfig;
+    }
+
+    /**
+     * Processess a given touch event and updates the state.
+     */
+    public void onTouchEvent(MotionEvent ev) {
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN: {
+                // Initialize the velocity tracker
+                initOrResetVelocityTracker();
+                mActivePointerId = ev.getPointerId(0);
+                mLastTouch.set(ev.getX(), ev.getY());
+                mDownTouch.set(mLastTouch);
+                mIsDragging = false;
+                mStartedDragging = false;
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                // Update the velocity tracker
+                mVelocityTracker.addMovement(ev);
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                float x = ev.getX(pointerIndex);
+                float y = ev.getY(pointerIndex);
+                mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y);
+                mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y);
+
+                boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop();
+                if (!mIsDragging) {
+                    if (hasMovedBeyondTap) {
+                        mIsDragging = true;
+                        mStartedDragging = true;
+                    }
+                } else {
+                    mStartedDragging = false;
+                }
+                mLastTouch.set(x, y);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP: {
+                // Update the velocity tracker
+                mVelocityTracker.addMovement(ev);
+
+                int pointerIndex = ev.getActionIndex();
+                int pointerId = ev.getPointerId(pointerIndex);
+                if (pointerId == mActivePointerId) {
+                    // Select a new active pointer id and reset the movement state
+                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
+                    mActivePointerId = ev.getPointerId(newPointerIndex);
+                    mLastTouch.set(ev.getX(newPointerIndex), ev.getY(newPointerIndex));
+                }
+                break;
+            }
+            case MotionEvent.ACTION_UP: {
+                // Update the velocity tracker
+                mVelocityTracker.addMovement(ev);
+                mVelocityTracker.computeCurrentVelocity(1000,
+                        mViewConfig.getScaledMaximumFlingVelocity());
+                mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                mLastTouch.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+                // Fall through to clean up
+            }
+            case MotionEvent.ACTION_CANCEL: {
+                recycleVelocityTracker();
+                break;
+            }
+        }
+    }
+
+    /**
+     * @return the velocity of the active touch pointer at the point it is lifted off the screen.
+     */
+    public PointF getVelocity() {
+        return mVelocity;
+    }
+
+    /**
+     * @return the last touch position of the active pointer.
+     */
+    public PointF getLastTouchPosition() {
+        return mLastTouch;
+    }
+
+    /**
+     * @return the movement delta between the last handled touch event and the previous touch
+     *         position.
+     */
+    public PointF getLastTouchDelta() {
+        return mLastDelta;
+    }
+
+    /**
+     * @return the movement delta between the last handled touch event and the down touch
+     *         position.
+     */
+    public PointF getDownTouchDelta() {
+        return mDownDelta;
+    }
+
+    /**
+     * @return whether the user has started dragging.
+     */
+    public boolean isDragging() {
+        return mIsDragging;
+    }
+
+    /**
+     * @return whether the user has started dragging just in the last handled touch event.
+     */
+    public boolean startedDragging() {
+        return mStartedDragging;
+    }
+
+    private void initOrResetVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+
+    private void recycleVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+}
index effb1b2..c711b39 100644 (file)
@@ -69,6 +69,7 @@ class PinnedStackController {
 
     // States that affect how the PIP can be manipulated
     private boolean mInInteractiveMode;
+    private boolean mIsMinimized;
     private boolean mIsImeShowing;
     private int mImeHeight;
     private ValueAnimator mBoundsAnimator = null;
@@ -103,6 +104,13 @@ class PinnedStackController {
         }
 
         @Override
+        public void setIsMinimized(final boolean isMinimized) {
+            mHandler.post(() -> {
+                mIsMinimized = isMinimized;
+            });
+        }
+
+        @Override
         public void setSnapToEdge(final boolean snapToEdge) {
             mHandler.post(() -> {
                 mSnapAlgorithm.setSnapToEdge(snapToEdge);
@@ -335,5 +343,6 @@ class PinnedStackController {
         pw.println();
         pw.println(prefix + "  mIsImeShowing=" + mIsImeShowing);
         pw.println(prefix + "  mInInteractiveMode=" + mInInteractiveMode);
+        pw.println(prefix + "  mIsMinimized=" + mIsMinimized);
     }
 }