1 /* 2 * Copyright (C) 2020 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 17 package com.android.wm.shell.bubbles; 18 19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 20 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 21 22 import static com.android.wm.shell.animation.Interpolators.ALPHA_IN; 23 import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; 24 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; 25 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; 26 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 27 import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; 28 29 import android.animation.Animator; 30 import android.animation.AnimatorListenerAdapter; 31 import android.animation.AnimatorSet; 32 import android.animation.ObjectAnimator; 33 import android.animation.ValueAnimator; 34 import android.annotation.SuppressLint; 35 import android.app.ActivityManager; 36 import android.content.ContentResolver; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.res.Resources; 40 import android.content.res.TypedArray; 41 import android.graphics.Outline; 42 import android.graphics.PointF; 43 import android.graphics.Rect; 44 import android.graphics.RectF; 45 import android.graphics.drawable.ColorDrawable; 46 import android.os.Bundle; 47 import android.provider.Settings; 48 import android.util.Log; 49 import android.view.Choreographer; 50 import android.view.LayoutInflater; 51 import android.view.MotionEvent; 52 import android.view.SurfaceControl; 53 import android.view.SurfaceHolder; 54 import android.view.SurfaceView; 55 import android.view.View; 56 import android.view.ViewGroup; 57 import android.view.ViewOutlineProvider; 58 import android.view.ViewTreeObserver; 59 import android.view.accessibility.AccessibilityNodeInfo; 60 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 61 import android.widget.FrameLayout; 62 import android.widget.ImageView; 63 import android.widget.TextView; 64 65 import androidx.annotation.NonNull; 66 import androidx.annotation.Nullable; 67 import androidx.dynamicanimation.animation.DynamicAnimation; 68 import androidx.dynamicanimation.animation.FloatPropertyCompat; 69 import androidx.dynamicanimation.animation.SpringAnimation; 70 import androidx.dynamicanimation.animation.SpringForce; 71 72 import com.android.internal.annotations.VisibleForTesting; 73 import com.android.internal.util.FrameworkStatsLog; 74 import com.android.wm.shell.R; 75 import com.android.wm.shell.animation.Interpolators; 76 import com.android.wm.shell.animation.PhysicsAnimator; 77 import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix; 78 import com.android.wm.shell.bubbles.animation.ExpandedAnimationController; 79 import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout; 80 import com.android.wm.shell.bubbles.animation.StackAnimationController; 81 import com.android.wm.shell.common.FloatingContentCoordinator; 82 import com.android.wm.shell.common.ShellExecutor; 83 import com.android.wm.shell.common.magnetictarget.MagnetizedObject; 84 85 import java.io.FileDescriptor; 86 import java.io.PrintWriter; 87 import java.math.BigDecimal; 88 import java.math.RoundingMode; 89 import java.util.ArrayList; 90 import java.util.Collections; 91 import java.util.List; 92 import java.util.function.Consumer; 93 import java.util.stream.Collectors; 94 95 /** 96 * Renders bubbles in a stack and handles animating expanded and collapsed states. 97 */ 98 public class BubbleStackView extends FrameLayout 99 implements ViewTreeObserver.OnComputeInternalInsetsListener { 100 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES; 101 102 /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ 103 static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f; 104 105 /** Velocity required to dismiss the flyout via drag. */ 106 private static final float FLYOUT_DISMISS_VELOCITY = 2000f; 107 108 /** 109 * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel 110 * for every 8 pixels overscrolled). 111 */ 112 private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f; 113 114 private static final int FADE_IN_DURATION = 320; 115 116 /** How long to wait, in milliseconds, before hiding the flyout. */ 117 @VisibleForTesting 118 static final int FLYOUT_HIDE_AFTER = 5000; 119 120 private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f; 121 122 private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; 123 124 private static final float SCRIM_ALPHA = 0.6f; 125 126 /** 127 * How long to wait to animate the stack temporarily invisible after a drag/flyout hide 128 * animation ends, if we are in fact temporarily invisible. 129 */ 130 private static final int ANIMATE_TEMPORARILY_INVISIBLE_DELAY = 1000; 131 132 private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG = 133 new PhysicsAnimator.SpringConfig( 134 StackAnimationController.IME_ANIMATION_STIFFNESS, 135 StackAnimationController.DEFAULT_BOUNCINESS); 136 137 private final PhysicsAnimator.SpringConfig mScaleInSpringConfig = 138 new PhysicsAnimator.SpringConfig(300f, 0.9f); 139 140 private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig = 141 new PhysicsAnimator.SpringConfig(900f, 1f); 142 143 private final PhysicsAnimator.SpringConfig mTranslateSpringConfig = 144 new PhysicsAnimator.SpringConfig( 145 SpringForce.STIFFNESS_VERY_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY); 146 147 /** 148 * Handler to use for all delayed animations - this way, we can easily cancel them before 149 * starting a new animation. 150 */ 151 private final ShellExecutor mDelayedAnimationExecutor; 152 private Runnable mDelayedAnimation; 153 154 /** 155 * Interface to synchronize {@link View} state and the screen. 156 * 157 * {@hide} 158 */ 159 public interface SurfaceSynchronizer { 160 /** 161 * Wait until requested change on a {@link View} is reflected on the screen. 162 * 163 * @param callback callback to run after the change is reflected on the screen. 164 */ syncSurfaceAndRun(Runnable callback)165 void syncSurfaceAndRun(Runnable callback); 166 } 167 168 private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER = 169 new SurfaceSynchronizer() { 170 @Override 171 public void syncSurfaceAndRun(Runnable callback) { 172 Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { 173 // Just wait 2 frames. There is no guarantee, but this is usually enough time that 174 // the requested change is reflected on the screen. 175 // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and 176 // surfaces, rewrite this logic with them. 177 private int mFrameWait = 2; 178 179 @Override 180 public void doFrame(long frameTimeNanos) { 181 if (--mFrameWait > 0) { 182 Choreographer.getInstance().postFrameCallback(this); 183 } else { 184 callback.run(); 185 } 186 } 187 }); 188 } 189 }; 190 private final BubbleController mBubbleController; 191 private final BubbleData mBubbleData; 192 private StackViewState mStackViewState = new StackViewState(); 193 194 private final ValueAnimator mDismissBubbleAnimator; 195 196 private PhysicsAnimationLayout mBubbleContainer; 197 private StackAnimationController mStackAnimationController; 198 private ExpandedAnimationController mExpandedAnimationController; 199 200 private View mScrim; 201 private View mManageMenuScrim; 202 private FrameLayout mExpandedViewContainer; 203 204 /** Matrix used to scale the expanded view container with a given pivot point. */ 205 private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix(); 206 207 /** 208 * SurfaceView that we draw screenshots of animating-out bubbles into. This allows us to animate 209 * between bubble activities without needing both to be alive at the same time. 210 */ 211 private SurfaceView mAnimatingOutSurfaceView; 212 private boolean mAnimatingOutSurfaceReady; 213 214 /** Container for the animating-out SurfaceView. */ 215 private FrameLayout mAnimatingOutSurfaceContainer; 216 217 /** Animator for animating the alpha value of the animating out SurfaceView. */ 218 private final ValueAnimator mAnimatingOutSurfaceAlphaAnimator = ValueAnimator.ofFloat(0f, 1f); 219 220 /** 221 * Buffer containing a screenshot of the animating-out bubble. This is drawn into the 222 * SurfaceView during animations. 223 */ 224 private SurfaceControl.ScreenshotHardwareBuffer mAnimatingOutBubbleBuffer; 225 226 private BubbleFlyoutView mFlyout; 227 /** Runnable that fades out the flyout and then sets it to GONE. */ 228 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */); 229 /** 230 * Callback to run after the flyout hides. Also called if a new flyout is shown before the 231 * previous one animates out. 232 */ 233 private Runnable mAfterFlyoutHidden; 234 /** 235 * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout 236 * once it collapses. 237 */ 238 @Nullable 239 private BubbleViewProvider mBubbleToExpandAfterFlyoutCollapse = null; 240 241 /** Layout change listener that moves the stack to the nearest valid position on rotation. */ 242 private OnLayoutChangeListener mOrientationChangedListener; 243 244 @Nullable private RelativeStackPosition mRelativeStackPositionBeforeRotation; 245 246 private int mBubbleSize; 247 private int mBubbleElevation; 248 private int mBubbleTouchPadding; 249 private int mExpandedViewPadding; 250 private int mCornerRadius; 251 @Nullable private BubbleViewProvider mExpandedBubble; 252 private boolean mIsExpanded; 253 254 /** Whether the stack is currently on the left side of the screen, or animating there. */ 255 private boolean mStackOnLeftOrWillBe = true; 256 257 /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */ 258 private boolean mIsGestureInProgress = false; 259 260 /** Whether or not the stack is temporarily invisible off the side of the screen. */ 261 private boolean mTemporarilyInvisible = false; 262 263 /** Whether we're in the middle of dragging the stack around by touch. */ 264 private boolean mIsDraggingStack = false; 265 266 /** Whether the expanded view has been hidden, because we are dragging out a bubble. */ 267 private boolean mExpandedViewTemporarilyHidden = false; 268 269 /** Animator for animating the expanded view's alpha (including the TaskView inside it). */ 270 private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f); 271 272 /** 273 * The pointer index of the ACTION_DOWN event we received prior to an ACTION_UP. We'll ignore 274 * touches from other pointer indices. 275 */ 276 private int mPointerIndexDown = -1; 277 278 /** Description of current animation controller state. */ dump(FileDescriptor fd, PrintWriter pw, String[] args)279 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 280 pw.println("Stack view state:"); 281 282 String bubblesOnScreen = BubbleDebugConfig.formatBubblesString( 283 getBubblesOnScreen(), getExpandedBubble()); 284 pw.print(" bubbles on screen: "); pw.println(bubblesOnScreen); 285 pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress); 286 pw.print(" showingDismiss: "); pw.println(mDismissView.isShowing()); 287 pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating); 288 pw.print(" expandedContainerVis: "); pw.println(mExpandedViewContainer.getVisibility()); 289 pw.print(" expandedContainerAlpha: "); pw.println(mExpandedViewContainer.getAlpha()); 290 pw.print(" expandedContainerMatrix: "); 291 pw.println(mExpandedViewContainer.getAnimationMatrix()); 292 293 mStackAnimationController.dump(fd, pw, args); 294 mExpandedAnimationController.dump(fd, pw, args); 295 296 if (mExpandedBubble != null) { 297 pw.println("Expanded bubble state:"); 298 pw.println(" expandedBubbleKey: " + mExpandedBubble.getKey()); 299 300 final BubbleExpandedView expandedView = mExpandedBubble.getExpandedView(); 301 302 if (expandedView != null) { 303 pw.println(" expandedViewVis: " + expandedView.getVisibility()); 304 pw.println(" expandedViewAlpha: " + expandedView.getAlpha()); 305 pw.println(" expandedViewTaskId: " + expandedView.getTaskId()); 306 307 final View av = expandedView.getTaskView(); 308 309 if (av != null) { 310 pw.println(" activityViewVis: " + av.getVisibility()); 311 pw.println(" activityViewAlpha: " + av.getAlpha()); 312 } else { 313 pw.println(" activityView is null"); 314 } 315 } else { 316 pw.println("Expanded bubble view state: expanded bubble view is null"); 317 } 318 } else { 319 pw.println("Expanded bubble state: expanded bubble is null"); 320 } 321 } 322 323 private Bubbles.BubbleExpandListener mExpandListener; 324 325 /** Callback to run when we want to unbubble the given notification's conversation. */ 326 private Consumer<String> mUnbubbleConversationCallback; 327 328 private boolean mViewUpdatedRequested = false; 329 private boolean mIsExpansionAnimating = false; 330 private boolean mIsBubbleSwitchAnimating = false; 331 332 /** The view to shrink and apply alpha to when magneted to the dismiss target. */ 333 @Nullable private View mViewBeingDismissed; 334 335 private Rect mTempRect = new Rect(); 336 337 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); 338 339 private ViewTreeObserver.OnPreDrawListener mViewUpdater = 340 new ViewTreeObserver.OnPreDrawListener() { 341 @Override 342 public boolean onPreDraw() { 343 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); 344 updateExpandedView(); 345 mViewUpdatedRequested = false; 346 return true; 347 } 348 }; 349 350 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater = 351 this::updateSystemGestureExcludeRects; 352 353 /** Float property that 'drags' the flyout. */ 354 private final FloatPropertyCompat mFlyoutCollapseProperty = 355 new FloatPropertyCompat("FlyoutCollapseSpring") { 356 @Override 357 public float getValue(Object o) { 358 return mFlyoutDragDeltaX; 359 } 360 361 @Override 362 public void setValue(Object o, float v) { 363 setFlyoutStateForDragLength(v); 364 } 365 }; 366 367 /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */ 368 private final SpringAnimation mFlyoutTransitionSpring = 369 new SpringAnimation(this, mFlyoutCollapseProperty); 370 371 /** Distance the flyout has been dragged in the X axis. */ 372 private float mFlyoutDragDeltaX = 0f; 373 374 /** 375 * Runnable that animates in the flyout. This reference is needed to cancel delayed postings. 376 */ 377 private Runnable mAnimateInFlyout; 378 379 /** 380 * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides 381 * it immediately. 382 */ 383 private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring = 384 (dynamicAnimation, b, v, v1) -> { 385 if (mFlyoutDragDeltaX == 0) { 386 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 387 } else { 388 mFlyout.hideFlyout(); 389 } 390 }; 391 392 @NonNull 393 private final SurfaceSynchronizer mSurfaceSynchronizer; 394 395 /** 396 * The currently magnetized object, which is being dragged and will be attracted to the magnetic 397 * dismiss target. 398 * 399 * This is either the stack itself, or an individual bubble. 400 */ 401 private MagnetizedObject<?> mMagnetizedObject; 402 403 /** 404 * The MagneticTarget instance for our circular dismiss view. This is added to the 405 * MagnetizedObject instances for the stack and any dragged-out bubbles. 406 */ 407 private MagnetizedObject.MagneticTarget mMagneticTarget; 408 409 /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */ 410 private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener = 411 new MagnetizedObject.MagnetListener() { 412 @Override 413 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { 414 if (mExpandedAnimationController.getDraggedOutBubble() == null) { 415 return; 416 } 417 animateDismissBubble( 418 mExpandedAnimationController.getDraggedOutBubble(), true); 419 } 420 421 @Override 422 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, 423 float velX, float velY, boolean wasFlungOut) { 424 if (mExpandedAnimationController.getDraggedOutBubble() == null) { 425 return; 426 } 427 animateDismissBubble( 428 mExpandedAnimationController.getDraggedOutBubble(), false); 429 430 if (wasFlungOut) { 431 mExpandedAnimationController.snapBubbleBack( 432 mExpandedAnimationController.getDraggedOutBubble(), velX, velY); 433 mDismissView.hide(); 434 } else { 435 mExpandedAnimationController.onUnstuckFromTarget(); 436 } 437 } 438 439 @Override 440 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { 441 if (mExpandedAnimationController.getDraggedOutBubble() == null) { 442 return; 443 } 444 445 mExpandedAnimationController.dismissDraggedOutBubble( 446 mExpandedAnimationController.getDraggedOutBubble() /* bubble */, 447 mDismissView.getHeight() /* translationYBy */, 448 BubbleStackView.this::dismissMagnetizedObject /* after */); 449 mDismissView.hide(); 450 } 451 }; 452 453 /** Magnet listener that handles animating and dismissing the entire stack. */ 454 private final MagnetizedObject.MagnetListener mStackMagnetListener = 455 new MagnetizedObject.MagnetListener() { 456 @Override 457 public void onStuckToTarget( 458 @NonNull MagnetizedObject.MagneticTarget target) { 459 animateDismissBubble(mBubbleContainer, true); 460 } 461 462 @Override 463 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, 464 float velX, float velY, boolean wasFlungOut) { 465 animateDismissBubble(mBubbleContainer, false); 466 if (wasFlungOut) { 467 mStackAnimationController.flingStackThenSpringToEdge( 468 mStackAnimationController.getStackPosition().x, velX, velY); 469 mDismissView.hide(); 470 } else { 471 mStackAnimationController.onUnstuckFromTarget(); 472 } 473 } 474 475 @Override 476 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { 477 mStackAnimationController.animateStackDismissal( 478 mDismissView.getHeight() /* translationYBy */, 479 () -> { 480 resetDismissAnimator(); 481 dismissMagnetizedObject(); 482 } 483 ); 484 mDismissView.hide(); 485 } 486 }; 487 488 /** 489 * Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack. 490 * When expanded, clicking a bubble either expands that bubble, or collapses the stack. 491 */ 492 private OnClickListener mBubbleClickListener = new OnClickListener() { 493 @Override 494 public void onClick(View view) { 495 mIsDraggingStack = false; // If the touch ended in a click, we're no longer dragging. 496 497 // Bubble clicks either trigger expansion/collapse or a bubble switch, both of which we 498 // shouldn't interrupt. These are quick transitions, so it's not worth trying to adjust 499 // the animations inflight. 500 if (mIsExpansionAnimating || mIsBubbleSwitchAnimating) { 501 return; 502 } 503 504 final Bubble clickedBubble = mBubbleData.getBubbleWithView(view); 505 506 // If the bubble has since left us, ignore the click. 507 if (clickedBubble == null) { 508 return; 509 } 510 511 final boolean clickedBubbleIsCurrentlyExpandedBubble = 512 clickedBubble.getKey().equals(mExpandedBubble.getKey()); 513 514 if (isExpanded()) { 515 mExpandedAnimationController.onGestureFinished(); 516 } 517 518 if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) { 519 if (clickedBubble != mBubbleData.getSelectedBubble()) { 520 // Select the clicked bubble. 521 mBubbleData.setSelectedBubble(clickedBubble); 522 } else { 523 // If the clicked bubble is the selected bubble (but not the expanded bubble), 524 // that means overflow was previously expanded. Set the selected bubble 525 // internally without going through BubbleData (which would ignore it since it's 526 // already selected). 527 setSelectedBubble(clickedBubble); 528 } 529 } else { 530 // Otherwise, we either tapped the stack (which means we're collapsed 531 // and should expand) or the currently selected bubble (we're expanded 532 // and should collapse). 533 if (!maybeShowStackEdu() && !mShowedUserEducationInTouchListenerActive) { 534 mBubbleData.setExpanded(!mBubbleData.isExpanded()); 535 } 536 mShowedUserEducationInTouchListenerActive = false; 537 } 538 } 539 }; 540 541 /** 542 * Touch listener set on each bubble view. This enables dragging and dismissing the stack (when 543 * collapsed), or individual bubbles (when expanded). 544 */ 545 private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() { 546 547 @Override 548 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { 549 // If we're expanding or collapsing, consume but ignore all touch events. 550 if (mIsExpansionAnimating) { 551 return true; 552 } 553 554 mShowedUserEducationInTouchListenerActive = false; 555 if (maybeShowStackEdu()) { 556 mShowedUserEducationInTouchListenerActive = true; 557 return true; 558 } else if (isStackEduShowing()) { 559 mStackEduView.hide(false /* fromExpansion */); 560 } 561 562 // If the manage menu is visible, just hide it. 563 if (mShowingManage) { 564 showManageMenu(false /* show */); 565 } 566 567 if (mBubbleData.isExpanded()) { 568 if (mManageEduView != null) { 569 mManageEduView.hide(); 570 } 571 572 // If we're expanded, tell the animation controller to prepare to drag this bubble, 573 // dispatching to the individual bubble magnet listener. 574 mExpandedAnimationController.prepareForBubbleDrag( 575 v /* bubble */, 576 mMagneticTarget, 577 mIndividualBubbleMagnetListener); 578 579 hideCurrentInputMethod(); 580 581 // Save the magnetized individual bubble so we can dispatch touch events to it. 582 mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut(); 583 } else { 584 // If we're collapsed, prepare to drag the stack. Cancel active animations, set the 585 // animation controller, and hide the flyout. 586 mStackAnimationController.cancelStackPositionAnimations(); 587 mBubbleContainer.setActiveController(mStackAnimationController); 588 hideFlyoutImmediate(); 589 590 if (mPositioner.showingInTaskbar()) { 591 // In taskbar, the stack isn't draggable so we shouldn't dispatch touch events. 592 mMagnetizedObject = null; 593 } else { 594 // Save the magnetized stack so we can dispatch touch events to it. 595 mMagnetizedObject = mStackAnimationController.getMagnetizedStack(); 596 mMagnetizedObject.clearAllTargets(); 597 mMagnetizedObject.addTarget(mMagneticTarget); 598 mMagnetizedObject.setMagnetListener(mStackMagnetListener); 599 } 600 601 mIsDraggingStack = true; 602 603 // Cancel animations to make the stack temporarily invisible, since we're now 604 // dragging it. 605 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 606 } 607 608 passEventToMagnetizedObject(ev); 609 610 // Bubbles are always interested in all touch events! 611 return true; 612 } 613 614 @Override 615 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 616 float viewInitialY, float dx, float dy) { 617 // If we're expanding or collapsing, ignore all touch events. 618 if (mIsExpansionAnimating 619 // Also ignore events if we shouldn't be draggable. 620 || (mPositioner.showingInTaskbar() && !mIsExpanded) 621 || mShowedUserEducationInTouchListenerActive) { 622 return; 623 } 624 625 // Show the dismiss target, if we haven't already. 626 mDismissView.show(); 627 628 if (mIsExpanded && mExpandedBubble != null && v.equals(mExpandedBubble.getIconView())) { 629 // Hide the expanded view if we're dragging out the expanded bubble, and we haven't 630 // already hidden it. 631 hideExpandedViewIfNeeded(); 632 } 633 634 // First, see if the magnetized object consumes the event - if so, we shouldn't move the 635 // bubble since it's stuck to the target. 636 if (!passEventToMagnetizedObject(ev)) { 637 updateBubbleShadows(true /* showForAllBubbles */); 638 if (mBubbleData.isExpanded() || mPositioner.showingInTaskbar()) { 639 mExpandedAnimationController.dragBubbleOut( 640 v, viewInitialX + dx, viewInitialY + dy); 641 } else { 642 if (isStackEduShowing()) { 643 mStackEduView.hide(false /* fromExpansion */); 644 } 645 mStackAnimationController.moveStackFromTouch( 646 viewInitialX + dx, viewInitialY + dy); 647 } 648 } 649 } 650 651 @Override 652 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 653 float viewInitialY, float dx, float dy, float velX, float velY) { 654 // If we're expanding or collapsing, ignore all touch events. 655 if (mIsExpansionAnimating 656 // Also ignore events if we shouldn't be draggable. 657 || (mPositioner.showingInTaskbar() && !mIsExpanded)) { 658 return; 659 } 660 if (mShowedUserEducationInTouchListenerActive) { 661 mShowedUserEducationInTouchListenerActive = false; 662 return; 663 } 664 665 // First, see if the magnetized object consumes the event - if so, the bubble was 666 // released in the target or flung out of it, and we should ignore the event. 667 if (!passEventToMagnetizedObject(ev)) { 668 if (mBubbleData.isExpanded()) { 669 mExpandedAnimationController.snapBubbleBack(v, velX, velY); 670 671 // Re-show the expanded view if we hid it. 672 showExpandedViewIfNeeded(); 673 } else { 674 // Fling the stack to the edge, and save whether or not it's going to end up on 675 // the left side of the screen. 676 final boolean oldOnLeft = mStackOnLeftOrWillBe; 677 mStackOnLeftOrWillBe = 678 mStackAnimationController.flingStackThenSpringToEdge( 679 viewInitialX + dx, velX, velY) <= 0; 680 final boolean updateForCollapsedStack = oldOnLeft != mStackOnLeftOrWillBe; 681 updateBadges(updateForCollapsedStack); 682 logBubbleEvent(null /* no bubble associated with bubble stack move */, 683 FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); 684 } 685 mDismissView.hide(); 686 } 687 688 mIsDraggingStack = false; 689 690 // Hide the stack after a delay, if needed. 691 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 692 } 693 }; 694 695 /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */ 696 private OnClickListener mFlyoutClickListener = new OnClickListener() { 697 @Override 698 public void onClick(View view) { 699 if (maybeShowStackEdu()) { 700 // If we're showing user education, don't open the bubble show the education first 701 mBubbleToExpandAfterFlyoutCollapse = null; 702 } else { 703 mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble(); 704 } 705 706 mFlyout.removeCallbacks(mHideFlyout); 707 mHideFlyout.run(); 708 } 709 }; 710 711 /** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */ 712 private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() { 713 714 @Override 715 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { 716 mFlyout.removeCallbacks(mHideFlyout); 717 return true; 718 } 719 720 @Override 721 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 722 float viewInitialY, float dx, float dy) { 723 setFlyoutStateForDragLength(dx); 724 } 725 726 @Override 727 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 728 float viewInitialY, float dx, float dy, float velX, float velY) { 729 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 730 final boolean metRequiredVelocity = 731 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY; 732 final boolean metRequiredDeltaX = 733 onLeft 734 ? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS 735 : dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS; 736 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0; 737 final boolean shouldDismiss = metRequiredVelocity 738 || (metRequiredDeltaX && !isCancelFling); 739 740 mFlyout.removeCallbacks(mHideFlyout); 741 animateFlyoutCollapsed(shouldDismiss, velX); 742 743 maybeShowStackEdu(); 744 } 745 }; 746 747 private BubbleOverflow mBubbleOverflow; 748 private StackEducationView mStackEduView; 749 private ManageEducationView mManageEduView; 750 private DismissView mDismissView; 751 752 private ViewGroup mManageMenu; 753 private ImageView mManageSettingsIcon; 754 private TextView mManageSettingsText; 755 private boolean mShowingManage = false; 756 private boolean mShowedUserEducationInTouchListenerActive = false; 757 private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig( 758 SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); 759 private BubblePositioner mPositioner; 760 761 @SuppressLint("ClickableViewAccessibility") BubbleStackView(Context context, BubbleController bubbleController, BubbleData data, @Nullable SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, ShellExecutor mainExecutor)762 public BubbleStackView(Context context, BubbleController bubbleController, 763 BubbleData data, @Nullable SurfaceSynchronizer synchronizer, 764 FloatingContentCoordinator floatingContentCoordinator, 765 ShellExecutor mainExecutor) { 766 super(context); 767 768 mDelayedAnimationExecutor = mainExecutor; 769 mBubbleController = bubbleController; 770 mBubbleData = data; 771 772 Resources res = getResources(); 773 mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); 774 mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); 775 mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding); 776 777 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); 778 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); 779 780 mPositioner = mBubbleController.getPositioner(); 781 782 final TypedArray ta = mContext.obtainStyledAttributes( 783 new int[] {android.R.attr.dialogCornerRadius}); 784 mCornerRadius = ta.getDimensionPixelSize(0, 0); 785 ta.recycle(); 786 787 final Runnable onBubbleAnimatedOut = () -> { 788 if (getBubbleCount() == 0 && !mBubbleData.isShowingOverflow()) { 789 mBubbleController.onAllBubblesAnimatedOut(); 790 } 791 }; 792 mStackAnimationController = new StackAnimationController( 793 floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut, 794 this::animateShadows /* onStackAnimationFinished */, mPositioner); 795 796 mExpandedAnimationController = new ExpandedAnimationController(mPositioner, 797 onBubbleAnimatedOut, this); 798 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; 799 800 // Force LTR by default since most of the Bubbles UI is positioned manually by the user, or 801 // is centered. It greatly simplifies translation positioning/animations. Views that will 802 // actually lay out differently in RTL, such as the flyout and expanded view, will set their 803 // layout direction to LOCALE. 804 setLayoutDirection(LAYOUT_DIRECTION_LTR); 805 806 mBubbleContainer = new PhysicsAnimationLayout(context); 807 mBubbleContainer.setActiveController(mStackAnimationController); 808 mBubbleContainer.setElevation(elevation); 809 mBubbleContainer.setClipChildren(false); 810 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 811 812 mExpandedViewContainer = new FrameLayout(context); 813 mExpandedViewContainer.setElevation(elevation); 814 mExpandedViewContainer.setClipChildren(false); 815 addView(mExpandedViewContainer); 816 817 mAnimatingOutSurfaceContainer = new FrameLayout(getContext()); 818 mAnimatingOutSurfaceContainer.setLayoutParams( 819 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 820 addView(mAnimatingOutSurfaceContainer); 821 822 mAnimatingOutSurfaceView = new SurfaceView(getContext()); 823 mAnimatingOutSurfaceView.setUseAlpha(); 824 mAnimatingOutSurfaceView.setZOrderOnTop(true); 825 mAnimatingOutSurfaceView.setCornerRadius(mCornerRadius); 826 mAnimatingOutSurfaceView.setLayoutParams(new ViewGroup.LayoutParams(0, 0)); 827 mAnimatingOutSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { 828 @Override 829 public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {} 830 831 @Override 832 public void surfaceCreated(SurfaceHolder surfaceHolder) { 833 mAnimatingOutSurfaceReady = true; 834 } 835 836 @Override 837 public void surfaceDestroyed(SurfaceHolder surfaceHolder) { 838 mAnimatingOutSurfaceReady = false; 839 } 840 }); 841 mAnimatingOutSurfaceContainer.addView(mAnimatingOutSurfaceView); 842 843 mAnimatingOutSurfaceContainer.setPadding( 844 mExpandedViewContainer.getPaddingLeft(), 845 mExpandedViewContainer.getPaddingTop(), 846 mExpandedViewContainer.getPaddingRight(), 847 mExpandedViewContainer.getPaddingBottom()); 848 849 setUpManageMenu(); 850 851 setUpFlyout(); 852 mFlyoutTransitionSpring.setSpring(new SpringForce() 853 .setStiffness(SpringForce.STIFFNESS_LOW) 854 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); 855 mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring); 856 857 setUpDismissView(); 858 859 setClipChildren(false); 860 setFocusable(true); 861 mBubbleContainer.bringToFront(); 862 863 mBubbleOverflow = mBubbleData.getOverflow(); 864 mBubbleContainer.addView(mBubbleOverflow.getIconView(), 865 mBubbleContainer.getChildCount() /* index */, 866 new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), 867 mPositioner.getBubbleSize())); 868 updateOverflow(); 869 mBubbleOverflow.getIconView().setOnClickListener((View v) -> { 870 mBubbleData.setShowingOverflow(true); 871 mBubbleData.setSelectedBubble(mBubbleOverflow); 872 mBubbleData.setExpanded(true); 873 }); 874 875 mScrim = new View(getContext()); 876 mScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 877 mScrim.setBackgroundDrawable(new ColorDrawable( 878 getResources().getColor(android.R.color.system_neutral1_1000))); 879 addView(mScrim); 880 mScrim.setAlpha(0f); 881 882 mManageMenuScrim = new View(getContext()); 883 mManageMenuScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 884 mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( 885 getResources().getColor(android.R.color.system_neutral1_1000))); 886 addView(mManageMenuScrim, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 887 mManageMenuScrim.setAlpha(0f); 888 mManageMenuScrim.setVisibility(INVISIBLE); 889 890 mOrientationChangedListener = 891 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 892 mPositioner.update(); 893 onDisplaySizeChanged(); 894 mExpandedAnimationController.updateResources(); 895 mStackAnimationController.updateResources(); 896 mBubbleOverflow.updateResources(); 897 898 if (mRelativeStackPositionBeforeRotation != null) { 899 mStackAnimationController.setStackPosition( 900 mRelativeStackPositionBeforeRotation); 901 mRelativeStackPositionBeforeRotation = null; 902 } 903 904 if (mIsExpanded) { 905 // Re-draw bubble row and pointer for new orientation. 906 beforeExpandedViewAnimation(); 907 updateOverflowVisibility(); 908 updatePointerPosition(false /* forIme */); 909 mExpandedAnimationController.expandFromStack(() -> { 910 afterExpandedViewAnimation(); 911 showManageMenu(mShowingManage); 912 } /* after */); 913 final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, 914 getBubbleIndex(mExpandedBubble)); 915 mExpandedViewContainer.setTranslationX(0f); 916 mExpandedViewContainer.setTranslationY(translationY); 917 mExpandedViewContainer.setAlpha(1f); 918 } 919 removeOnLayoutChangeListener(mOrientationChangedListener); 920 }; 921 final float maxDismissSize = getResources().getDimensionPixelSize( 922 R.dimen.dismiss_circle_size); 923 final float minDismissSize = getResources().getDimensionPixelSize( 924 R.dimen.dismiss_circle_small); 925 final float sizePercent = minDismissSize / maxDismissSize; 926 mDismissBubbleAnimator = ValueAnimator.ofFloat(1f, 0f); 927 mDismissBubbleAnimator.addUpdateListener(animation -> { 928 final float animatedValue = (float) animation.getAnimatedValue(); 929 if (mDismissView != null) { 930 mDismissView.setPivotX((mDismissView.getRight() - mDismissView.getLeft()) / 2f); 931 mDismissView.setPivotY((mDismissView.getBottom() - mDismissView.getTop()) / 2f); 932 final float scaleValue = Math.max(animatedValue, sizePercent); 933 mDismissView.getCircle().setScaleX(scaleValue); 934 mDismissView.getCircle().setScaleY(scaleValue); 935 } 936 if (mViewBeingDismissed != null) { 937 mViewBeingDismissed.setAlpha(Math.max(animatedValue, 0.7f)); 938 } 939 }); 940 941 // If the stack itself is clicked, it means none of its touchable views (bubbles, flyouts, 942 // TaskView, etc.) were touched. Collapse the stack if it's expanded. 943 setOnClickListener(view -> { 944 if (mShowingManage) { 945 showManageMenu(false /* show */); 946 } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { 947 mManageEduView.hide(); 948 } else if (isStackEduShowing()) { 949 mStackEduView.hide(false /* isExpanding */); 950 } else if (mBubbleData.isExpanded()) { 951 mBubbleData.setExpanded(false); 952 } else { 953 maybeShowStackEdu(); 954 } 955 }); 956 957 animate() 958 .setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED) 959 .setDuration(FADE_IN_DURATION); 960 961 mExpandedViewAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION); 962 mExpandedViewAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); 963 mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() { 964 @Override 965 public void onAnimationStart(Animator animation) { 966 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 967 // We need to be Z ordered on top in order for alpha animations to work. 968 mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true); 969 mExpandedBubble.getExpandedView().setAlphaAnimating(true); 970 } 971 } 972 973 @Override 974 public void onAnimationEnd(Animator animation) { 975 if (mExpandedBubble != null 976 && mExpandedBubble.getExpandedView() != null 977 // The surface needs to be Z ordered on top for alpha values to work on the 978 // TaskView, and if we're temporarily hidden, we are still on the screen 979 // with alpha = 0f until we animate back. Stay Z ordered on top so the alpha 980 // = 0f remains in effect. 981 && !mExpandedViewTemporarilyHidden) { 982 mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); 983 mExpandedBubble.getExpandedView().setAlphaAnimating(false); 984 } 985 } 986 }); 987 mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> { 988 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 989 mExpandedBubble.getExpandedView().setTaskViewAlpha( 990 (float) valueAnimator.getAnimatedValue()); 991 } 992 }); 993 994 mAnimatingOutSurfaceAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION); 995 mAnimatingOutSurfaceAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); 996 mAnimatingOutSurfaceAlphaAnimator.addUpdateListener(valueAnimator -> { 997 if (!mExpandedViewTemporarilyHidden) { 998 mAnimatingOutSurfaceView.setAlpha((float) valueAnimator.getAnimatedValue()); 999 } 1000 }); 1001 mAnimatingOutSurfaceAlphaAnimator.addListener(new AnimatorListenerAdapter() { 1002 @Override 1003 public void onAnimationEnd(Animator animation) { 1004 releaseAnimatingOutBubbleBuffer(); 1005 } 1006 }); 1007 } 1008 1009 /** 1010 * Sets whether or not the stack should become temporarily invisible by moving off the side of 1011 * the screen. 1012 * 1013 * If a flyout comes in while it's invisible, it will animate back in while the flyout is 1014 * showing but disappear again when the flyout is gone. 1015 */ setTemporarilyInvisible(boolean invisible)1016 public void setTemporarilyInvisible(boolean invisible) { 1017 mTemporarilyInvisible = invisible; 1018 1019 // If we are animating out, hide immediately if possible so we animate out with the status 1020 // bar. 1021 updateTemporarilyInvisibleAnimation(invisible /* hideImmediately */); 1022 } 1023 1024 /** 1025 * Animates the stack to be temporarily invisible, if needed. 1026 * 1027 * If we're currently dragging the stack, or a flyout is visible, the stack will remain visible. 1028 * regardless of the value of {@link #mTemporarilyInvisible}. This method is called on ACTION_UP 1029 * as well as whenever a flyout hides, so we will animate invisible at that point if needed. 1030 */ updateTemporarilyInvisibleAnimation(boolean hideImmediately)1031 private void updateTemporarilyInvisibleAnimation(boolean hideImmediately) { 1032 removeCallbacks(mAnimateTemporarilyInvisibleImmediate); 1033 1034 if (mIsDraggingStack) { 1035 // If we're dragging the stack, don't animate it invisible. 1036 return; 1037 } 1038 1039 final boolean shouldHide = 1040 mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE; 1041 1042 postDelayed(mAnimateTemporarilyInvisibleImmediate, 1043 shouldHide && !hideImmediately ? ANIMATE_TEMPORARILY_INVISIBLE_DELAY : 0); 1044 } 1045 1046 private final Runnable mAnimateTemporarilyInvisibleImmediate = () -> { 1047 if (mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE) { 1048 if (mStackAnimationController.isStackOnLeftSide()) { 1049 animate().translationX(-mBubbleSize).start(); 1050 } else { 1051 animate().translationX(mBubbleSize).start(); 1052 } 1053 } else { 1054 animate().translationX(0).start(); 1055 } 1056 }; 1057 setUpDismissView()1058 private void setUpDismissView() { 1059 if (mDismissView != null) { 1060 removeView(mDismissView); 1061 } 1062 mDismissView = new DismissView(getContext()); 1063 int elevation = getResources().getDimensionPixelSize(R.dimen.bubble_elevation); 1064 1065 addView(mDismissView); 1066 mDismissView.setElevation(elevation); 1067 1068 final ContentResolver contentResolver = getContext().getContentResolver(); 1069 final int dismissRadius = Settings.Secure.getInt( 1070 contentResolver, "bubble_dismiss_radius", mBubbleSize * 2 /* default */); 1071 1072 // Save the MagneticTarget instance for the newly set up view - we'll add this to the 1073 // MagnetizedObjects when the dismiss view gets shown. 1074 mMagneticTarget = new MagnetizedObject.MagneticTarget( 1075 mDismissView.getCircle(), dismissRadius); 1076 mBubbleContainer.bringToFront(); 1077 } 1078 1079 // TODO: Create ManageMenuView and move setup / animations there setUpManageMenu()1080 private void setUpManageMenu() { 1081 if (mManageMenu != null) { 1082 removeView(mManageMenu); 1083 } 1084 1085 mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate( 1086 R.layout.bubble_manage_menu, this, false); 1087 mManageMenu.setVisibility(View.INVISIBLE); 1088 1089 PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig); 1090 1091 mManageMenu.setOutlineProvider(new ViewOutlineProvider() { 1092 @Override 1093 public void getOutline(View view, Outline outline) { 1094 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); 1095 } 1096 }); 1097 mManageMenu.setClipToOutline(true); 1098 1099 mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener( 1100 view -> { 1101 showManageMenu(false /* show */); 1102 dismissBubbleIfExists(mBubbleData.getSelectedBubble()); 1103 }); 1104 1105 mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener( 1106 view -> { 1107 showManageMenu(false /* show */); 1108 mUnbubbleConversationCallback.accept(mBubbleData.getSelectedBubble().getKey()); 1109 }); 1110 1111 mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener( 1112 view -> { 1113 showManageMenu(false /* show */); 1114 final BubbleViewProvider bubble = mBubbleData.getSelectedBubble(); 1115 if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { 1116 // If it's in the stack it's a proper Bubble. 1117 final Intent intent = ((Bubble) bubble).getSettingsIntent(mContext); 1118 mBubbleData.setExpanded(false); 1119 mContext.startActivityAsUser(intent, ((Bubble) bubble).getUser()); 1120 logBubbleEvent(bubble, 1121 FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); 1122 } 1123 }); 1124 1125 mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon); 1126 mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name); 1127 1128 // The menu itself should respect locale direction so the icons are on the correct side. 1129 mManageMenu.setLayoutDirection(LAYOUT_DIRECTION_LOCALE); 1130 addView(mManageMenu); 1131 } 1132 1133 /** 1134 * Whether the educational view should show for the expanded view "manage" menu. 1135 */ shouldShowManageEdu()1136 private boolean shouldShowManageEdu() { 1137 if (ActivityManager.isRunningInTestHarness()) { 1138 return false; 1139 } 1140 final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION); 1141 final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext)) 1142 && mExpandedBubble != null; 1143 if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { 1144 Log.d(TAG, "Show manage edu: " + shouldShow); 1145 } 1146 return shouldShow; 1147 } 1148 maybeShowManageEdu()1149 private void maybeShowManageEdu() { 1150 if (!shouldShowManageEdu()) { 1151 return; 1152 } 1153 if (mManageEduView == null) { 1154 mManageEduView = new ManageEducationView(mContext, mPositioner); 1155 addView(mManageEduView); 1156 } 1157 mManageEduView.show(mExpandedBubble.getExpandedView()); 1158 } 1159 1160 /** 1161 * Whether education view should show for the collapsed stack. 1162 */ shouldShowStackEdu()1163 private boolean shouldShowStackEdu() { 1164 if (ActivityManager.isRunningInTestHarness()) { 1165 return false; 1166 } 1167 final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION); 1168 final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext); 1169 if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { 1170 Log.d(TAG, "Show stack edu: " + shouldShow); 1171 } 1172 return shouldShow; 1173 } 1174 getPrefBoolean(String key)1175 private boolean getPrefBoolean(String key) { 1176 return mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE) 1177 .getBoolean(key, false /* default */); 1178 } 1179 1180 /** 1181 * @return true if education view for collapsed stack should show and was not showing before. 1182 */ maybeShowStackEdu()1183 private boolean maybeShowStackEdu() { 1184 if (!shouldShowStackEdu() || isExpanded()) { 1185 return false; 1186 } 1187 if (mStackEduView == null) { 1188 mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); 1189 addView(mStackEduView); 1190 } 1191 mBubbleContainer.bringToFront(); 1192 return mStackEduView.show(mPositioner.getDefaultStartPosition()); 1193 } 1194 isStackEduShowing()1195 private boolean isStackEduShowing() { 1196 return mStackEduView != null && mStackEduView.getVisibility() == VISIBLE; 1197 } 1198 1199 // Recreates & shows the education views. Call when a theme/config change happens. updateUserEdu()1200 private void updateUserEdu() { 1201 if (isStackEduShowing()) { 1202 removeView(mStackEduView); 1203 mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); 1204 addView(mStackEduView); 1205 mBubbleContainer.bringToFront(); // Stack appears on top of the stack education 1206 mStackEduView.show(mPositioner.getDefaultStartPosition()); 1207 } 1208 if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { 1209 removeView(mManageEduView); 1210 mManageEduView = new ManageEducationView(mContext, mPositioner); 1211 addView(mManageEduView); 1212 mManageEduView.show(mExpandedBubble.getExpandedView()); 1213 } 1214 } 1215 1216 @SuppressLint("ClickableViewAccessibility") setUpFlyout()1217 private void setUpFlyout() { 1218 if (mFlyout != null) { 1219 removeView(mFlyout); 1220 } 1221 mFlyout = new BubbleFlyoutView(getContext(), mPositioner); 1222 mFlyout.setVisibility(GONE); 1223 mFlyout.setOnClickListener(mFlyoutClickListener); 1224 mFlyout.setOnTouchListener(mFlyoutTouchListener); 1225 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 1226 } 1227 updateFontScale()1228 void updateFontScale() { 1229 setUpManageMenu(); 1230 mFlyout.updateFontSize(); 1231 for (Bubble b : mBubbleData.getBubbles()) { 1232 if (b.getExpandedView() != null) { 1233 b.getExpandedView().updateFontSize(); 1234 } 1235 } 1236 if (mBubbleOverflow != null) { 1237 mBubbleOverflow.getExpandedView().updateFontSize(); 1238 } 1239 } 1240 updateOverflow()1241 private void updateOverflow() { 1242 mBubbleOverflow.update(); 1243 mBubbleContainer.reorderView(mBubbleOverflow.getIconView(), 1244 mBubbleContainer.getChildCount() - 1 /* index */); 1245 updateOverflowVisibility(); 1246 } 1247 updateOverflowButtonDot()1248 void updateOverflowButtonDot() { 1249 for (Bubble b : mBubbleData.getOverflowBubbles()) { 1250 if (b.showDot()) { 1251 mBubbleOverflow.setShowDot(true); 1252 return; 1253 } 1254 } 1255 mBubbleOverflow.setShowDot(false); 1256 } 1257 1258 /** 1259 * Handle theme changes. 1260 */ onThemeChanged()1261 public void onThemeChanged() { 1262 setUpFlyout(); 1263 setUpManageMenu(); 1264 setUpDismissView(); 1265 updateOverflow(); 1266 updateUserEdu(); 1267 updateExpandedViewTheme(); 1268 mScrim.setBackgroundDrawable(new ColorDrawable( 1269 getResources().getColor(android.R.color.system_neutral1_1000))); 1270 mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( 1271 getResources().getColor(android.R.color.system_neutral1_1000))); 1272 } 1273 1274 /** 1275 * Respond to the phone being rotated by repositioning the stack and hiding any flyouts. 1276 * This is called prior to the rotation occurring, any values that should be updated 1277 * based on the new rotation should occur in {@link #mOrientationChangedListener}. 1278 */ onOrientationChanged()1279 public void onOrientationChanged() { 1280 mRelativeStackPositionBeforeRotation = new RelativeStackPosition( 1281 mPositioner.getRestingPosition(), 1282 mStackAnimationController.getAllowableStackPositionRegion()); 1283 addOnLayoutChangeListener(mOrientationChangedListener); 1284 hideFlyoutImmediate(); 1285 } 1286 1287 /** Tells the views with locale-dependent layout direction to resolve the new direction. */ onLayoutDirectionChanged(int direction)1288 public void onLayoutDirectionChanged(int direction) { 1289 mManageMenu.setLayoutDirection(direction); 1290 mFlyout.setLayoutDirection(direction); 1291 if (mStackEduView != null) { 1292 mStackEduView.setLayoutDirection(direction); 1293 } 1294 if (mManageEduView != null) { 1295 mManageEduView.setLayoutDirection(direction); 1296 } 1297 updateExpandedViewDirection(direction); 1298 } 1299 1300 /** Respond to the display size change by recalculating view size and location. */ onDisplaySizeChanged()1301 public void onDisplaySizeChanged() { 1302 updateOverflow(); 1303 setUpManageMenu(); 1304 setUpFlyout(); 1305 setUpDismissView(); 1306 updateUserEdu(); 1307 mBubbleSize = mPositioner.getBubbleSize(); 1308 for (Bubble b : mBubbleData.getBubbles()) { 1309 if (b.getIconView() == null) { 1310 Log.d(TAG, "Display size changed. Icon null: " + b); 1311 continue; 1312 } 1313 b.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize)); 1314 if (b.getExpandedView() != null) { 1315 b.getExpandedView().updateDimensions(); 1316 } 1317 } 1318 mBubbleOverflow.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize)); 1319 mExpandedAnimationController.updateResources(); 1320 mStackAnimationController.updateResources(); 1321 mDismissView.updateResources(); 1322 mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2); 1323 mStackAnimationController.setStackPosition( 1324 new RelativeStackPosition( 1325 mPositioner.getRestingPosition(), 1326 mStackAnimationController.getAllowableStackPositionRegion())); 1327 if (mIsExpanded) { 1328 updateExpandedView(); 1329 } 1330 } 1331 1332 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)1333 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 1334 inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 1335 1336 mTempRect.setEmpty(); 1337 getTouchableRegion(mTempRect); 1338 inoutInfo.touchableRegion.set(mTempRect); 1339 } 1340 1341 @Override onAttachedToWindow()1342 protected void onAttachedToWindow() { 1343 super.onAttachedToWindow(); 1344 mPositioner.update(); 1345 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 1346 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); 1347 } 1348 1349 @Override onDetachedFromWindow()1350 protected void onDetachedFromWindow() { 1351 super.onDetachedFromWindow(); 1352 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); 1353 getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater); 1354 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 1355 if (mBubbleOverflow != null) { 1356 mBubbleOverflow.cleanUpExpandedState(); 1357 } 1358 } 1359 1360 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)1361 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 1362 super.onInitializeAccessibilityNodeInfoInternal(info); 1363 setupLocalMenu(info); 1364 } 1365 updateExpandedViewTheme()1366 void updateExpandedViewTheme() { 1367 final List<Bubble> bubbles = mBubbleData.getBubbles(); 1368 if (bubbles.isEmpty()) { 1369 return; 1370 } 1371 bubbles.forEach(bubble -> { 1372 if (bubble.getExpandedView() != null) { 1373 bubble.getExpandedView().applyThemeAttrs(); 1374 } 1375 }); 1376 } 1377 updateExpandedViewDirection(int direction)1378 void updateExpandedViewDirection(int direction) { 1379 final List<Bubble> bubbles = mBubbleData.getBubbles(); 1380 if (bubbles.isEmpty()) { 1381 return; 1382 } 1383 bubbles.forEach(bubble -> { 1384 if (bubble.getExpandedView() != null) { 1385 bubble.getExpandedView().setLayoutDirection(direction); 1386 } 1387 }); 1388 } 1389 setupLocalMenu(AccessibilityNodeInfo info)1390 void setupLocalMenu(AccessibilityNodeInfo info) { 1391 Resources res = mContext.getResources(); 1392 1393 // Custom local actions. 1394 AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left, 1395 res.getString(R.string.bubble_accessibility_action_move_top_left)); 1396 info.addAction(moveTopLeft); 1397 1398 AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right, 1399 res.getString(R.string.bubble_accessibility_action_move_top_right)); 1400 info.addAction(moveTopRight); 1401 1402 AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left, 1403 res.getString(R.string.bubble_accessibility_action_move_bottom_left)); 1404 info.addAction(moveBottomLeft); 1405 1406 AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right, 1407 res.getString(R.string.bubble_accessibility_action_move_bottom_right)); 1408 info.addAction(moveBottomRight); 1409 1410 // Default actions. 1411 info.addAction(AccessibilityAction.ACTION_DISMISS); 1412 if (mIsExpanded) { 1413 info.addAction(AccessibilityAction.ACTION_COLLAPSE); 1414 } else { 1415 info.addAction(AccessibilityAction.ACTION_EXPAND); 1416 } 1417 } 1418 1419 @Override performAccessibilityActionInternal(int action, Bundle arguments)1420 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 1421 if (super.performAccessibilityActionInternal(action, arguments)) { 1422 return true; 1423 } 1424 final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion(); 1425 1426 // R constants are not final so we cannot use switch-case here. 1427 if (action == AccessibilityNodeInfo.ACTION_DISMISS) { 1428 mBubbleData.dismissAll(Bubbles.DISMISS_ACCESSIBILITY_ACTION); 1429 announceForAccessibility( 1430 getResources().getString(R.string.accessibility_bubble_dismissed)); 1431 return true; 1432 } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { 1433 mBubbleData.setExpanded(false); 1434 return true; 1435 } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) { 1436 mBubbleData.setExpanded(true); 1437 return true; 1438 } else if (action == R.id.action_move_top_left) { 1439 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top); 1440 return true; 1441 } else if (action == R.id.action_move_top_right) { 1442 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top); 1443 return true; 1444 } else if (action == R.id.action_move_bottom_left) { 1445 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom); 1446 return true; 1447 } else if (action == R.id.action_move_bottom_right) { 1448 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom); 1449 return true; 1450 } 1451 return false; 1452 } 1453 1454 /** 1455 * Update content description for a11y TalkBack. 1456 */ updateContentDescription()1457 public void updateContentDescription() { 1458 if (mBubbleData.getBubbles().isEmpty()) { 1459 return; 1460 } 1461 1462 for (int i = 0; i < mBubbleData.getBubbles().size(); i++) { 1463 final Bubble bubble = mBubbleData.getBubbles().get(i); 1464 final String appName = bubble.getAppName(); 1465 1466 String titleStr = bubble.getTitle(); 1467 if (titleStr == null) { 1468 titleStr = getResources().getString(R.string.notification_bubble_title); 1469 } 1470 1471 if (bubble.getIconView() != null) { 1472 if (mIsExpanded || i > 0) { 1473 bubble.getIconView().setContentDescription(getResources().getString( 1474 R.string.bubble_content_description_single, titleStr, appName)); 1475 } else { 1476 final int moreCount = mBubbleContainer.getChildCount() - 1; 1477 bubble.getIconView().setContentDescription(getResources().getString( 1478 R.string.bubble_content_description_stack, 1479 titleStr, appName, moreCount)); 1480 } 1481 } 1482 } 1483 } 1484 updateSystemGestureExcludeRects()1485 private void updateSystemGestureExcludeRects() { 1486 // Exclude the region occupied by the first BubbleView in the stack 1487 Rect excludeZone = mSystemGestureExclusionRects.get(0); 1488 if (getBubbleCount() > 0) { 1489 View firstBubble = mBubbleContainer.getChildAt(0); 1490 excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(), 1491 firstBubble.getBottom()); 1492 excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f), 1493 (int) (firstBubble.getTranslationY() + 0.5f)); 1494 mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects); 1495 } else { 1496 excludeZone.setEmpty(); 1497 mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList()); 1498 } 1499 } 1500 1501 /** 1502 * Sets the listener to notify when the bubble stack is expanded. 1503 */ setExpandListener(Bubbles.BubbleExpandListener listener)1504 public void setExpandListener(Bubbles.BubbleExpandListener listener) { 1505 mExpandListener = listener; 1506 } 1507 1508 /** Sets the function to call to un-bubble the given conversation. */ setUnbubbleConversationCallback( Consumer<String> unbubbleConversationCallback)1509 public void setUnbubbleConversationCallback( 1510 Consumer<String> unbubbleConversationCallback) { 1511 mUnbubbleConversationCallback = unbubbleConversationCallback; 1512 } 1513 1514 /** 1515 * Whether the stack of bubbles is expanded or not. 1516 */ isExpanded()1517 public boolean isExpanded() { 1518 return mIsExpanded; 1519 } 1520 1521 /** 1522 * Whether the stack of bubbles is animating to or from expansion. 1523 */ isExpansionAnimating()1524 public boolean isExpansionAnimating() { 1525 return mIsExpansionAnimating; 1526 } 1527 1528 /** 1529 * The {@link Bubble} that is expanded, null if one does not exist. 1530 */ 1531 @VisibleForTesting 1532 @Nullable getExpandedBubble()1533 public BubbleViewProvider getExpandedBubble() { 1534 return mExpandedBubble; 1535 } 1536 1537 // via BubbleData.Listener 1538 @SuppressLint("ClickableViewAccessibility") addBubble(Bubble bubble)1539 void addBubble(Bubble bubble) { 1540 if (DEBUG_BUBBLE_STACK_VIEW) { 1541 Log.d(TAG, "addBubble: " + bubble); 1542 } 1543 1544 if (getBubbleCount() == 0 && shouldShowStackEdu()) { 1545 // Override the default stack position if we're showing user education. 1546 mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition()); 1547 } 1548 1549 if (bubble.getIconView() == null) { 1550 return; 1551 } 1552 1553 mBubbleContainer.addView(bubble.getIconView(), 0, 1554 new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), 1555 mPositioner.getBubbleSize())); 1556 1557 if (getBubbleCount() == 0) { 1558 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); 1559 } 1560 // Set the dot position to the opposite of the side the stack is resting on, since the stack 1561 // resting slightly off-screen would result in the dot also being off-screen. 1562 bubble.getIconView().setDotBadgeOnLeft(!mStackOnLeftOrWillBe /* onLeft */); 1563 bubble.getIconView().setOnClickListener(mBubbleClickListener); 1564 bubble.getIconView().setOnTouchListener(mBubbleTouchListener); 1565 updateBubbleShadows(false /* showForAllBubbles */); 1566 animateInFlyoutForBubble(bubble); 1567 requestUpdate(); 1568 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__POSTED); 1569 } 1570 1571 // via BubbleData.Listener removeBubble(Bubble bubble)1572 void removeBubble(Bubble bubble) { 1573 if (DEBUG_BUBBLE_STACK_VIEW) { 1574 Log.d(TAG, "removeBubble: " + bubble); 1575 } 1576 // Remove it from the views 1577 for (int i = 0; i < getBubbleCount(); i++) { 1578 View v = mBubbleContainer.getChildAt(i); 1579 if (v instanceof BadgedImageView 1580 && ((BadgedImageView) v).getKey().equals(bubble.getKey())) { 1581 mBubbleContainer.removeViewAt(i); 1582 if (mBubbleData.hasOverflowBubbleWithKey(bubble.getKey())) { 1583 bubble.cleanupExpandedView(); 1584 } else { 1585 bubble.cleanupViews(); 1586 } 1587 updatePointerPosition(false /* forIme */); 1588 updateExpandedView(); 1589 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); 1590 return; 1591 } 1592 } 1593 Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); 1594 } 1595 updateOverflowVisibility()1596 private void updateOverflowVisibility() { 1597 mBubbleOverflow.setVisible((mIsExpanded || mBubbleData.isShowingOverflow()) 1598 ? VISIBLE 1599 : GONE); 1600 } 1601 1602 // via BubbleData.Listener updateBubble(Bubble bubble)1603 void updateBubble(Bubble bubble) { 1604 animateInFlyoutForBubble(bubble); 1605 requestUpdate(); 1606 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED); 1607 } 1608 1609 /** 1610 * Update bubble order and pointer position. 1611 */ updateBubbleOrder(List<Bubble> bubbles)1612 public void updateBubbleOrder(List<Bubble> bubbles) { 1613 final Runnable reorder = () -> { 1614 for (int i = 0; i < bubbles.size(); i++) { 1615 Bubble bubble = bubbles.get(i); 1616 mBubbleContainer.reorderView(bubble.getIconView(), i); 1617 } 1618 }; 1619 if (mIsExpanded || isExpansionAnimating()) { 1620 reorder.run(); 1621 updateBadges(false /* setBadgeForCollapsedStack */); 1622 updateZOrder(); 1623 } else if (!isExpansionAnimating()) { 1624 List<View> bubbleViews = bubbles.stream() 1625 .map(b -> b.getIconView()).collect(Collectors.toList()); 1626 mStackAnimationController.animateReorder(bubbleViews, reorder); 1627 } 1628 updatePointerPosition(false /* forIme */); 1629 } 1630 1631 /** 1632 * Changes the currently selected bubble. If the stack is already expanded, the newly selected 1633 * bubble will be shown immediately. This does not change the expanded state or change the 1634 * position of any bubble. 1635 */ 1636 // via BubbleData.Listener setSelectedBubble(@ullable BubbleViewProvider bubbleToSelect)1637 public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) { 1638 if (DEBUG_BUBBLE_STACK_VIEW) { 1639 Log.d(TAG, "setSelectedBubble: " + bubbleToSelect); 1640 } 1641 1642 if (bubbleToSelect == null) { 1643 mBubbleData.setShowingOverflow(false); 1644 return; 1645 } 1646 1647 // Ignore this new bubble only if it is the exact same bubble object. Otherwise, we'll want 1648 // to re-render it even if it has the same key (equals() returns true). If the currently 1649 // expanded bubble is removed and instantly re-added, we'll get back a new Bubble instance 1650 // with the same key (with newly inflated expanded views), and we need to render those new 1651 // views. 1652 if (mExpandedBubble == bubbleToSelect) { 1653 return; 1654 } 1655 1656 if (bubbleToSelect.getKey().equals(BubbleOverflow.KEY)) { 1657 mBubbleData.setShowingOverflow(true); 1658 } else { 1659 mBubbleData.setShowingOverflow(false); 1660 } 1661 1662 if (mIsExpanded && mIsExpansionAnimating) { 1663 // If the bubble selection changed during the expansion animation, the expanding bubble 1664 // probably crashed or immediately removed itself (or, we just got unlucky with a new 1665 // auto-expanding bubble showing up at just the right time). Cancel the animations so we 1666 // can start fresh. 1667 cancelAllExpandCollapseSwitchAnimations(); 1668 } 1669 showManageMenu(false /* show */); 1670 1671 // If we're expanded, screenshot the currently expanded bubble (before expanding the newly 1672 // selected bubble) so we can animate it out. 1673 if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null 1674 && !mExpandedViewTemporarilyHidden) { 1675 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 1676 // Before screenshotting, have the real TaskView show on top of other surfaces 1677 // so that the screenshot doesn't flicker on top of it. 1678 mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true); 1679 } 1680 1681 try { 1682 screenshotAnimatingOutBubbleIntoSurface((success) -> { 1683 mAnimatingOutSurfaceContainer.setVisibility( 1684 success ? View.VISIBLE : View.INVISIBLE); 1685 showNewlySelectedBubble(bubbleToSelect); 1686 }); 1687 } catch (Exception e) { 1688 showNewlySelectedBubble(bubbleToSelect); 1689 e.printStackTrace(); 1690 } 1691 } else { 1692 showNewlySelectedBubble(bubbleToSelect); 1693 } 1694 } 1695 showNewlySelectedBubble(BubbleViewProvider bubbleToSelect)1696 private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) { 1697 final BubbleViewProvider previouslySelected = mExpandedBubble; 1698 mExpandedBubble = bubbleToSelect; 1699 1700 if (mIsExpanded) { 1701 hideCurrentInputMethod(); 1702 1703 // Make the container of the expanded view transparent before removing the expanded view 1704 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the 1705 // expanded view becomes visible on the screen. See b/126856255 1706 mExpandedViewContainer.setAlpha(0.0f); 1707 mSurfaceSynchronizer.syncSurfaceAndRun(() -> { 1708 if (previouslySelected != null) { 1709 previouslySelected.setTaskViewVisibility(false); 1710 } 1711 1712 updateExpandedBubble(); 1713 requestUpdate(); 1714 1715 logBubbleEvent(previouslySelected, 1716 FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); 1717 logBubbleEvent(bubbleToSelect, 1718 FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); 1719 notifyExpansionChanged(previouslySelected, false /* expanded */); 1720 notifyExpansionChanged(bubbleToSelect, true /* expanded */); 1721 }); 1722 } 1723 } 1724 1725 /** 1726 * Changes the expanded state of the stack. 1727 * Don't call this directly, call mBubbleData#setExpanded. 1728 * 1729 * @param shouldExpand whether the bubble stack should appear expanded 1730 */ 1731 // via BubbleData.Listener setExpanded(boolean shouldExpand)1732 public void setExpanded(boolean shouldExpand) { 1733 if (DEBUG_BUBBLE_STACK_VIEW) { 1734 Log.d(TAG, "setExpanded: " + shouldExpand); 1735 } 1736 1737 if (!shouldExpand) { 1738 // If we're collapsing, release the animating-out surface immediately since we have no 1739 // need for it, and this ensures it cannot remain visible as we collapse. 1740 releaseAnimatingOutBubbleBuffer(); 1741 } 1742 1743 if (shouldExpand == mIsExpanded) { 1744 return; 1745 } 1746 1747 hideCurrentInputMethod(); 1748 1749 mBubbleController.getSysuiProxy().onStackExpandChanged(shouldExpand); 1750 1751 if (mIsExpanded) { 1752 animateCollapse(); 1753 logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); 1754 } else { 1755 animateExpansion(); 1756 // TODO: move next line to BubbleData 1757 logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); 1758 logBubbleEvent(mExpandedBubble, 1759 FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); 1760 } 1761 notifyExpansionChanged(mExpandedBubble, mIsExpanded); 1762 } 1763 1764 /** 1765 * Called when back press occurs while bubbles are expanded. 1766 */ onBackPressed()1767 public void onBackPressed() { 1768 if (mIsExpanded) { 1769 if (mShowingManage) { 1770 showManageMenu(false); 1771 } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { 1772 mManageEduView.hide(); 1773 } else { 1774 mBubbleData.setExpanded(false); 1775 } 1776 } 1777 } 1778 setBubbleVisibility(Bubble b, boolean visible)1779 void setBubbleVisibility(Bubble b, boolean visible) { 1780 if (b.getIconView() != null) { 1781 b.getIconView().setVisibility(visible ? VISIBLE : GONE); 1782 } 1783 // TODO(b/181166384): Animate in / out & handle adjusting how the bubbles overlap 1784 } 1785 1786 /** 1787 * Asks the BubbleController to hide the IME from anywhere, whether it's focused on Bubbles or 1788 * not. 1789 */ hideCurrentInputMethod()1790 void hideCurrentInputMethod() { 1791 mPositioner.setImeVisible(false, 0); 1792 mBubbleController.hideCurrentInputMethod(); 1793 } 1794 1795 /** Set the stack position to whatever the positioner says. */ updateStackPosition()1796 void updateStackPosition() { 1797 mStackAnimationController.setStackPosition(mPositioner.getRestingPosition()); 1798 mDismissView.hide(); 1799 } 1800 beforeExpandedViewAnimation()1801 private void beforeExpandedViewAnimation() { 1802 mIsExpansionAnimating = true; 1803 hideFlyoutImmediate(); 1804 updateExpandedBubble(); 1805 updateExpandedView(); 1806 } 1807 afterExpandedViewAnimation()1808 private void afterExpandedViewAnimation() { 1809 mIsExpansionAnimating = false; 1810 updateExpandedView(); 1811 requestUpdate(); 1812 } 1813 1814 /** Animate the expanded view hidden. This is done while we're dragging out a bubble. */ hideExpandedViewIfNeeded()1815 private void hideExpandedViewIfNeeded() { 1816 if (mExpandedViewTemporarilyHidden 1817 || mExpandedBubble == null 1818 || mExpandedBubble.getExpandedView() == null) { 1819 return; 1820 } 1821 1822 mExpandedViewTemporarilyHidden = true; 1823 1824 // Scale down. 1825 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 1826 .spring(AnimatableScaleMatrix.SCALE_X, 1827 AnimatableScaleMatrix.getAnimatableValueForScaleFactor( 1828 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), 1829 mScaleOutSpringConfig) 1830 .spring(AnimatableScaleMatrix.SCALE_Y, 1831 AnimatableScaleMatrix.getAnimatableValueForScaleFactor( 1832 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), 1833 mScaleOutSpringConfig) 1834 .addUpdateListener((target, values) -> 1835 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix)) 1836 .start(); 1837 1838 // Animate alpha from 1f to 0f. 1839 mExpandedViewAlphaAnimator.reverse(); 1840 } 1841 1842 /** 1843 * Animate the expanded view visible again. This is done when we're done dragging out a bubble. 1844 */ showExpandedViewIfNeeded()1845 private void showExpandedViewIfNeeded() { 1846 if (!mExpandedViewTemporarilyHidden) { 1847 return; 1848 } 1849 1850 mExpandedViewTemporarilyHidden = false; 1851 1852 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 1853 .spring(AnimatableScaleMatrix.SCALE_X, 1854 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 1855 mScaleOutSpringConfig) 1856 .spring(AnimatableScaleMatrix.SCALE_Y, 1857 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 1858 mScaleOutSpringConfig) 1859 .addUpdateListener((target, values) -> 1860 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix)) 1861 .start(); 1862 1863 mExpandedViewAlphaAnimator.start(); 1864 } 1865 showScrim(boolean show)1866 private void showScrim(boolean show) { 1867 if (show) { 1868 mScrim.animate() 1869 .setInterpolator(ALPHA_IN) 1870 .alpha(SCRIM_ALPHA) 1871 .start(); 1872 } else { 1873 mScrim.animate() 1874 .alpha(0f) 1875 .setInterpolator(ALPHA_OUT) 1876 .start(); 1877 } 1878 } 1879 animateExpansion()1880 private void animateExpansion() { 1881 cancelDelayedExpandCollapseSwitchAnimations(); 1882 final boolean showVertically = mPositioner.showBubblesVertically(); 1883 mIsExpanded = true; 1884 if (isStackEduShowing()) { 1885 mStackEduView.hide(true /* fromExpansion */); 1886 } 1887 beforeExpandedViewAnimation(); 1888 1889 showScrim(true); 1890 updateZOrder(); 1891 updateBadges(false /* setBadgeForCollapsedStack */); 1892 mBubbleContainer.setActiveController(mExpandedAnimationController); 1893 updateOverflowVisibility(); 1894 updatePointerPosition(false /* forIme */); 1895 mExpandedAnimationController.expandFromStack(() -> { 1896 if (mIsExpanded && mExpandedBubble.getExpandedView() != null) { 1897 maybeShowManageEdu(); 1898 } 1899 } /* after */); 1900 int index; 1901 if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { 1902 index = mBubbleData.getBubbles().size(); 1903 } else { 1904 index = getBubbleIndex(mExpandedBubble); 1905 } 1906 PointF p = mPositioner.getExpandedBubbleXY(index, getState()); 1907 final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, 1908 mPositioner.showBubblesVertically() ? p.y : p.x); 1909 mExpandedViewContainer.setTranslationX(0f); 1910 mExpandedViewContainer.setTranslationY(translationY); 1911 mExpandedViewContainer.setAlpha(1f); 1912 1913 // How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles 1914 // that are animating farther, so that the expanded view doesn't move as much. 1915 final float relevantStackPosition = showVertically 1916 ? mStackAnimationController.getStackPosition().y 1917 : mStackAnimationController.getStackPosition().x; 1918 final float bubbleWillBeAt = showVertically 1919 ? p.y 1920 : p.x; 1921 final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition); 1922 1923 // Wait for the path animation target to reach its end, and add a small amount of extra time 1924 // if the bubble is moving a lot horizontally. 1925 long startDelay = 0L; 1926 1927 // Should not happen since we lay out before expanding, but just in case... 1928 if (getWidth() > 0) { 1929 startDelay = (long) 1930 (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 1.2f 1931 + (distanceAnimated / getWidth()) * 30); 1932 } 1933 1934 // Set the pivot point for the scale, so the expanded view animates out from the bubble. 1935 if (showVertically) { 1936 float pivotX; 1937 if (mStackOnLeftOrWillBe) { 1938 pivotX = p.x + mBubbleSize + mExpandedViewPadding; 1939 } else { 1940 pivotX = p.x - mExpandedViewPadding; 1941 } 1942 mExpandedViewContainerMatrix.setScale( 1943 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1944 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1945 pivotX, 1946 p.y + mBubbleSize / 2f); 1947 } else { 1948 mExpandedViewContainerMatrix.setScale( 1949 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1950 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1951 p.x + mBubbleSize / 2f, 1952 p.y + mBubbleSize + mExpandedViewPadding); 1953 } 1954 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 1955 1956 if (mExpandedBubble.getExpandedView() != null) { 1957 mExpandedBubble.getExpandedView().setTaskViewAlpha(0f); 1958 1959 // We'll be starting the alpha animation after a slight delay, so set this flag early 1960 // here. 1961 mExpandedBubble.getExpandedView().setAlphaAnimating(true); 1962 } 1963 1964 mDelayedAnimation = () -> { 1965 mExpandedViewAlphaAnimator.start(); 1966 1967 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 1968 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 1969 .spring(AnimatableScaleMatrix.SCALE_X, 1970 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 1971 mScaleInSpringConfig) 1972 .spring(AnimatableScaleMatrix.SCALE_Y, 1973 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 1974 mScaleInSpringConfig) 1975 .addUpdateListener((target, values) -> { 1976 if (mExpandedBubble == null || mExpandedBubble.getIconView() == null) { 1977 return; 1978 } 1979 float translation = showVertically 1980 ? mExpandedBubble.getIconView().getTranslationY() 1981 : mExpandedBubble.getIconView().getTranslationX(); 1982 mExpandedViewContainerMatrix.postTranslate( 1983 translation - bubbleWillBeAt, 1984 0); 1985 mExpandedViewContainer.setAnimationMatrix( 1986 mExpandedViewContainerMatrix); 1987 }) 1988 .withEndActions(() -> { 1989 mExpandedViewContainer.setAnimationMatrix(null); 1990 afterExpandedViewAnimation(); 1991 if (mExpandedBubble != null 1992 && mExpandedBubble.getExpandedView() != null) { 1993 mExpandedBubble.getExpandedView() 1994 .setSurfaceZOrderedOnTop(false); 1995 } 1996 }) 1997 .start(); 1998 }; 1999 mDelayedAnimationExecutor.executeDelayed(mDelayedAnimation, startDelay); 2000 } 2001 animateCollapse()2002 private void animateCollapse() { 2003 cancelDelayedExpandCollapseSwitchAnimations(); 2004 2005 if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { 2006 mManageEduView.hide(); 2007 } 2008 // Hide the menu if it's visible. 2009 showManageMenu(false); 2010 2011 mIsExpanded = false; 2012 mIsExpansionAnimating = true; 2013 2014 showScrim(false); 2015 2016 mBubbleContainer.cancelAllAnimations(); 2017 2018 // If we were in the middle of swapping, the animating-out surface would have been scaling 2019 // to zero - finish it off. 2020 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); 2021 mAnimatingOutSurfaceContainer.setScaleX(0f); 2022 mAnimatingOutSurfaceContainer.setScaleY(0f); 2023 2024 // Let the expanded animation controller know that it shouldn't animate child adds/reorders 2025 // since we're about to animate collapsed. 2026 mExpandedAnimationController.notifyPreparingToCollapse(); 2027 2028 mExpandedAnimationController.collapseBackToStack( 2029 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge() 2030 /* collapseTo */, 2031 () -> mBubbleContainer.setActiveController(mStackAnimationController)); 2032 2033 int index; 2034 if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { 2035 index = mBubbleData.getBubbles().size(); 2036 } else { 2037 index = mBubbleData.getBubbles().indexOf(mExpandedBubble); 2038 } 2039 // Value the bubble is animating from (back into the stack). 2040 final PointF p = mPositioner.getExpandedBubbleXY(index, getState()); 2041 if (mPositioner.showBubblesVertically()) { 2042 float pivotX; 2043 float pivotY = p.y + mBubbleSize / 2f; 2044 if (mStackOnLeftOrWillBe) { 2045 pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; 2046 } else { 2047 pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding; 2048 } 2049 mExpandedViewContainerMatrix.setScale( 2050 1f, 1f, 2051 pivotX, pivotY); 2052 } else { 2053 mExpandedViewContainerMatrix.setScale( 2054 1f, 1f, 2055 p.x + mBubbleSize / 2f, 2056 p.y + mBubbleSize + mExpandedViewPadding); 2057 } 2058 2059 mExpandedViewAlphaAnimator.reverse(); 2060 2061 // When the animation completes, we should no longer be showing the content. 2062 if (mExpandedBubble.getExpandedView() != null) { 2063 mExpandedBubble.getExpandedView().setContentVisibility(false); 2064 } 2065 2066 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 2067 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 2068 .spring(AnimatableScaleMatrix.SCALE_X, 2069 AnimatableScaleMatrix.getAnimatableValueForScaleFactor( 2070 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), 2071 mScaleOutSpringConfig) 2072 .spring(AnimatableScaleMatrix.SCALE_Y, 2073 AnimatableScaleMatrix.getAnimatableValueForScaleFactor( 2074 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), 2075 mScaleOutSpringConfig) 2076 .addUpdateListener((target, values) -> { 2077 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2078 }) 2079 .withEndActions(() -> { 2080 final BubbleViewProvider previouslySelected = mExpandedBubble; 2081 beforeExpandedViewAnimation(); 2082 if (mManageEduView != null) { 2083 mManageEduView.hide(); 2084 } 2085 2086 if (DEBUG_BUBBLE_STACK_VIEW) { 2087 Log.d(TAG, "animateCollapse"); 2088 Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(), 2089 mExpandedBubble)); 2090 } 2091 updateOverflowVisibility(); 2092 updateZOrder(); 2093 updateBadges(true /* setBadgeForCollapsedStack */); 2094 afterExpandedViewAnimation(); 2095 if (previouslySelected != null) { 2096 previouslySelected.setTaskViewVisibility(false); 2097 } 2098 }) 2099 .start(); 2100 } 2101 animateSwitchBubbles()2102 private void animateSwitchBubbles() { 2103 // If we're no longer expanded, this is meaningless. 2104 if (!mIsExpanded) { 2105 return; 2106 } 2107 2108 mIsBubbleSwitchAnimating = true; 2109 2110 // The surface contains a screenshot of the animating out bubble, so we just need to animate 2111 // it out (and then release the GraphicBuffer). 2112 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); 2113 2114 mAnimatingOutSurfaceAlphaAnimator.reverse(); 2115 mExpandedViewAlphaAnimator.start(); 2116 2117 if (mPositioner.showBubblesVertically()) { 2118 float translationX = mStackAnimationController.isStackOnLeftSide() 2119 ? mAnimatingOutSurfaceContainer.getTranslationX() + mBubbleSize * 2 2120 : mAnimatingOutSurfaceContainer.getTranslationX(); 2121 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) 2122 .spring(DynamicAnimation.TRANSLATION_X, translationX, mTranslateSpringConfig) 2123 .start(); 2124 } else { 2125 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) 2126 .spring(DynamicAnimation.TRANSLATION_Y, 2127 mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize, 2128 mTranslateSpringConfig) 2129 .start(); 2130 } 2131 2132 boolean isOverflow = mExpandedBubble != null 2133 && mExpandedBubble.getKey().equals(BubbleOverflow.KEY); 2134 PointF p = mPositioner.getExpandedBubbleXY(isOverflow 2135 ? mBubbleContainer.getChildCount() - 1 2136 : mBubbleData.getBubbles().indexOf(mExpandedBubble), 2137 getState()); 2138 mExpandedViewContainer.setAlpha(1f); 2139 mExpandedViewContainer.setVisibility(View.VISIBLE); 2140 2141 if (mPositioner.showBubblesVertically()) { 2142 float pivotX; 2143 float pivotY = p.y + mBubbleSize / 2f; 2144 if (mStackOnLeftOrWillBe) { 2145 pivotX = p.x + mBubbleSize + mExpandedViewPadding; 2146 } else { 2147 pivotX = p.x - mExpandedViewPadding; 2148 } 2149 mExpandedViewContainerMatrix.setScale( 2150 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2151 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2152 pivotX, pivotY); 2153 } else { 2154 mExpandedViewContainerMatrix.setScale( 2155 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2156 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2157 p.x + mBubbleSize / 2f, 2158 p.y + mBubbleSize + mExpandedViewPadding); 2159 } 2160 2161 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2162 2163 mDelayedAnimationExecutor.executeDelayed(() -> { 2164 if (!mIsExpanded) { 2165 mIsBubbleSwitchAnimating = false; 2166 return; 2167 } 2168 2169 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 2170 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 2171 .spring(AnimatableScaleMatrix.SCALE_X, 2172 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2173 mScaleInSpringConfig) 2174 .spring(AnimatableScaleMatrix.SCALE_Y, 2175 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2176 mScaleInSpringConfig) 2177 .addUpdateListener((target, values) -> { 2178 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2179 }) 2180 .withEndActions(() -> { 2181 mExpandedViewTemporarilyHidden = false; 2182 mIsBubbleSwitchAnimating = false; 2183 mExpandedViewContainer.setAnimationMatrix(null); 2184 }) 2185 .start(); 2186 }, 25); 2187 } 2188 2189 /** 2190 * Cancels any delayed steps for expand/collapse and bubble switch animations, and resets the is 2191 * animating flags for those animations. 2192 */ cancelDelayedExpandCollapseSwitchAnimations()2193 private void cancelDelayedExpandCollapseSwitchAnimations() { 2194 mDelayedAnimationExecutor.removeCallbacks(mDelayedAnimation); 2195 2196 mIsExpansionAnimating = false; 2197 mIsBubbleSwitchAnimating = false; 2198 } 2199 cancelAllExpandCollapseSwitchAnimations()2200 private void cancelAllExpandCollapseSwitchAnimations() { 2201 cancelDelayedExpandCollapseSwitchAnimations(); 2202 2203 PhysicsAnimator.getInstance(mAnimatingOutSurfaceView).cancel(); 2204 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 2205 2206 mExpandedViewContainer.setAnimationMatrix(null); 2207 } 2208 notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded)2209 private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) { 2210 if (mExpandListener != null && bubble != null) { 2211 mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey()); 2212 } 2213 } 2214 2215 /** 2216 * Updates the stack based for IME changes. When collapsed it'll move the stack if it 2217 * overlaps where they IME would be. When expanded it'll shift the expanded bubbles 2218 * if they might overlap with the IME (this only happens for large screens). 2219 */ animateForIme(boolean visible)2220 public void animateForIme(boolean visible) { 2221 if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) { 2222 // This will update the animation so the bubbles move to position for the IME 2223 mExpandedAnimationController.expandFromStack(() -> { 2224 updatePointerPosition(false /* forIme */); 2225 afterExpandedViewAnimation(); 2226 } /* after */); 2227 return; 2228 } 2229 2230 if (!mIsExpanded && getBubbleCount() > 0) { 2231 final float stackDestinationY = 2232 mStackAnimationController.animateForImeVisibility(visible); 2233 2234 // How far the stack is animating due to IME, we'll just animate the flyout by that 2235 // much too. 2236 final float stackDy = 2237 stackDestinationY - mStackAnimationController.getStackPosition().y; 2238 2239 // If the flyout is visible, translate it along with the bubble stack. 2240 if (mFlyout.getVisibility() == VISIBLE) { 2241 PhysicsAnimator.getInstance(mFlyout) 2242 .spring(DynamicAnimation.TRANSLATION_Y, 2243 mFlyout.getTranslationY() + stackDy, 2244 FLYOUT_IME_ANIMATION_SPRING_CONFIG) 2245 .start(); 2246 } 2247 } else if (mPositioner.showBubblesVertically() && mIsExpanded 2248 && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 2249 mExpandedBubble.getExpandedView().setImeVisible(visible); 2250 List<Animator> animList = new ArrayList(); 2251 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { 2252 View child = mBubbleContainer.getChildAt(i); 2253 float transY = mPositioner.getExpandedBubbleXY(i, getState()).y; 2254 ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY); 2255 animList.add(anim); 2256 } 2257 updatePointerPosition(true /* forIme */); 2258 AnimatorSet set = new AnimatorSet(); 2259 set.playTogether(animList); 2260 set.start(); 2261 } 2262 } 2263 2264 @Override dispatchTouchEvent(MotionEvent ev)2265 public boolean dispatchTouchEvent(MotionEvent ev) { 2266 if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) { 2267 // Ignore touches from additional pointer indices. 2268 return false; 2269 } 2270 2271 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 2272 mPointerIndexDown = ev.getActionIndex(); 2273 } else if (ev.getAction() == MotionEvent.ACTION_UP 2274 || ev.getAction() == MotionEvent.ACTION_CANCEL) { 2275 mPointerIndexDown = -1; 2276 } 2277 2278 boolean dispatched = super.dispatchTouchEvent(ev); 2279 2280 // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned 2281 // at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will 2282 // then be passed to the new bubble, which will not consume them since it hasn't received an 2283 // ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler 2284 // until the current gesture ends with an ACTION_UP event. 2285 if (!dispatched && !mIsExpanded && mIsGestureInProgress) { 2286 dispatched = mBubbleTouchListener.onTouch(this /* view */, ev); 2287 } 2288 2289 mIsGestureInProgress = 2290 ev.getAction() != MotionEvent.ACTION_UP 2291 && ev.getAction() != MotionEvent.ACTION_CANCEL; 2292 2293 return dispatched; 2294 } 2295 setFlyoutStateForDragLength(float deltaX)2296 void setFlyoutStateForDragLength(float deltaX) { 2297 // This shouldn't happen, but if it does, just wait until the flyout lays out. This method 2298 // is continually called. 2299 if (mFlyout.getWidth() <= 0) { 2300 return; 2301 } 2302 2303 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 2304 mFlyoutDragDeltaX = deltaX; 2305 2306 final float collapsePercent = 2307 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth(); 2308 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent))); 2309 2310 // Calculate how to translate the flyout if it has been dragged too far in either direction. 2311 float overscrollTranslation = 0f; 2312 if (collapsePercent < 0f || collapsePercent > 1f) { 2313 // Whether we are more than 100% transitioned to the dot. 2314 final boolean overscrollingPastDot = collapsePercent > 1f; 2315 2316 // Whether we are overscrolling physically to the left - this can either be pulling the 2317 // flyout away from the stack (if the stack is on the right) or pushing it to the left 2318 // after it has already become the dot. 2319 final boolean overscrollingLeft = 2320 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f); 2321 overscrollTranslation = 2322 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1) 2323 * (overscrollingLeft ? -1 : 1) 2324 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR 2325 // Attenuate the smaller dot less than the larger flyout. 2326 / (overscrollingPastDot ? 2 : 1))); 2327 } 2328 2329 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation); 2330 } 2331 2332 /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */ passEventToMagnetizedObject(MotionEvent event)2333 private boolean passEventToMagnetizedObject(MotionEvent event) { 2334 return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event); 2335 } 2336 2337 /** 2338 * Dismisses the magnetized object - either an individual bubble, if we're expanded, or the 2339 * stack, if we're collapsed. 2340 */ dismissMagnetizedObject()2341 private void dismissMagnetizedObject() { 2342 if (mIsExpanded) { 2343 final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject(); 2344 dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView)); 2345 } else { 2346 mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE); 2347 } 2348 } 2349 dismissBubbleIfExists(@ullable BubbleViewProvider bubble)2350 private void dismissBubbleIfExists(@Nullable BubbleViewProvider bubble) { 2351 if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { 2352 mBubbleData.dismissBubbleWithKey(bubble.getKey(), Bubbles.DISMISS_USER_GESTURE); 2353 } 2354 } 2355 2356 /** Prepares and starts the dismiss animation on the bubble stack. */ animateDismissBubble(View targetView, boolean applyAlpha)2357 private void animateDismissBubble(View targetView, boolean applyAlpha) { 2358 mViewBeingDismissed = targetView; 2359 2360 if (mViewBeingDismissed == null) { 2361 return; 2362 } 2363 if (applyAlpha) { 2364 mDismissBubbleAnimator.removeAllListeners(); 2365 mDismissBubbleAnimator.start(); 2366 } else { 2367 mDismissBubbleAnimator.removeAllListeners(); 2368 mDismissBubbleAnimator.addListener(new AnimatorListenerAdapter() { 2369 @Override 2370 public void onAnimationEnd(Animator animation) { 2371 super.onAnimationEnd(animation); 2372 resetDismissAnimator(); 2373 } 2374 2375 @Override 2376 public void onAnimationCancel(Animator animation) { 2377 super.onAnimationCancel(animation); 2378 resetDismissAnimator(); 2379 } 2380 }); 2381 mDismissBubbleAnimator.reverse(); 2382 } 2383 } 2384 resetDismissAnimator()2385 private void resetDismissAnimator() { 2386 mDismissBubbleAnimator.removeAllListeners(); 2387 mDismissBubbleAnimator.cancel(); 2388 2389 if (mViewBeingDismissed != null) { 2390 mViewBeingDismissed.setAlpha(1f); 2391 mViewBeingDismissed = null; 2392 } 2393 if (mDismissView != null) { 2394 mDismissView.getCircle().setScaleX(1f); 2395 mDismissView.getCircle().setScaleY(1f); 2396 } 2397 } 2398 2399 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */ animateFlyoutCollapsed(boolean collapsed, float velX)2400 private void animateFlyoutCollapsed(boolean collapsed, float velX) { 2401 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 2402 // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's 2403 // faster. 2404 mFlyoutTransitionSpring.getSpring().setStiffness( 2405 (mBubbleToExpandAfterFlyoutCollapse != null) 2406 ? SpringForce.STIFFNESS_MEDIUM 2407 : SpringForce.STIFFNESS_LOW); 2408 mFlyoutTransitionSpring 2409 .setStartValue(mFlyoutDragDeltaX) 2410 .setStartVelocity(velX) 2411 .animateToFinalPosition(collapsed 2412 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth()) 2413 : 0f); 2414 } 2415 shouldShowFlyout(Bubble bubble)2416 private boolean shouldShowFlyout(Bubble bubble) { 2417 Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage(); 2418 final BadgedImageView bubbleView = bubble.getIconView(); 2419 if (flyoutMessage == null 2420 || flyoutMessage.message == null 2421 || !bubble.showFlyout() 2422 || isStackEduShowing() 2423 || isExpanded() 2424 || mIsExpansionAnimating 2425 || mIsGestureInProgress 2426 || mBubbleToExpandAfterFlyoutCollapse != null 2427 || bubbleView == null) { 2428 if (bubbleView != null && mFlyout.getVisibility() != VISIBLE) { 2429 bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); 2430 } 2431 // Skip the message if none exists, we're expanded or animating expansion, or we're 2432 // about to expand a bubble from the previous tapped flyout, or if bubble view is null. 2433 return false; 2434 } 2435 return true; 2436 } 2437 2438 /** 2439 * Animates in the flyout for the given bubble, if available, and then hides it after some time. 2440 */ 2441 @VisibleForTesting animateInFlyoutForBubble(Bubble bubble)2442 void animateInFlyoutForBubble(Bubble bubble) { 2443 if (!shouldShowFlyout(bubble)) { 2444 return; 2445 } 2446 2447 mFlyoutDragDeltaX = 0f; 2448 clearFlyoutOnHide(); 2449 mAfterFlyoutHidden = () -> { 2450 // Null it out to ensure it runs once. 2451 mAfterFlyoutHidden = null; 2452 2453 if (mBubbleToExpandAfterFlyoutCollapse != null) { 2454 // User tapped on the flyout and we should expand 2455 mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse); 2456 mBubbleData.setExpanded(true); 2457 mBubbleToExpandAfterFlyoutCollapse = null; 2458 } 2459 2460 // Stop suppressing the dot now that the flyout has morphed into the dot. 2461 if (bubble.getIconView() != null) { 2462 bubble.getIconView().removeDotSuppressionFlag( 2463 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); 2464 } 2465 // Hide the stack after a delay, if needed. 2466 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 2467 }; 2468 2469 // Suppress the dot when we are animating the flyout. 2470 bubble.getIconView().addDotSuppressionFlag( 2471 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); 2472 2473 // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0. 2474 post(() -> { 2475 // An auto-expanding bubble could have been posted during the time it takes to 2476 // layout. 2477 if (isExpanded() || bubble.getIconView() == null) { 2478 return; 2479 } 2480 final Runnable expandFlyoutAfterDelay = () -> { 2481 mAnimateInFlyout = () -> { 2482 mFlyout.setVisibility(VISIBLE); 2483 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 2484 mFlyoutDragDeltaX = 2485 mStackAnimationController.isStackOnLeftSide() 2486 ? -mFlyout.getWidth() 2487 : mFlyout.getWidth(); 2488 animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */); 2489 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 2490 }; 2491 mFlyout.postDelayed(mAnimateInFlyout, 200); 2492 }; 2493 2494 2495 if (mFlyout.getVisibility() == View.VISIBLE) { 2496 mFlyout.animateUpdate(bubble.getFlyoutMessage(), 2497 mStackAnimationController.getStackPosition(), !bubble.showDot(), 2498 bubble.getIconView().getDotCenter(), 2499 mAfterFlyoutHidden /* onHide */); 2500 } else { 2501 mFlyout.setVisibility(INVISIBLE); 2502 mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(), 2503 mStackAnimationController.getStackPosition(), 2504 mStackAnimationController.isStackOnLeftSide(), 2505 bubble.getIconView().getDotColor() /* dotColor */, 2506 expandFlyoutAfterDelay /* onLayoutComplete */, 2507 mAfterFlyoutHidden /* onHide */, 2508 bubble.getIconView().getDotCenter(), 2509 !bubble.showDot()); 2510 } 2511 mFlyout.bringToFront(); 2512 }); 2513 mFlyout.removeCallbacks(mHideFlyout); 2514 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 2515 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); 2516 } 2517 2518 /** Hide the flyout immediately and cancel any pending hide runnables. */ hideFlyoutImmediate()2519 private void hideFlyoutImmediate() { 2520 clearFlyoutOnHide(); 2521 mFlyout.removeCallbacks(mAnimateInFlyout); 2522 mFlyout.removeCallbacks(mHideFlyout); 2523 mFlyout.hideFlyout(); 2524 } 2525 clearFlyoutOnHide()2526 private void clearFlyoutOnHide() { 2527 mFlyout.removeCallbacks(mAnimateInFlyout); 2528 if (mAfterFlyoutHidden == null) { 2529 return; 2530 } 2531 mAfterFlyoutHidden.run(); 2532 mAfterFlyoutHidden = null; 2533 } 2534 2535 /** 2536 * Fills the Rect with the touchable region of the bubbles. This will be used by WindowManager 2537 * to decide which touch events go to Bubbles. 2538 * 2539 * Bubbles is below the status bar/notification shade but above application windows. If you're 2540 * trying to get touch events from the status bar or another higher-level window layer, you'll 2541 * need to re-order TYPE_BUBBLES in WindowManagerPolicy so that we have the opportunity to steal 2542 * them. 2543 */ getTouchableRegion(Rect outRect)2544 public void getTouchableRegion(Rect outRect) { 2545 if (isStackEduShowing()) { 2546 // When user education shows then capture all touches 2547 outRect.set(0, 0, getWidth(), getHeight()); 2548 return; 2549 } 2550 2551 if (!mIsExpanded) { 2552 if (getBubbleCount() > 0 || mBubbleData.isShowingOverflow()) { 2553 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect); 2554 // Increase the touch target size of the bubble 2555 outRect.top -= mBubbleTouchPadding; 2556 outRect.left -= mBubbleTouchPadding; 2557 outRect.right += mBubbleTouchPadding; 2558 outRect.bottom += mBubbleTouchPadding; 2559 } 2560 } else { 2561 mBubbleContainer.getBoundsOnScreen(outRect); 2562 // Account for the IME in the touchable region so that the touchable region of the 2563 // Bubble window doesn't obscure the IME. The touchable region affects which areas 2564 // of the screen can be excluded by lower windows (IME is just above the embedded task) 2565 outRect.bottom -= mPositioner.getImeHeight(); 2566 } 2567 2568 if (mFlyout.getVisibility() == View.VISIBLE) { 2569 final Rect flyoutBounds = new Rect(); 2570 mFlyout.getBoundsOnScreen(flyoutBounds); 2571 outRect.union(flyoutBounds); 2572 } 2573 } 2574 requestUpdate()2575 private void requestUpdate() { 2576 if (mViewUpdatedRequested || mIsExpansionAnimating) { 2577 return; 2578 } 2579 mViewUpdatedRequested = true; 2580 getViewTreeObserver().addOnPreDrawListener(mViewUpdater); 2581 invalidate(); 2582 } 2583 2584 /** Hide or show the manage menu for the currently expanded bubble. */ 2585 @VisibleForTesting showManageMenu(boolean show)2586 public void showManageMenu(boolean show) { 2587 mShowingManage = show; 2588 2589 // This should not happen, since the manage menu is only visible when there's an expanded 2590 // bubble. If we end up in this state, just hide the menu immediately. 2591 if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { 2592 mManageMenu.setVisibility(View.INVISIBLE); 2593 mManageMenuScrim.setVisibility(INVISIBLE); 2594 mBubbleController.getSysuiProxy().onManageMenuExpandChanged(false /* show */); 2595 return; 2596 } 2597 if (show) { 2598 mManageMenuScrim.setVisibility(VISIBLE); 2599 mManageMenuScrim.setTranslationZ(mManageMenu.getElevation() - 1f); 2600 } 2601 Runnable endAction = () -> { 2602 if (!show) { 2603 mManageMenuScrim.setVisibility(INVISIBLE); 2604 mManageMenuScrim.setTranslationZ(0f); 2605 } 2606 }; 2607 2608 mBubbleController.getSysuiProxy().onManageMenuExpandChanged(show); 2609 mManageMenuScrim.animate() 2610 .setInterpolator(show ? ALPHA_IN : ALPHA_OUT) 2611 .alpha(show ? SCRIM_ALPHA : 0f) 2612 .withEndAction(endAction) 2613 .start(); 2614 2615 // If available, update the manage menu's settings option with the expanded bubble's app 2616 // name and icon. 2617 if (show && mBubbleData.hasBubbleInStackWithKey(mExpandedBubble.getKey())) { 2618 final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey()); 2619 mManageSettingsIcon.setImageBitmap(bubble.getAppBadge()); 2620 mManageSettingsText.setText(getResources().getString( 2621 R.string.bubbles_app_settings, bubble.getAppName())); 2622 } 2623 2624 if (mExpandedBubble.getExpandedView().getTaskView() != null) { 2625 mExpandedBubble.getExpandedView().getTaskView().setObscuredTouchRect(mShowingManage 2626 ? new Rect(0, 0, getWidth(), getHeight()) 2627 : null); 2628 } 2629 2630 final boolean isLtr = 2631 getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_LTR; 2632 2633 // When the menu is open, it should be at these coordinates. The menu pops out to the right 2634 // in LTR and to the left in RTL. 2635 mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); 2636 final float margin = mExpandedBubble.getExpandedView().getManageButtonMargin(); 2637 final float targetX = isLtr 2638 ? mTempRect.left - margin 2639 : mTempRect.right + margin - mManageMenu.getWidth(); 2640 final float targetY = mTempRect.bottom - mManageMenu.getHeight(); 2641 2642 final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f; 2643 if (show) { 2644 mManageMenu.setScaleX(0.5f); 2645 mManageMenu.setScaleY(0.5f); 2646 mManageMenu.setTranslationX(targetX - xOffsetForAnimation); 2647 mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4f); 2648 mManageMenu.setAlpha(0f); 2649 2650 PhysicsAnimator.getInstance(mManageMenu) 2651 .spring(DynamicAnimation.ALPHA, 1f) 2652 .spring(DynamicAnimation.SCALE_X, 1f) 2653 .spring(DynamicAnimation.SCALE_Y, 1f) 2654 .spring(DynamicAnimation.TRANSLATION_X, targetX) 2655 .spring(DynamicAnimation.TRANSLATION_Y, targetY) 2656 .withEndActions(() -> { 2657 View child = mManageMenu.getChildAt(0); 2658 child.requestAccessibilityFocus(); 2659 // Update the AV's obscured touchable region for the new visibility state. 2660 mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); 2661 }) 2662 .start(); 2663 2664 mManageMenu.setVisibility(View.VISIBLE); 2665 } else { 2666 PhysicsAnimator.getInstance(mManageMenu) 2667 .spring(DynamicAnimation.ALPHA, 0f) 2668 .spring(DynamicAnimation.SCALE_X, 0.5f) 2669 .spring(DynamicAnimation.SCALE_Y, 0.5f) 2670 .spring(DynamicAnimation.TRANSLATION_X, targetX - xOffsetForAnimation) 2671 .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4f) 2672 .withEndActions(() -> { 2673 mManageMenu.setVisibility(View.INVISIBLE); 2674 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 2675 // Update the AV's obscured touchable region for the new state. 2676 mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); 2677 } 2678 }) 2679 .start(); 2680 } 2681 } 2682 updateExpandedBubble()2683 private void updateExpandedBubble() { 2684 if (DEBUG_BUBBLE_STACK_VIEW) { 2685 Log.d(TAG, "updateExpandedBubble()"); 2686 } 2687 2688 mExpandedViewContainer.removeAllViews(); 2689 if (mIsExpanded && mExpandedBubble != null 2690 && mExpandedBubble.getExpandedView() != null) { 2691 BubbleExpandedView bev = mExpandedBubble.getExpandedView(); 2692 bev.setContentVisibility(false); 2693 bev.setAlphaAnimating(!mIsExpansionAnimating); 2694 mExpandedViewContainerMatrix.setScaleX(0f); 2695 mExpandedViewContainerMatrix.setScaleY(0f); 2696 mExpandedViewContainerMatrix.setTranslate(0f, 0f); 2697 mExpandedViewContainer.setVisibility(View.INVISIBLE); 2698 mExpandedViewContainer.setAlpha(0f); 2699 mExpandedViewContainer.addView(bev); 2700 bev.setManageClickListener((view) -> showManageMenu(!mShowingManage)); 2701 2702 if (!mIsExpansionAnimating) { 2703 mSurfaceSynchronizer.syncSurfaceAndRun(() -> { 2704 post(this::animateSwitchBubbles); 2705 }); 2706 } 2707 } 2708 } 2709 2710 /** 2711 * Requests a snapshot from the currently expanded bubble's TaskView and displays it in a 2712 * SurfaceView. This allows us to load a newly expanded bubble's Activity into the TaskView, 2713 * while animating the (screenshot of the) previously selected bubble's content away. 2714 * 2715 * @param onComplete Callback to run once we're done here - called with 'false' if something 2716 * went wrong, or 'true' if the SurfaceView is now showing a screenshot of the 2717 * expanded bubble. 2718 */ screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete)2719 private void screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete) { 2720 if (!mIsExpanded || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { 2721 // You can't animate null. 2722 onComplete.accept(false); 2723 return; 2724 } 2725 2726 final BubbleExpandedView animatingOutExpandedView = mExpandedBubble.getExpandedView(); 2727 2728 // Release the previous screenshot if it hasn't been released already. 2729 if (mAnimatingOutBubbleBuffer != null) { 2730 releaseAnimatingOutBubbleBuffer(); 2731 } 2732 2733 try { 2734 mAnimatingOutBubbleBuffer = animatingOutExpandedView.snapshotActivitySurface(); 2735 } catch (Exception e) { 2736 // If we fail for any reason, print the stack trace and then notify the callback of our 2737 // failure. This is not expected to occur, but it's not worth crashing over. 2738 Log.wtf(TAG, e); 2739 onComplete.accept(false); 2740 } 2741 2742 if (mAnimatingOutBubbleBuffer == null 2743 || mAnimatingOutBubbleBuffer.getHardwareBuffer() == null) { 2744 // While no exception was thrown, we were unable to get a snapshot. 2745 onComplete.accept(false); 2746 return; 2747 } 2748 2749 // Make sure the surface container's properties have been reset. 2750 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); 2751 mAnimatingOutSurfaceContainer.setScaleX(1f); 2752 mAnimatingOutSurfaceContainer.setScaleY(1f); 2753 mAnimatingOutSurfaceContainer.setTranslationX(mExpandedViewContainer.getPaddingLeft()); 2754 mAnimatingOutSurfaceContainer.setTranslationY(0); 2755 2756 final int[] taskViewLocation = 2757 mExpandedBubble.getExpandedView().getTaskViewLocationOnScreen(); 2758 final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen(); 2759 2760 // Translate the surface to overlap the real TaskView. 2761 mAnimatingOutSurfaceContainer.setTranslationY( 2762 taskViewLocation[1] - surfaceViewLocation[1]); 2763 2764 // Set the width/height of the SurfaceView to match the snapshot. 2765 mAnimatingOutSurfaceView.getLayoutParams().width = 2766 mAnimatingOutBubbleBuffer.getHardwareBuffer().getWidth(); 2767 mAnimatingOutSurfaceView.getLayoutParams().height = 2768 mAnimatingOutBubbleBuffer.getHardwareBuffer().getHeight(); 2769 mAnimatingOutSurfaceView.requestLayout(); 2770 2771 // Post to wait for layout. 2772 post(() -> { 2773 // The buffer might have been destroyed if the user is mashing on bubbles, that's okay. 2774 if (mAnimatingOutBubbleBuffer == null 2775 || mAnimatingOutBubbleBuffer.getHardwareBuffer() == null 2776 || mAnimatingOutBubbleBuffer.getHardwareBuffer().isClosed()) { 2777 onComplete.accept(false); 2778 return; 2779 } 2780 2781 if (!mIsExpanded || !mAnimatingOutSurfaceReady) { 2782 onComplete.accept(false); 2783 return; 2784 } 2785 2786 // Attach the buffer! We're now displaying the snapshot. 2787 mAnimatingOutSurfaceView.getHolder().getSurface().attachAndQueueBufferWithColorSpace( 2788 mAnimatingOutBubbleBuffer.getHardwareBuffer(), 2789 mAnimatingOutBubbleBuffer.getColorSpace()); 2790 2791 mAnimatingOutSurfaceView.setAlpha(1f); 2792 mExpandedViewContainer.setVisibility(View.GONE); 2793 2794 mSurfaceSynchronizer.syncSurfaceAndRun(() -> { 2795 post(() -> { 2796 onComplete.accept(true); 2797 }); 2798 }); 2799 }); 2800 } 2801 2802 /** 2803 * Releases the buffer containing the screenshot of the animating-out bubble, if it exists and 2804 * isn't yet destroyed. 2805 */ releaseAnimatingOutBubbleBuffer()2806 private void releaseAnimatingOutBubbleBuffer() { 2807 if (mAnimatingOutBubbleBuffer != null 2808 && !mAnimatingOutBubbleBuffer.getHardwareBuffer().isClosed()) { 2809 mAnimatingOutBubbleBuffer.getHardwareBuffer().close(); 2810 } 2811 } 2812 updateExpandedView()2813 private void updateExpandedView() { 2814 if (DEBUG_BUBBLE_STACK_VIEW) { 2815 Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded); 2816 } 2817 boolean isOverflowExpanded = mExpandedBubble != null 2818 && BubbleOverflow.KEY.equals(mExpandedBubble.getKey()); 2819 int[] paddings = mPositioner.getExpandedViewContainerPadding( 2820 mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded); 2821 mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]); 2822 if (mIsExpansionAnimating) { 2823 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); 2824 } 2825 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 2826 PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble), 2827 getState()); 2828 mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble, 2829 mPositioner.showBubblesVertically() ? p.y : p.x)); 2830 mExpandedViewContainer.setTranslationX(0f); 2831 mExpandedBubble.getExpandedView().updateView( 2832 mExpandedViewContainer.getLocationOnScreen()); 2833 updatePointerPosition(false /* forIme */); 2834 } 2835 2836 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); 2837 } 2838 2839 /** 2840 * Updates whether each of the bubbles should show shadows. When collapsed & resting, only the 2841 * visible bubbles (top 2) will show a shadow. When the stack is being dragged, everything 2842 * shows a shadow. When an individual bubble is dragged out, it should show a shadow. 2843 */ updateBubbleShadows(boolean showForAllBubbles)2844 private void updateBubbleShadows(boolean showForAllBubbles) { 2845 int bubbleCount = getBubbleCount(); 2846 for (int i = 0; i < bubbleCount; i++) { 2847 final float z = (mPositioner.getMaxBubbles() * mBubbleElevation) - i; 2848 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); 2849 boolean isDraggedOut = mMagnetizedObject != null 2850 && mMagnetizedObject.getUnderlyingObject().equals(bv); 2851 if (showForAllBubbles || isDraggedOut) { 2852 bv.setZ(z); 2853 } else { 2854 final float tz = i < NUM_VISIBLE_WHEN_RESTING ? z : 0f; 2855 bv.setZ(tz); 2856 } 2857 } 2858 } 2859 2860 /** 2861 * When the bubbles are flung and then rest, the shadows stack up for the bubbles hidden 2862 * beneath the top two bubbles, to avoid this we animate the Z translations once the stack 2863 * is resting so that they fade away nicely. 2864 */ 2865 private void animateShadows() { 2866 int bubbleCount = getBubbleCount(); 2867 for (int i = 0; i < bubbleCount; i++) { 2868 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); 2869 boolean fullShadow = i < NUM_VISIBLE_WHEN_RESTING; 2870 if (!fullShadow) { 2871 bv.animate().translationZ(0).start(); 2872 } 2873 } 2874 } 2875 2876 private void updateZOrder() { 2877 int bubbleCount = getBubbleCount(); 2878 for (int i = 0; i < bubbleCount; i++) { 2879 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); 2880 bv.setZ(i < NUM_VISIBLE_WHEN_RESTING 2881 ? (mPositioner.getMaxBubbles() * mBubbleElevation) - i 2882 : 0f); 2883 } 2884 } 2885 2886 private void updateBadges(boolean setBadgeForCollapsedStack) { 2887 int bubbleCount = getBubbleCount(); 2888 for (int i = 0; i < bubbleCount; i++) { 2889 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); 2890 if (mIsExpanded) { 2891 // If we're not displaying vertically, we always show the badge on the left. 2892 boolean onLeft = mPositioner.showBubblesVertically() && !mStackOnLeftOrWillBe; 2893 bv.showDotAndBadge(onLeft); 2894 } else if (setBadgeForCollapsedStack) { 2895 if (i == 0) { 2896 bv.showDotAndBadge(!mStackOnLeftOrWillBe); 2897 } else { 2898 bv.hideDotAndBadge(!mStackOnLeftOrWillBe); 2899 } 2900 } 2901 } 2902 } 2903 2904 /** 2905 * Updates the position of the pointer based on the expanded bubble. 2906 * 2907 * @param forIme whether the position is being updated due to the ime appearing, in this case 2908 * the pointer is animated to the location. 2909 */ 2910 private void updatePointerPosition(boolean forIme) { 2911 if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { 2912 return; 2913 } 2914 int index = getBubbleIndex(mExpandedBubble); 2915 if (index == -1) { 2916 return; 2917 } 2918 PointF position = mPositioner.getExpandedBubbleXY(index, getState()); 2919 float bubblePosition = mPositioner.showBubblesVertically() 2920 ? position.y 2921 : position.x; 2922 mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition, 2923 mStackOnLeftOrWillBe, forIme /* animate */); 2924 } 2925 2926 /** 2927 * @return the number of bubbles in the stack view. 2928 */ 2929 public int getBubbleCount() { 2930 // Subtract 1 for the overflow button that is always in the bubble container. 2931 return mBubbleContainer.getChildCount() - 1; 2932 } 2933 2934 /** 2935 * Finds the bubble index within the stack. 2936 * 2937 * @param provider the bubble view provider with the bubble to look up. 2938 * @return the index of the bubble view within the bubble stack. The range of the position 2939 * is between 0 and the bubble count minus 1. 2940 */ 2941 int getBubbleIndex(@Nullable BubbleViewProvider provider) { 2942 if (provider == null) { 2943 return 0; 2944 } 2945 return mBubbleContainer.indexOfChild(provider.getIconView()); 2946 } 2947 2948 /** 2949 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places. 2950 */ 2951 public float getNormalizedXPosition() { 2952 return new BigDecimal(getStackPosition().x / mPositioner.getAvailableRect().width()) 2953 .setScale(4, RoundingMode.CEILING.HALF_UP) 2954 .floatValue(); 2955 } 2956 2957 /** 2958 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places. 2959 */ 2960 public float getNormalizedYPosition() { 2961 return new BigDecimal(getStackPosition().y / mPositioner.getAvailableRect().height()) 2962 .setScale(4, RoundingMode.CEILING.HALF_UP) 2963 .floatValue(); 2964 } 2965 2966 /** @return the position of the bubble stack. */ 2967 public PointF getStackPosition() { 2968 return mStackAnimationController.getStackPosition(); 2969 } 2970 2971 /** 2972 * Logs the bubble UI event. 2973 * 2974 * @param provider the bubble view provider that is being interacted on. Null value indicates 2975 * that the user interaction is not specific to one bubble. 2976 * @param action the user interaction enum. 2977 */ 2978 private void logBubbleEvent(@Nullable BubbleViewProvider provider, int action) { 2979 final String packageName = 2980 (provider != null && provider instanceof Bubble) 2981 ? ((Bubble) provider).getPackageName() 2982 : "null"; 2983 mBubbleData.logBubbleEvent(provider, 2984 action, 2985 packageName, 2986 getBubbleCount(), 2987 getBubbleIndex(provider), 2988 getNormalizedXPosition(), 2989 getNormalizedYPosition()); 2990 } 2991 2992 /** For debugging only */ 2993 List<Bubble> getBubblesOnScreen() { 2994 List<Bubble> bubbles = new ArrayList<>(); 2995 for (int i = 0; i < getBubbleCount(); i++) { 2996 View child = mBubbleContainer.getChildAt(i); 2997 if (child instanceof BadgedImageView) { 2998 String key = ((BadgedImageView) child).getKey(); 2999 Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); 3000 bubbles.add(bubble); 3001 } 3002 } 3003 return bubbles; 3004 } 3005 3006 /** @return the current stack state. */ 3007 public StackViewState getState() { 3008 mStackViewState.numberOfBubbles = mBubbleContainer.getChildCount(); 3009 mStackViewState.selectedIndex = getBubbleIndex(mExpandedBubble); 3010 mStackViewState.onLeft = mStackOnLeftOrWillBe; 3011 return mStackViewState; 3012 } 3013 3014 /** 3015 * Holds some commonly queried information about the stack. 3016 */ 3017 public static class StackViewState { 3018 // Number of bubbles (including the overflow itself) in the stack. 3019 public int numberOfBubbles; 3020 // The selected index if the stack is expanded. 3021 public int selectedIndex; 3022 // Whether the stack is resting on the left or right side of the screen when collapsed. 3023 public boolean onLeft; 3024 } 3025 3026 /** 3027 * Representation of stack position that uses relative properties rather than absolute 3028 * coordinates. This is used to maintain similar stack positions across configuration changes. 3029 */ 3030 public static class RelativeStackPosition { 3031 /** Whether to place the stack at the leftmost allowed position. */ 3032 private boolean mOnLeft; 3033 3034 /** 3035 * How far down the vertically allowed region to place the stack. For example, if the stack 3036 * allowed region is between y = 100 and y = 1100 and this is 0.2f, we'll place the stack at 3037 * 100 + (0.2f * 1000) = 300. 3038 */ 3039 private float mVerticalOffsetPercent; 3040 3041 public RelativeStackPosition(boolean onLeft, float verticalOffsetPercent) { 3042 mOnLeft = onLeft; 3043 mVerticalOffsetPercent = clampVerticalOffsetPercent(verticalOffsetPercent); 3044 } 3045 3046 /** Constructs a relative position given a region and a point in that region. */ 3047 public RelativeStackPosition(PointF position, RectF region) { 3048 mOnLeft = position.x < region.width() / 2; 3049 mVerticalOffsetPercent = 3050 clampVerticalOffsetPercent((position.y - region.top) / region.height()); 3051 } 3052 3053 /** Ensures that the offset percent is between 0f and 1f. */ 3054 private float clampVerticalOffsetPercent(float offsetPercent) { 3055 return Math.max(0f, Math.min(1f, offsetPercent)); 3056 } 3057 3058 /** 3059 * Given an allowable stack position region, returns the point within that region 3060 * represented by this relative position. 3061 */ 3062 public PointF getAbsolutePositionInRegion(RectF region) { 3063 return new PointF( 3064 mOnLeft ? region.left : region.right, 3065 region.top + mVerticalOffsetPercent * region.height()); 3066 } 3067 } 3068 } 3069