PipTouchHandler.java revision 9b919413687a4363d9658740fa98dbb780332e55
1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.systemui.pip.phone; 18 19import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE; 20import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE; 21import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL; 22 23import android.animation.Animator; 24import android.animation.AnimatorListenerAdapter; 25import android.animation.ValueAnimator; 26import android.animation.ValueAnimator.AnimatorUpdateListener; 27import android.app.IActivityManager; 28import android.content.ComponentName; 29import android.content.Context; 30import android.graphics.Point; 31import android.graphics.PointF; 32import android.graphics.Rect; 33import android.os.Handler; 34import android.os.RemoteException; 35import android.util.Log; 36import android.util.Size; 37import android.view.IPinnedStackController; 38import android.view.MotionEvent; 39import android.view.ViewConfiguration; 40import android.view.accessibility.AccessibilityEvent; 41import android.view.accessibility.AccessibilityManager; 42import android.view.accessibility.AccessibilityNodeInfo; 43 44import com.android.internal.logging.MetricsLogger; 45import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 46import com.android.internal.policy.PipSnapAlgorithm; 47import com.android.systemui.Dependency; 48import com.android.systemui.R; 49import com.android.systemui.statusbar.FlingAnimationUtils; 50import com.android.systemui.tuner.TunerService; 51 52import java.io.PrintWriter; 53 54/** 55 * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding 56 * the PIP. 57 */ 58public class PipTouchHandler { 59 private static final String TAG = "PipTouchHandler"; 60 61 // Allow the PIP to be dragged to the edge of the screen to be minimized. 62 private static final boolean ENABLE_MINIMIZE = false; 63 // Allow the PIP to be flung from anywhere on the screen to the bottom to be dismissed. 64 private static final boolean ENABLE_FLING_DISMISS = false; 65 66 // These values are used for metrics and should never change 67 private static final int METRIC_VALUE_DISMISSED_BY_TAP = 0; 68 private static final int METRIC_VALUE_DISMISSED_BY_DRAG = 1; 69 70 private static final int SHOW_DISMISS_AFFORDANCE_DELAY = 225; 71 72 // Allow dragging the PIP to a location to close it 73 private static final boolean ENABLE_DISMISS_DRAG_TO_EDGE = true; 74 75 private final Context mContext; 76 private final IActivityManager mActivityManager; 77 private final ViewConfiguration mViewConfig; 78 private final PipMenuListener mMenuListener = new PipMenuListener(); 79 private IPinnedStackController mPinnedStackController; 80 81 private final PipMenuActivityController mMenuController; 82 private final PipDismissViewController mDismissViewController; 83 private final PipSnapAlgorithm mSnapAlgorithm; 84 private final AccessibilityManager mAccessibilityManager; 85 private boolean mShowPipMenuOnAnimationEnd = false; 86 87 // The current movement bounds 88 private Rect mMovementBounds = new Rect(); 89 90 // The reference bounds used to calculate the normal/expanded target bounds 91 private Rect mNormalBounds = new Rect(); 92 private Rect mNormalMovementBounds = new Rect(); 93 private Rect mExpandedBounds = new Rect(); 94 private Rect mExpandedMovementBounds = new Rect(); 95 private int mExpandedShortestEdgeSize; 96 97 // Used to workaround an issue where the WM rotation happens before we are notified, allowing 98 // us to send stale bounds 99 private int mDeferResizeToNormalBoundsUntilRotation = -1; 100 private int mDisplayRotation; 101 102 private Handler mHandler = new Handler(); 103 private Runnable mShowDismissAffordance = new Runnable() { 104 @Override 105 public void run() { 106 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 107 mDismissViewController.showDismissTarget(); 108 } 109 } 110 }; 111 private ValueAnimator.AnimatorUpdateListener mUpdateScrimListener = 112 new AnimatorUpdateListener() { 113 @Override 114 public void onAnimationUpdate(ValueAnimator animation) { 115 updateDismissFraction(); 116 } 117 }; 118 119 // Behaviour states 120 private int mMenuState = MENU_STATE_NONE; 121 private boolean mIsMinimized; 122 private boolean mIsImeShowing; 123 private int mImeHeight; 124 private float mSavedSnapFraction = -1f; 125 private boolean mSendingHoverAccessibilityEvents; 126 private boolean mMovementWithinMinimize; 127 private boolean mMovementWithinDismiss; 128 129 // Touch state 130 private final PipTouchState mTouchState; 131 private final FlingAnimationUtils mFlingAnimationUtils; 132 private final PipTouchGesture[] mGestures; 133 private final PipMotionHelper mMotionHelper; 134 135 // Temp vars 136 private final Rect mTmpBounds = new Rect(); 137 138 /** 139 * A listener for the PIP menu activity. 140 */ 141 private class PipMenuListener implements PipMenuActivityController.Listener { 142 @Override 143 public void onPipMenuStateChanged(int menuState, boolean resize) { 144 setMenuState(menuState, resize); 145 } 146 147 @Override 148 public void onPipExpand() { 149 if (!mIsMinimized) { 150 mMotionHelper.expandPip(); 151 } 152 } 153 154 @Override 155 public void onPipMinimize() { 156 setMinimizedStateInternal(true); 157 mMotionHelper.animateToClosestMinimizedState(mMovementBounds, null /* updateListener */); 158 } 159 160 @Override 161 public void onPipDismiss() { 162 mMotionHelper.dismissPip(); 163 MetricsLogger.action(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED, 164 METRIC_VALUE_DISMISSED_BY_TAP); 165 } 166 167 @Override 168 public void onPipShowMenu() { 169 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 170 mMovementBounds, true /* allowMenuTimeout */); 171 } 172 } 173 174 public PipTouchHandler(Context context, IActivityManager activityManager, 175 PipMenuActivityController menuController, 176 InputConsumerController inputConsumerController) { 177 178 // Initialize the Pip input consumer 179 mContext = context; 180 mActivityManager = activityManager; 181 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 182 mViewConfig = ViewConfiguration.get(context); 183 mMenuController = menuController; 184 mMenuController.addListener(mMenuListener); 185 mDismissViewController = new PipDismissViewController(context); 186 mSnapAlgorithm = new PipSnapAlgorithm(mContext); 187 mTouchState = new PipTouchState(mViewConfig); 188 mFlingAnimationUtils = new FlingAnimationUtils(context, 2f); 189 mGestures = new PipTouchGesture[] { 190 mDefaultMovementGesture 191 }; 192 mMotionHelper = new PipMotionHelper(mContext, mActivityManager, mMenuController, 193 mSnapAlgorithm, mFlingAnimationUtils); 194 mExpandedShortestEdgeSize = context.getResources().getDimensionPixelSize( 195 R.dimen.pip_expanded_shortest_edge_size); 196 197 // Register the listener for input consumer touch events 198 inputConsumerController.setTouchListener(this::handleTouchEvent); 199 inputConsumerController.setRegistrationListener(this::onRegistrationChanged); 200 onRegistrationChanged(inputConsumerController.isRegistered()); 201 } 202 203 public void setTouchEnabled(boolean enabled) { 204 mTouchState.setAllowTouches(enabled); 205 } 206 207 public void showPictureInPictureMenu() { 208 // Only show the menu if the user isn't currently interacting with the PiP 209 if (!mTouchState.isUserInteracting()) { 210 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 211 mMovementBounds, false /* allowMenuTimeout */); 212 } 213 } 214 215 public void onActivityPinned() { 216 cleanUp(); 217 mShowPipMenuOnAnimationEnd = true; 218 } 219 220 public void onActivityUnpinned(ComponentName topPipActivity) { 221 if (topPipActivity == null) { 222 // Clean up state after the last PiP activity is removed 223 cleanUp(); 224 } 225 } 226 227 public void onPinnedStackAnimationEnded() { 228 // Always synchronize the motion helper bounds once PiP animations finish 229 mMotionHelper.synchronizePinnedStackBounds(); 230 231 if (mShowPipMenuOnAnimationEnd) { 232 mMenuController.showMenu(MENU_STATE_CLOSE, mMotionHelper.getBounds(), 233 mMovementBounds, true /* allowMenuTimeout */); 234 mShowPipMenuOnAnimationEnd = false; 235 } 236 } 237 238 public void onConfigurationChanged() { 239 mMotionHelper.onConfigurationChanged(); 240 mMotionHelper.synchronizePinnedStackBounds(); 241 } 242 243 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { 244 mIsImeShowing = imeVisible; 245 mImeHeight = imeHeight; 246 } 247 248 public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect animatingBounds, 249 boolean fromImeAdjustement, int displayRotation) { 250 // Re-calculate the expanded bounds 251 mNormalBounds = normalBounds; 252 Rect normalMovementBounds = new Rect(); 253 mSnapAlgorithm.getMovementBounds(mNormalBounds, insetBounds, normalMovementBounds, 254 mIsImeShowing ? mImeHeight : 0); 255 256 // Calculate the expanded size 257 float aspectRatio = (float) normalBounds.width() / normalBounds.height(); 258 Point displaySize = new Point(); 259 mContext.getDisplay().getRealSize(displaySize); 260 Size expandedSize = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, 261 mExpandedShortestEdgeSize, displaySize.x, displaySize.y); 262 mExpandedBounds.set(0, 0, expandedSize.getWidth(), expandedSize.getHeight()); 263 Rect expandedMovementBounds = new Rect(); 264 mSnapAlgorithm.getMovementBounds(mExpandedBounds, insetBounds, expandedMovementBounds, 265 mIsImeShowing ? mImeHeight : 0); 266 267 268 // If this is from an IME adjustment, then we should move the PiP so that it is not occluded 269 // by the IME 270 if (fromImeAdjustement) { 271 if (mTouchState.isUserInteracting()) { 272 // Defer the update of the current movement bounds until after the user finishes 273 // touching the screen 274 } else { 275 final Rect bounds = new Rect(animatingBounds); 276 final Rect toMovementBounds = mMenuState == MENU_STATE_FULL 277 ? expandedMovementBounds 278 : normalMovementBounds; 279 if (mIsImeShowing) { 280 // IME visible 281 if (bounds.top == mMovementBounds.bottom) { 282 // If the PIP is currently resting on top of the IME, then adjust it with 283 // the hiding IME 284 bounds.offsetTo(bounds.left, toMovementBounds.bottom); 285 } else { 286 bounds.offset(0, Math.min(0, toMovementBounds.bottom - bounds.top)); 287 } 288 } else { 289 // IME hidden 290 if (bounds.top == mMovementBounds.bottom) { 291 // If the PIP is resting on top of the IME, then adjust it with the hiding IME 292 bounds.offsetTo(bounds.left, toMovementBounds.bottom); 293 } 294 } 295 mMotionHelper.animateToIMEOffset(bounds); 296 } 297 } 298 299 // Update the movement bounds after doing the calculations based on the old movement bounds 300 // above 301 mNormalMovementBounds = normalMovementBounds; 302 mExpandedMovementBounds = expandedMovementBounds; 303 mDisplayRotation = displayRotation; 304 updateMovementBounds(mMenuState); 305 306 // If we have a deferred resize, apply it now 307 if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) { 308 mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, 309 mNormalMovementBounds, mMovementBounds, mIsMinimized, 310 true /* immediate */); 311 mSavedSnapFraction = -1f; 312 mDeferResizeToNormalBoundsUntilRotation = -1; 313 } 314 } 315 316 private void onRegistrationChanged(boolean isRegistered) { 317 mAccessibilityManager.setPictureInPictureActionReplacingConnection(isRegistered 318 ? new PipAccessibilityInteractionConnection(mMotionHelper, 319 this::onAccessibilityShowMenu, mHandler) : null); 320 321 if (!isRegistered && mTouchState.isUserInteracting()) { 322 // If the input consumer is unregistered while the user is interacting, then we may not 323 // get the final TOUCH_UP event, so clean up the dismiss target as well 324 cleanUpDismissTarget(); 325 } 326 } 327 328 private void onAccessibilityShowMenu() { 329 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 330 mMovementBounds, false /* allowMenuTimeout */); 331 } 332 333 private boolean handleTouchEvent(MotionEvent ev) { 334 // Skip touch handling until we are bound to the controller 335 if (mPinnedStackController == null) { 336 return true; 337 } 338 339 // Update the touch state 340 mTouchState.onTouchEvent(ev); 341 342 switch (ev.getAction()) { 343 case MotionEvent.ACTION_DOWN: { 344 mMotionHelper.synchronizePinnedStackBounds(); 345 346 for (PipTouchGesture gesture : mGestures) { 347 gesture.onDown(mTouchState); 348 } 349 break; 350 } 351 case MotionEvent.ACTION_MOVE: { 352 for (PipTouchGesture gesture : mGestures) { 353 if (gesture.onMove(mTouchState)) { 354 break; 355 } 356 } 357 break; 358 } 359 case MotionEvent.ACTION_UP: { 360 // Update the movement bounds again if the state has changed since the user started 361 // dragging (ie. when the IME shows) 362 updateMovementBounds(mMenuState); 363 364 for (PipTouchGesture gesture : mGestures) { 365 if (gesture.onUp(mTouchState)) { 366 break; 367 } 368 } 369 370 // Fall through to clean up 371 } 372 case MotionEvent.ACTION_CANCEL: { 373 mTouchState.reset(); 374 break; 375 } 376 case MotionEvent.ACTION_HOVER_ENTER: 377 case MotionEvent.ACTION_HOVER_MOVE: { 378 if (!mSendingHoverAccessibilityEvents) { 379 AccessibilityEvent event = AccessibilityEvent.obtain( 380 AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 381 event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID); 382 mAccessibilityManager.sendAccessibilityEvent(event); 383 mSendingHoverAccessibilityEvents = true; 384 } 385 break; 386 } 387 case MotionEvent.ACTION_HOVER_EXIT: { 388 if (mSendingHoverAccessibilityEvents) { 389 AccessibilityEvent event = AccessibilityEvent.obtain( 390 AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 391 event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID); 392 mAccessibilityManager.sendAccessibilityEvent(event); 393 mSendingHoverAccessibilityEvents = false; 394 } 395 break; 396 } 397 } 398 return mMenuState == MENU_STATE_NONE; 399 } 400 401 /** 402 * Updates the appearance of the menu and scrim on top of the PiP while dismissing. 403 */ 404 void updateDismissFraction() { 405 if (mMenuController != null) { 406 Rect bounds = mMotionHelper.getBounds(); 407 final float target = mMovementBounds.bottom + bounds.height(); 408 float fraction = 0f; 409 if (bounds.bottom > target) { 410 final float distance = bounds.bottom - target; 411 fraction = Math.min(distance / bounds.height(), 1f); 412 } 413 if (Float.compare(fraction, 0f) != 0 || mMenuState != MENU_STATE_NONE) { 414 // Update if the fraction > 0, or if fraction == 0 and the menu was already visible 415 mMenuController.setDismissFraction(fraction); 416 } 417 } 418 } 419 420 /** 421 * Sets the controller to update the system of changes from user interaction. 422 */ 423 void setPinnedStackController(IPinnedStackController controller) { 424 mPinnedStackController = controller; 425 } 426 427 /** 428 * Sets the minimized state. 429 */ 430 void setMinimizedStateInternal(boolean isMinimized) { 431 if (!ENABLE_MINIMIZE) { 432 return; 433 } 434 setMinimizedState(isMinimized, false /* fromController */); 435 } 436 437 /** 438 * Sets the minimized state. 439 */ 440 void setMinimizedState(boolean isMinimized, boolean fromController) { 441 if (!ENABLE_MINIMIZE) { 442 return; 443 } 444 if (mIsMinimized != isMinimized) { 445 MetricsLogger.action(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MINIMIZED, 446 isMinimized); 447 } 448 mIsMinimized = isMinimized; 449 mSnapAlgorithm.setMinimized(isMinimized); 450 451 if (fromController) { 452 if (isMinimized) { 453 // Move the PiP to the new bounds immediately if minimized 454 mMotionHelper.movePip(mMotionHelper.getClosestMinimizedBounds(mNormalBounds, 455 mMovementBounds)); 456 } 457 } else if (mPinnedStackController != null) { 458 try { 459 mPinnedStackController.setIsMinimized(isMinimized); 460 } catch (RemoteException e) { 461 Log.e(TAG, "Could not set minimized state", e); 462 } 463 } 464 } 465 466 /** 467 * Sets the menu visibility. 468 */ 469 void setMenuState(int menuState, boolean resize) { 470 if (menuState == MENU_STATE_FULL) { 471 // Save the current snap fraction and if we do not drag or move the PiP, then 472 // we store back to this snap fraction. Otherwise, we'll reset the snap 473 // fraction and snap to the closest edge 474 Rect expandedBounds = new Rect(mExpandedBounds); 475 if (resize) { 476 mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds, 477 mMovementBounds, mExpandedMovementBounds); 478 } 479 } else if (menuState == MENU_STATE_NONE) { 480 // Try and restore the PiP to the closest edge, using the saved snap fraction 481 // if possible 482 if (resize) { 483 if (mDeferResizeToNormalBoundsUntilRotation == -1) { 484 // This is a very special case: when the menu is expanded and visible, 485 // navigating to another activity can trigger auto-enter PiP, and if the 486 // revealed activity has a forced rotation set, then the controller will get 487 // updated with the new rotation of the display. However, at the same time, 488 // SystemUI will try to hide the menu by creating an animation to the normal 489 // bounds which are now stale. In such a case we defer the animation to the 490 // normal bounds until after the next onMovementBoundsChanged() call to get the 491 // bounds in the new orientation 492 try { 493 int displayRotation = mPinnedStackController.getDisplayRotation(); 494 if (mDisplayRotation != displayRotation) { 495 mDeferResizeToNormalBoundsUntilRotation = displayRotation; 496 } 497 } catch (RemoteException e) { 498 Log.e(TAG, "Could not get display rotation from controller"); 499 } 500 } 501 502 if (mDeferResizeToNormalBoundsUntilRotation == -1) { 503 Rect normalBounds = new Rect(mNormalBounds); 504 mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, 505 mNormalMovementBounds, mMovementBounds, mIsMinimized, 506 false /* immediate */); 507 mSavedSnapFraction = -1f; 508 } 509 } else { 510 // If resizing is not allowed, then the PiP should be frozen until the transition 511 // ends as well 512 setTouchEnabled(false); 513 mSavedSnapFraction = -1f; 514 } 515 } 516 mMenuState = menuState; 517 updateMovementBounds(menuState); 518 if (menuState != MENU_STATE_CLOSE) { 519 MetricsLogger.visibility(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MENU, 520 menuState == MENU_STATE_FULL); 521 } 522 } 523 524 /** 525 * @return the motion helper. 526 */ 527 public PipMotionHelper getMotionHelper() { 528 return mMotionHelper; 529 } 530 531 /** 532 * Gesture controlling normal movement of the PIP. 533 */ 534 private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() { 535 // Whether the PiP was on the left side of the screen at the start of the gesture 536 private boolean mStartedOnLeft; 537 538 @Override 539 public void onDown(PipTouchState touchState) { 540 if (!touchState.isUserInteracting()) { 541 return; 542 } 543 544 mStartedOnLeft = mMotionHelper.getBounds().left < mMovementBounds.centerX(); 545 mMovementWithinMinimize = true; 546 mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom; 547 548 // If the menu is still visible, and we aren't minimized, then just poke the menu 549 // so that it will timeout after the user stops touching it 550 if (mMenuState != MENU_STATE_NONE && !mIsMinimized) { 551 mMenuController.pokeMenu(); 552 } 553 554 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 555 mDismissViewController.createDismissTarget(); 556 mHandler.postDelayed(mShowDismissAffordance, SHOW_DISMISS_AFFORDANCE_DELAY); 557 } 558 } 559 560 @Override 561 boolean onMove(PipTouchState touchState) { 562 if (!touchState.isUserInteracting()) { 563 return false; 564 } 565 566 if (touchState.startedDragging()) { 567 mSavedSnapFraction = -1f; 568 569 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 570 mHandler.removeCallbacks(mShowDismissAffordance); 571 mDismissViewController.showDismissTarget(); 572 } 573 } 574 575 if (touchState.isDragging()) { 576 // Move the pinned stack freely 577 mTmpBounds.set(mMotionHelper.getBounds()); 578 final PointF lastDelta = touchState.getLastTouchDelta(); 579 float left = mTmpBounds.left + lastDelta.x; 580 float top = mTmpBounds.top + lastDelta.y; 581 if (!touchState.allowDraggingOffscreen() || !ENABLE_MINIMIZE) { 582 left = Math.max(mMovementBounds.left, Math.min(mMovementBounds.right, left)); 583 } 584 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 585 // Allow pip to move past bottom bounds 586 top = Math.max(mMovementBounds.top, top); 587 } else { 588 top = Math.max(mMovementBounds.top, Math.min(mMovementBounds.bottom, top)); 589 } 590 mTmpBounds.offsetTo((int) left, (int) top); 591 mMotionHelper.movePip(mTmpBounds); 592 593 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 594 updateDismissFraction(); 595 } 596 597 final PointF curPos = touchState.getLastTouchPosition(); 598 if (mMovementWithinMinimize) { 599 // Track if movement remains near starting edge to identify swipes to minimize 600 mMovementWithinMinimize = mStartedOnLeft 601 ? curPos.x <= mMovementBounds.left + mTmpBounds.width() 602 : curPos.x >= mMovementBounds.right; 603 } 604 if (mMovementWithinDismiss) { 605 // Track if movement remains near the bottom edge to identify swipe to dismiss 606 mMovementWithinDismiss = curPos.y >= mMovementBounds.bottom; 607 } 608 return true; 609 } 610 return false; 611 } 612 613 @Override 614 public boolean onUp(PipTouchState touchState) { 615 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 616 // Clean up the dismiss target regardless of the touch state in case the touch 617 // enabled state changes while the user is interacting 618 cleanUpDismissTarget(); 619 } 620 621 if (!touchState.isUserInteracting()) { 622 return false; 623 } 624 625 final PointF vel = touchState.getVelocity(); 626 final boolean isHorizontal = Math.abs(vel.x) > Math.abs(vel.y); 627 final float velocity = PointF.length(vel.x, vel.y); 628 final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond(); 629 final boolean isUpWithinDimiss = ENABLE_FLING_DISMISS 630 && touchState.getLastTouchPosition().y >= mMovementBounds.bottom 631 && mMotionHelper.isGestureToDismissArea(mMotionHelper.getBounds(), vel.x, 632 vel.y, isFling); 633 final boolean isFlingToBot = isFling && vel.y > 0 && !isHorizontal 634 && (mMovementWithinDismiss || isUpWithinDimiss); 635 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 636 // Check if the user dragged or flung the PiP offscreen to dismiss it 637 if (mMotionHelper.shouldDismissPip() || isFlingToBot) { 638 mMotionHelper.animateDismiss(mMotionHelper.getBounds(), vel.x, 639 vel.y, mUpdateScrimListener); 640 MetricsLogger.action(mContext, 641 MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED, 642 METRIC_VALUE_DISMISSED_BY_DRAG); 643 return true; 644 } 645 } 646 647 if (touchState.isDragging()) { 648 final boolean isFlingToEdge = isFling && isHorizontal && mMovementWithinMinimize 649 && (mStartedOnLeft ? vel.x < 0 : vel.x > 0); 650 if (ENABLE_MINIMIZE && 651 !mIsMinimized && (mMotionHelper.shouldMinimizePip() || isFlingToEdge)) { 652 // Pip should be minimized 653 setMinimizedStateInternal(true); 654 if (mMenuState == MENU_STATE_FULL) { 655 // If the user dragged the expanded PiP to the edge, then hiding the menu 656 // will trigger the PiP to be scaled back to the normal size with the 657 // minimize offset adjusted 658 mMenuController.hideMenu(); 659 } else { 660 mMotionHelper.animateToClosestMinimizedState(mMovementBounds, 661 mUpdateScrimListener); 662 } 663 return true; 664 } 665 if (mIsMinimized) { 666 // If we're dragging and it wasn't a minimize gesture then we shouldn't be 667 // minimized. 668 setMinimizedStateInternal(false); 669 } 670 671 AnimatorListenerAdapter postAnimationCallback = null; 672 if (mMenuState != MENU_STATE_NONE) { 673 // If the menu is still visible, and we aren't minimized, then just poke the 674 // menu so that it will timeout after the user stops touching it 675 mMenuController.showMenu(mMenuState, mMotionHelper.getBounds(), 676 mMovementBounds, true /* allowMenuTimeout */); 677 } else { 678 // If the menu is not visible, then we can still be showing the activity for the 679 // dismiss overlay, so just finish it after the animation completes 680 postAnimationCallback = new AnimatorListenerAdapter() { 681 @Override 682 public void onAnimationEnd(Animator animation) { 683 mMenuController.hideMenu(); 684 } 685 }; 686 } 687 688 if (isFling) { 689 mMotionHelper.flingToSnapTarget(velocity, vel.x, vel.y, mMovementBounds, 690 mUpdateScrimListener, postAnimationCallback); 691 } else { 692 mMotionHelper.animateToClosestSnapTarget(mMovementBounds, mUpdateScrimListener, 693 postAnimationCallback); 694 } 695 } else if (mIsMinimized) { 696 // This was a tap, so no longer minimized 697 mMotionHelper.animateToClosestSnapTarget(mMovementBounds, null /* updateListener */, 698 null /* animatorListener */); 699 setMinimizedStateInternal(false); 700 } else if (mMenuState != MENU_STATE_FULL) { 701 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 702 mMovementBounds, true /* allowMenuTimeout */); 703 } else { 704 mMenuController.hideMenu(); 705 mMotionHelper.expandPip(); 706 } 707 return true; 708 } 709 }; 710 711 /** 712 * Updates the current movement bounds based on whether the menu is currently visible. 713 */ 714 private void updateMovementBounds(int menuState) { 715 boolean isMenuExpanded = menuState == MENU_STATE_FULL; 716 mMovementBounds = isMenuExpanded 717 ? mExpandedMovementBounds 718 : mNormalMovementBounds; 719 try { 720 mPinnedStackController.setMinEdgeSize(isMenuExpanded ? mExpandedShortestEdgeSize : 0); 721 } catch (RemoteException e) { 722 Log.e(TAG, "Could not set minimized state", e); 723 } 724 } 725 726 /** 727 * Removes the dismiss target and cancels any pending callbacks to show it. 728 */ 729 private void cleanUpDismissTarget() { 730 mHandler.removeCallbacks(mShowDismissAffordance); 731 mDismissViewController.destroyDismissTarget(); 732 } 733 734 /** 735 * Resets some states related to the touch handling. 736 */ 737 private void cleanUp() { 738 if (mIsMinimized) { 739 setMinimizedStateInternal(false); 740 } 741 cleanUpDismissTarget(); 742 } 743 744 public void dump(PrintWriter pw, String prefix) { 745 final String innerPrefix = prefix + " "; 746 pw.println(prefix + TAG); 747 pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds); 748 pw.println(innerPrefix + "mNormalBounds=" + mNormalBounds); 749 pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds); 750 pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds); 751 pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds); 752 pw.println(innerPrefix + "mMenuState=" + mMenuState); 753 pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized); 754 pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); 755 pw.println(innerPrefix + "mImeHeight=" + mImeHeight); 756 pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction); 757 pw.println(innerPrefix + "mEnableDragToEdgeDismiss=" + ENABLE_DISMISS_DRAG_TO_EDGE); 758 pw.println(innerPrefix + "mEnableMinimize=" + ENABLE_MINIMIZE); 759 mSnapAlgorithm.dump(pw, innerPrefix); 760 mTouchState.dump(pw, innerPrefix); 761 mMotionHelper.dump(pw, innerPrefix); 762 } 763 764} 765