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