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