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