| /* |
| * 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 static android.app.ActivityManager.StackId.PINNED_STACK_ID; |
| 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; |
| import android.animation.ValueAnimator; |
| 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.Looper; |
| import android.os.RemoteException; |
| import android.util.Log; |
| import android.view.IPinnedStackController; |
| import android.view.IWindowManager; |
| import android.view.InputChannel; |
| import android.view.InputEvent; |
| import android.view.InputEventReceiver; |
| import android.view.MotionEvent; |
| import android.view.ViewConfiguration; |
| |
| import com.android.internal.os.BackgroundThread; |
| import com.android.internal.policy.PipMotionHelper; |
| import com.android.internal.policy.PipSnapAlgorithm; |
| import com.android.systemui.statusbar.FlingAnimationUtils; |
| import com.android.systemui.tuner.TunerService; |
| |
| /** |
| * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding |
| * the PIP. |
| */ |
| public class PipTouchHandler implements TunerService.Tunable { |
| private static final String TAG = "PipTouchHandler"; |
| private static final boolean DEBUG_ALLOW_OUT_OF_BOUNDS_STACK = false; |
| |
| private static final String TUNER_KEY_DRAG_TO_DISMISS = "pip_drag_to_dismiss"; |
| 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 that the user has to drag offscreen to minimize the PIP |
| private static final float MINIMIZE_OFFSCREEN_FRACTION = 0.2f; |
| |
| private final Context mContext; |
| private final IActivityManager mActivityManager; |
| private final IWindowManager mWindowManager; |
| private final ViewConfiguration mViewConfig; |
| private final PipMenuListener mMenuListener = new PipMenuListener(); |
| private IPinnedStackController mPinnedStackController; |
| |
| private PipInputEventReceiver mInputEventReceiver; |
| private PipMenuActivityController mMenuController; |
| private PipDismissViewController mDismissViewController; |
| private final PipSnapAlgorithm mSnapAlgorithm; |
| private PipMotionHelper mMotionHelper; |
| |
| // Allow dragging the PIP to a location to close it |
| private boolean mEnableDragToDismiss = false; |
| // Allow the PIP to be "docked" slightly offscreen |
| private boolean mEnableMinimizing = true; |
| |
| private final Rect mStableInsets = new Rect(); |
| private final Rect mPinnedStackBounds = new Rect(); |
| private final Rect mBoundedPinnedStackBounds = new Rect(); |
| private ValueAnimator mPinnedStackBoundsAnimator = null; |
| private ValueAnimator.AnimatorUpdateListener mUpdatePinnedStackBoundsListener = |
| new AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| mPinnedStackBounds.set((Rect) animation.getAnimatedValue()); |
| } |
| }; |
| |
| // Behaviour states |
| private boolean mIsTappingThrough; |
| private boolean mIsMinimized; |
| |
| // Touch state |
| private final PipTouchState mTouchState; |
| private final FlingAnimationUtils mFlingAnimationUtils; |
| private final PipTouchGesture[] mGestures; |
| |
| // Temporary vars |
| private final Rect mTmpBounds = new Rect(); |
| |
| /** |
| * Input handler used for Pip windows. |
| */ |
| private final class PipInputEventReceiver extends InputEventReceiver { |
| |
| public PipInputEventReceiver(InputChannel inputChannel, Looper looper) { |
| super(inputChannel, looper); |
| } |
| |
| @Override |
| public void onInputEvent(InputEvent event) { |
| boolean handled = true; |
| try { |
| // To be implemented for input handling over Pip windows |
| if (event instanceof MotionEvent) { |
| MotionEvent ev = (MotionEvent) event; |
| handled = handleTouchEvent(ev); |
| } |
| } finally { |
| finishInputEvent(event, handled); |
| } |
| } |
| } |
| |
| /** |
| * A listener for the PIP menu activity. |
| */ |
| private class PipMenuListener implements PipMenuActivityController.Listener { |
| @Override |
| public void onPipMenuVisibilityChanged(boolean visible) { |
| if (!visible) { |
| mIsTappingThrough = false; |
| registerInputConsumer(); |
| } else { |
| unregisterInputConsumer(); |
| } |
| } |
| |
| @Override |
| public void onPipExpand() { |
| if (!mIsMinimized) { |
| expandPinnedStackToFullscreen(); |
| } |
| } |
| |
| @Override |
| public void onPipMinimize() { |
| setMinimizedState(true); |
| animateToClosestMinimizedTarget(); |
| } |
| |
| @Override |
| public void onPipDismiss() { |
| BackgroundThread.getHandler().post(PipTouchHandler.this::dismissPinnedStack); |
| } |
| } |
| |
| public PipTouchHandler(Context context, PipMenuActivityController menuController, |
| IActivityManager activityManager, IWindowManager windowManager) { |
| |
| // Initialize the Pip input consumer |
| mContext = context; |
| mActivityManager = activityManager; |
| mWindowManager = windowManager; |
| mViewConfig = ViewConfiguration.get(context); |
| mMenuController = menuController; |
| mMenuController.addListener(mMenuListener); |
| mDismissViewController = new PipDismissViewController(context); |
| mSnapAlgorithm = new PipSnapAlgorithm(mContext); |
| mTouchState = new PipTouchState(mViewConfig); |
| mFlingAnimationUtils = new FlingAnimationUtils(context, 2f); |
| mGestures = new PipTouchGesture[] { |
| mDragToDismissGesture, mDefaultMovementGesture |
| }; |
| mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler()); |
| registerInputConsumer(); |
| setSnapToEdge(true); |
| |
| // Register any tuner settings changes |
| TunerService.get(context).addTunable(this, TUNER_KEY_DRAG_TO_DISMISS, |
| TUNER_KEY_ALLOW_MINIMIZE); |
| } |
| |
| @Override |
| public void onTuningChanged(String key, String newValue) { |
| if (newValue == null) { |
| // Reset back to default |
| mEnableDragToDismiss = false; |
| mEnableMinimizing = true; |
| setMinimizedState(false); |
| return; |
| } |
| switch (key) { |
| case TUNER_KEY_DRAG_TO_DISMISS: |
| mEnableDragToDismiss = Integer.parseInt(newValue) != 0; |
| break; |
| case TUNER_KEY_ALLOW_MINIMIZE: |
| mEnableMinimizing = Integer.parseInt(newValue) != 0; |
| break; |
| } |
| } |
| |
| public void onActivityPinned() { |
| // Reset some states once we are pinned |
| if (mIsTappingThrough) { |
| mIsTappingThrough = false; |
| registerInputConsumer(); |
| } |
| if (mIsMinimized) { |
| setMinimizedState(false); |
| } |
| } |
| |
| public void onConfigurationChanged() { |
| mSnapAlgorithm.onConfigurationChanged(); |
| updateBoundedPinnedStackBounds(false /* updatePinnedStackBounds */); |
| } |
| |
| public void onMinimizedStateChanged(boolean isMinimized) { |
| mIsMinimized = isMinimized; |
| mSnapAlgorithm.setMinimized(isMinimized); |
| } |
| |
| public void onSnapToEdgeStateChanged(boolean isSnapToEdge) { |
| mSnapAlgorithm.setSnapToEdge(isSnapToEdge); |
| } |
| |
| private boolean handleTouchEvent(MotionEvent ev) { |
| // Skip touch handling until we are bound to the controller |
| if (mPinnedStackController == null) { |
| return true; |
| } |
| |
| // Update the touch state |
| mTouchState.onTouchEvent(ev); |
| |
| switch (ev.getAction()) { |
| case MotionEvent.ACTION_DOWN: { |
| // Cancel any existing animations on the pinned stack |
| if (mPinnedStackBoundsAnimator != null) { |
| mPinnedStackBoundsAnimator.cancel(); |
| } |
| |
| updateBoundedPinnedStackBounds(true /* updatePinnedStackBounds */); |
| for (PipTouchGesture gesture : mGestures) { |
| gesture.onDown(mTouchState); |
| } |
| try { |
| mPinnedStackController.setInInteractiveMode(true); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not set dragging state", e); |
| } |
| break; |
| } |
| case MotionEvent.ACTION_MOVE: { |
| for (PipTouchGesture gesture : mGestures) { |
| if (gesture.onMove(mTouchState)) { |
| break; |
| } |
| } |
| break; |
| } |
| case MotionEvent.ACTION_UP: { |
| // Update the movement bounds again if the state has changed since the user started |
| // dragging (ie. when the IME shows) |
| updateBoundedPinnedStackBounds(false /* updatePinnedStackBounds */); |
| |
| for (PipTouchGesture gesture : mGestures) { |
| if (gesture.onUp(mTouchState)) { |
| break; |
| } |
| } |
| |
| // Fall through to clean up |
| } |
| case MotionEvent.ACTION_CANCEL: { |
| try { |
| mPinnedStackController.setInInteractiveMode(false); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not set dragging state", e); |
| } |
| break; |
| } |
| } |
| return !mIsTappingThrough; |
| } |
| |
| /** |
| * @return whether the current touch state places the pip partially 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); |
| } |
| |
| /** |
| * Registers the input consumer. |
| */ |
| private void registerInputConsumer() { |
| if (mInputEventReceiver == null) { |
| final InputChannel inputChannel = new InputChannel(); |
| try { |
| mWindowManager.destroyInputConsumer(INPUT_CONSUMER_PIP); |
| mWindowManager.createInputConsumer(INPUT_CONSUMER_PIP, inputChannel); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to create PIP input consumer", e); |
| } |
| mInputEventReceiver = new PipInputEventReceiver(inputChannel, Looper.myLooper()); |
| } |
| } |
| |
| /** |
| * Unregisters the input consumer. |
| */ |
| private void unregisterInputConsumer() { |
| if (mInputEventReceiver != null) { |
| try { |
| mWindowManager.destroyInputConsumer(INPUT_CONSUMER_PIP); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to destroy PIP input consumer", e); |
| } |
| mInputEventReceiver.dispose(); |
| mInputEventReceiver = null; |
| } |
| } |
| |
| /** |
| * Sets the controller to update the system of changes from user interaction. |
| */ |
| void setPinnedStackController(IPinnedStackController controller) { |
| mPinnedStackController = controller; |
| } |
| |
| /** |
| * Sets the snap-to-edge state and notifies the controller. |
| */ |
| private void setSnapToEdge(boolean snapToEdge) { |
| onSnapToEdgeStateChanged(snapToEdge); |
| |
| if (mPinnedStackController != null) { |
| try { |
| mPinnedStackController.setSnapToEdge(snapToEdge); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not set snap mode to edge", e); |
| } |
| } |
| } |
| |
| /** |
| * Sets the minimized state and notifies the controller. |
| */ |
| private void setMinimizedState(boolean isMinimized) { |
| onMinimizedStateChanged(isMinimized); |
| |
| if (mPinnedStackController != null) { |
| 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) { |
| // We currently only allow flinging the minimized stack up and down, so just lock the |
| // movement bounds to the current stack bounds horizontally |
| 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() { |
| Point displaySize = new Point(); |
| mContext.getDisplay().getRealSize(displaySize); |
| Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds, |
| mPinnedStackBounds); |
| mSnapAlgorithm.applyMinimizedOffset(toBounds, mBoundedPinnedStackBounds, displaySize, |
| mStableInsets); |
| mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds, |
| toBounds, MINIMIZE_STACK_MAX_DURATION, LINEAR_OUT_SLOW_IN, |
| mUpdatePinnedStackBoundsListener); |
| mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mMenuController.hideMenu(); |
| } |
| }); |
| mPinnedStackBoundsAnimator.start(); |
| } |
| |
| /** |
| * Flings the PIP to the closest snap target. |
| */ |
| private void flingToSnapTarget(float velocity, float velocityX, float velocityY) { |
| Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds, |
| mPinnedStackBounds, velocityX, velocityY); |
| if (!mPinnedStackBounds.equals(toBounds)) { |
| mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds, |
| toBounds, 0, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener); |
| mFlingAnimationUtils.apply(mPinnedStackBoundsAnimator, 0, |
| distanceBetweenRectOffsets(mPinnedStackBounds, toBounds), |
| velocity); |
| mPinnedStackBoundsAnimator.start(); |
| } |
| } |
| |
| /** |
| * Animates the PIP to the closest snap target. |
| */ |
| private void animateToClosestSnapTarget() { |
| Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds, |
| mPinnedStackBounds); |
| if (!mPinnedStackBounds.equals(toBounds)) { |
| mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds, |
| toBounds, SNAP_STACK_DURATION, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener); |
| mPinnedStackBoundsAnimator.start(); |
| } |
| } |
| |
| /** |
| * Animates the dismissal of the PIP over the dismiss target bounds. |
| */ |
| private void animateDismissPinnedStack(Rect dismissBounds) { |
| Rect toBounds = new Rect(dismissBounds.centerX(), |
| dismissBounds.centerY(), |
| dismissBounds.centerX() + 1, |
| dismissBounds.centerY() + 1); |
| mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds, |
| toBounds, DISMISS_STACK_DURATION, FAST_OUT_LINEAR_IN, mUpdatePinnedStackBoundsListener); |
| mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| BackgroundThread.getHandler().post(PipTouchHandler.this::dismissPinnedStack); |
| } |
| }); |
| mPinnedStackBoundsAnimator.start(); |
| } |
| |
| /** |
| * Resizes the pinned stack back to fullscreen. |
| */ |
| void expandPinnedStackToFullscreen() { |
| BackgroundThread.getHandler().post(() -> { |
| try { |
| mActivityManager.resizeStack(PINNED_STACK_ID, null /* bounds */, |
| true /* allowResizeInDockedMode */, true /* preserveWindows */, |
| true /* animate */, EXPAND_STACK_DURATION); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error showing PIP menu activity", e); |
| } |
| }); |
| } |
| |
| /** |
| * 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) { |
| try { |
| StackInfo info = mActivityManager.getStackInfo(PINNED_STACK_ID); |
| if (info != null) { |
| if (updatePinnedStackBounds) { |
| mPinnedStackBounds.set(info.bounds); |
| } |
| mWindowManager.getStableInsets(info.displayId, mStableInsets); |
| mBoundedPinnedStackBounds.set(mWindowManager.getPictureInPictureMovementBounds( |
| info.displayId)); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not fetch PIP movement bounds.", e); |
| } |
| } |
| |
| /** |
| * @return the distance between points {@param p1} and {@param p2}. |
| */ |
| 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 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 (!touchState.allowDraggingOffscreen()) { |
| 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(); |
| if (!mIsMinimized && (shouldMinimizedPinnedStack() |
| || isHorizontalFlingTowardsCurrentEdge(vel))) { |
| // Pip should be minimized |
| setMinimizedState(true); |
| animateToClosestMinimizedTarget(); |
| return true; |
| } |
| if (mIsMinimized) { |
| // If we're dragging and it wasn't a minimize gesture |
| // then we shouldn't be minimized. |
| setMinimizedState(false); |
| } |
| |
| final float velocity = PointF.length(vel.x, vel.y); |
| if (velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond()) { |
| flingToSnapTarget(velocity, vel.x, vel.y); |
| } else { |
| animateToClosestSnapTarget(); |
| } |
| } else if (mIsMinimized) { |
| // This was a tap, so no longer minimized |
| animateToClosestSnapTarget(); |
| setMinimizedState(false); |
| } else if (!mIsTappingThrough) { |
| mMenuController.showMenu(); |
| mIsTappingThrough = true; |
| } else { |
| expandPinnedStackToFullscreen(); |
| } |
| return true; |
| } |
| }; |
| |
| /** |
| * @return whether the gesture ending in the {@param vel} is fast enough to be a fling towards |
| * the same edge the PIP is on. Used to identify a minimize gesture. |
| */ |
| private boolean isHorizontalFlingTowardsCurrentEdge(PointF vel) { |
| final boolean isHorizontal = Math.abs(vel.x) > Math.abs(vel.y); |
| final boolean isFling = PointF.length(vel.x, vel.y) > mFlingAnimationUtils |
| .getMinVelocityPxPerSecond(); |
| final boolean towardsCurrentEdge = onEdge(true /* left */) && vel.x < 0 |
| || onEdge(false /* right */) && vel.x > 0; |
| return towardsCurrentEdge && isHorizontal && isFling; |
| } |
| |
| private boolean onEdge(boolean checkLeft) { |
| if (checkLeft) { |
| return mPinnedStackBounds.left <= mBoundedPinnedStackBounds.left; |
| } else { |
| return mPinnedStackBounds.right >= mBoundedPinnedStackBounds.right |
| + mPinnedStackBounds.width(); |
| } |
| } |
| } |