blob: cff371f93f3d8302619d095030e774d525ffe800 [file] [log] [blame]
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.bubbles;
18
Joshua Tsujib1a796b2019-01-16 15:43:12 -080019import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080020import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
21
Mady Mellor5a3e94b2020-02-07 12:16:21 -080022import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
23import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_EDUCATION;
24import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION;
Mady Mellorb8aaf972019-11-26 10:28:00 -080025import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_DEFAULT;
26import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_SUPPRESSED_FOR_FLYOUT;
Issei Suzukia8d07312019-06-07 12:56:19 +020027import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080028import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION;
Issei Suzukia8d07312019-06-07 12:56:19 +020029import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
30import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
31
Joshua Tsuji4accf5982019-04-22 17:36:11 -040032import android.animation.Animator;
33import android.animation.AnimatorListenerAdapter;
34import android.animation.ValueAnimator;
Lyn Han6c40fe72019-05-08 14:06:33 -070035import android.app.Notification;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080036import android.content.Context;
Lyn Hanf4730312019-06-18 11:18:58 -070037import android.content.res.Configuration;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080038import android.content.res.Resources;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080039import android.content.res.TypedArray;
40import android.graphics.Color;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040041import android.graphics.ColorMatrix;
42import android.graphics.ColorMatrixColorFilter;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040043import android.graphics.Paint;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080044import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080045import android.graphics.PointF;
46import android.graphics.Rect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080047import android.graphics.RectF;
Mady Mellor217b2e92019-02-27 11:44:16 -080048import android.os.Bundle;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040049import android.os.Vibrator;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050050import android.util.Log;
Issei Suzukic0387542019-03-08 17:31:14 +010051import android.view.Choreographer;
Mady Mellor9be3bed2019-08-21 17:26:26 -070052import android.view.DisplayCutout;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -040053import android.view.Gravity;
Mady Mellordea7ecf2018-12-10 15:47:40 -080054import android.view.LayoutInflater;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080055import android.view.MotionEvent;
56import android.view.View;
Joshua Tsuji20103542020-02-18 14:06:28 -050057import android.view.ViewGroup;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080058import android.view.ViewTreeObserver;
Joshua Tsuji0fee7682019-01-25 11:37:49 -050059import android.view.WindowInsets;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080060import android.view.WindowManager;
Mady Mellor217b2e92019-02-27 11:44:16 -080061import android.view.accessibility.AccessibilityNodeInfo;
Lyn Hane68d0912019-05-02 18:28:01 -070062import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Joshua Tsuji614b1df2019-03-26 13:57:05 -040063import android.view.animation.AccelerateDecelerateInterpolator;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080064import android.widget.FrameLayout;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080065import android.widget.TextView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080066
Mark Renoufcecc77b2019-01-30 16:32:24 -050067import androidx.annotation.MainThread;
Joshua Tsuji20103542020-02-18 14:06:28 -050068import androidx.annotation.NonNull;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080069import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080070import androidx.dynamicanimation.animation.DynamicAnimation;
Joshua Tsuji6549e702019-05-02 13:13:16 -040071import androidx.dynamicanimation.animation.FloatPropertyCompat;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080072import androidx.dynamicanimation.animation.SpringAnimation;
73import androidx.dynamicanimation.animation.SpringForce;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080074
Mady Melloredd4ee12019-01-18 10:45:11 -080075import com.android.internal.annotations.VisibleForTesting;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080076import com.android.internal.util.ContrastColorUtil;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080077import com.android.internal.widget.ViewClippingUtil;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080078import com.android.systemui.Prefs;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080079import com.android.systemui.R;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080080import com.android.systemui.bubbles.animation.ExpandedAnimationController;
81import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
82import com.android.systemui.bubbles.animation.StackAnimationController;
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -080083import com.android.systemui.shared.system.SysUiStatsLog;
Joshua Tsuji20103542020-02-18 14:06:28 -050084import com.android.systemui.util.DismissCircleView;
Joshua Tsuji7155bf12020-02-13 16:14:29 -050085import com.android.systemui.util.FloatingContentCoordinator;
Joshua Tsuji20103542020-02-18 14:06:28 -050086import com.android.systemui.util.animation.PhysicsAnimator;
87import com.android.systemui.util.magnetictarget.MagnetizedObject;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080088
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040089import java.io.FileDescriptor;
90import java.io.PrintWriter;
Steven Wua254dab2019-01-29 11:30:39 -050091import java.math.BigDecimal;
92import java.math.RoundingMode;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040093import java.util.ArrayList;
Mark Renouf821e6782019-04-01 14:17:37 -040094import java.util.Collections;
95import java.util.List;
Steven Wua254dab2019-01-29 11:30:39 -050096
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080097/**
98 * Renders bubbles in a stack and handles animating expanded and collapsed states.
99 */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500100public class BubbleStackView extends FrameLayout {
Issei Suzukia8d07312019-06-07 12:56:19 +0200101 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800102
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800103 /** Animation durations for bubble stack user education views. **/
104 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION = 200;
105 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT = 40;
106
Joshua Tsuji6549e702019-05-02 13:13:16 -0400107 /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
108 static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
109
110 /** Velocity required to dismiss the flyout via drag. */
111 private static final float FLYOUT_DISMISS_VELOCITY = 2000f;
112
113 /**
114 * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel
115 * for every 8 pixels overscrolled).
116 */
117 private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
118
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400119 /** Duration of the flyout alpha animations. */
120 private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
121
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400122 /** Percent to darken the bubbles when they're in the dismiss target. */
123 private static final float DARKEN_PERCENT = 0.3f;
124
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400125 /** How long to wait, in milliseconds, before hiding the flyout. */
126 @VisibleForTesting
127 static final int FLYOUT_HIDE_AFTER = 5000;
128
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400129 private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG =
130 new PhysicsAnimator.SpringConfig(
131 StackAnimationController.IME_ANIMATION_STIFFNESS,
132 StackAnimationController.DEFAULT_BOUNCINESS);
133
Issei Suzukic0387542019-03-08 17:31:14 +0100134 /**
135 * Interface to synchronize {@link View} state and the screen.
136 *
137 * {@hide}
138 */
139 interface SurfaceSynchronizer {
140 /**
141 * Wait until requested change on a {@link View} is reflected on the screen.
142 *
143 * @param callback callback to run after the change is reflected on the screen.
144 */
145 void syncSurfaceAndRun(Runnable callback);
146 }
147
148 private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER =
149 new SurfaceSynchronizer() {
150 @Override
151 public void syncSurfaceAndRun(Runnable callback) {
152 Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
153 // Just wait 2 frames. There is no guarantee, but this is usually enough time that
154 // the requested change is reflected on the screen.
155 // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and
156 // surfaces, rewrite this logic with them.
157 private int mFrameWait = 2;
158
159 @Override
160 public void doFrame(long frameTimeNanos) {
161 if (--mFrameWait > 0) {
162 Choreographer.getInstance().postFrameCallback(this);
163 } else {
164 callback.run();
165 }
166 }
167 });
168 }
169 };
170
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800171 private Point mDisplaySize;
172
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800173 private final SpringAnimation mExpandedViewXAnim;
174 private final SpringAnimation mExpandedViewYAnim;
Mady Mellorcfd06c12019-02-13 14:32:12 -0800175 private final BubbleData mBubbleData;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800176
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400177 private final Vibrator mVibrator;
178 private final ValueAnimator mDesaturateAndDarkenAnimator;
179 private final Paint mDesaturateAndDarkenPaint = new Paint();
180
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800181 private PhysicsAnimationLayout mBubbleContainer;
182 private StackAnimationController mStackAnimationController;
183 private ExpandedAnimationController mExpandedAnimationController;
184
Mady Mellor3dff9e62019-02-05 18:12:53 -0800185 private FrameLayout mExpandedViewContainer;
186
Joshua Tsuji6549e702019-05-02 13:13:16 -0400187 private BubbleFlyoutView mFlyout;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400188 /** Runnable that fades out the flyout and then sets it to GONE. */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400189 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
Mady Mellordf48d0a2019-06-25 18:26:46 -0700190 /**
191 * Callback to run after the flyout hides. Also called if a new flyout is shown before the
192 * previous one animates out.
193 */
Mady Mellorb8aaf972019-11-26 10:28:00 -0800194 private Runnable mAfterFlyoutHidden;
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800195 /**
196 * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
197 * once it collapses.
198 */
199 @Nullable
200 private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400201
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400202 /** Layout change listener that moves the stack to the nearest valid position on rotation. */
Lyn Hanf4730312019-06-18 11:18:58 -0700203 private OnLayoutChangeListener mOrientationChangedListener;
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400204 /** Whether the stack was on the left side of the screen prior to rotation. */
205 private boolean mWasOnLeftBeforeRotation = false;
206 /**
207 * How far down the screen the stack was before rotation, in terms of percentage of the way down
208 * the allowable region. Defaults to -1 if not set.
209 */
210 private float mVerticalPosPercentBeforeRotation = -1;
211
Mady Mellor70958542019-09-24 17:12:46 -0700212 private int mMaxBubbles;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800213 private int mBubbleSize;
Mady Mellor70958542019-09-24 17:12:46 -0700214 private int mBubbleElevation;
Lyn Han4a8efe32019-05-30 09:43:27 -0700215 private int mBubblePaddingTop;
Mady Mellore9371bc2019-07-10 18:50:59 -0700216 private int mBubbleTouchPadding;
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700217 private int mExpandedViewPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800218 private int mExpandedAnimateXDistance;
219 private int mExpandedAnimateYDistance;
Lyn Han5aa27e22019-05-15 10:55:07 -0700220 private int mPointerHeight;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500221 private int mStatusBarHeight;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500222 private int mImeOffset;
Lyn Han3cd75d72020-02-15 19:10:12 -0800223 private BubbleViewProvider mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800224 private boolean mIsExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800225
Joshua Tsuji6549e702019-05-02 13:13:16 -0400226 /** Whether the stack is currently on the left side of the screen, or animating there. */
227 private boolean mStackOnLeftOrWillBe = false;
228
229 /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
230 private boolean mIsGestureInProgress = false;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400231
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400232 /** Description of current animation controller state. */
233 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
234 pw.println("Stack view state:");
235 pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress);
236 pw.print(" showingDismiss: "); pw.println(mShowingDismiss);
237 pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400238 mStackAnimationController.dump(fd, pw, args);
239 mExpandedAnimationController.dump(fd, pw, args);
240 }
241
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800242 private BubbleTouchHandler mTouchHandler;
Mady Mellorcd9b1302018-11-06 18:08:04 -0800243 private BubbleController.BubbleExpandListener mExpandListener;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800244
245 private boolean mViewUpdatedRequested = false;
Mady Mellorbc078c22019-03-26 17:10:34 -0700246 private boolean mIsExpansionAnimating = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400247 private boolean mShowingDismiss = false;
248
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400249 /** The view to desaturate/darken when magneted to the dismiss target. */
250 private View mDesaturateAndDarkenTargetView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800251
Mady Mellor3dff9e62019-02-05 18:12:53 -0800252 private LayoutInflater mInflater;
253
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800254 // Used for determining view / touch intersection
255 int[] mTempLoc = new int[2];
256 RectF mTempRect = new RectF();
257
Mark Renouf821e6782019-04-01 14:17:37 -0400258 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
259
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800260 private ViewTreeObserver.OnPreDrawListener mViewUpdater =
261 new ViewTreeObserver.OnPreDrawListener() {
262 @Override
263 public boolean onPreDraw() {
264 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
Lyn Han285ad302019-05-29 19:01:39 -0700265 updateExpandedView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800266 mViewUpdatedRequested = false;
267 return true;
268 }
269 };
270
Mark Renouf821e6782019-04-01 14:17:37 -0400271 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
272 this::updateSystemGestureExcludeRects;
273
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800274 private ViewClippingUtil.ClippingParameters mClippingParameters =
275 new ViewClippingUtil.ClippingParameters() {
276
Lyn Han522e9ff2019-05-17 13:26:13 -0700277 @Override
278 public boolean shouldFinish(View view) {
279 return false;
280 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800281
Lyn Han522e9ff2019-05-17 13:26:13 -0700282 @Override
283 public boolean isClippingEnablingAllowed(View view) {
284 return !mIsExpanded;
285 }
286 };
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800287
Joshua Tsuji6549e702019-05-02 13:13:16 -0400288 /** Float property that 'drags' the flyout. */
289 private final FloatPropertyCompat mFlyoutCollapseProperty =
290 new FloatPropertyCompat("FlyoutCollapseSpring") {
291 @Override
292 public float getValue(Object o) {
293 return mFlyoutDragDeltaX;
294 }
295
296 @Override
297 public void setValue(Object o, float v) {
298 onFlyoutDragged(v);
299 }
300 };
301
302 /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */
303 private final SpringAnimation mFlyoutTransitionSpring =
304 new SpringAnimation(this, mFlyoutCollapseProperty);
305
306 /** Distance the flyout has been dragged in the X axis. */
307 private float mFlyoutDragDeltaX = 0f;
308
309 /**
Joshua Tsuji14e68552019-06-06 17:17:08 -0400310 * Runnable that animates in the flyout. This reference is needed to cancel delayed postings.
311 */
312 private Runnable mAnimateInFlyout;
313
314 /**
Joshua Tsuji6549e702019-05-02 13:13:16 -0400315 * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides
316 * it immediately.
317 */
318 private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring =
319 (dynamicAnimation, b, v, v1) -> {
320 if (mFlyoutDragDeltaX == 0) {
321 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
322 } else {
323 mFlyout.hideFlyout();
324 }
325 };
326
Lyn Han1b4f25e2019-06-11 13:56:34 -0700327 @NonNull
328 private final SurfaceSynchronizer mSurfaceSynchronizer;
Issei Suzukic0387542019-03-08 17:31:14 +0100329
Joshua Tsuji20103542020-02-18 14:06:28 -0500330 /**
331 * The currently magnetized object, which is being dragged and will be attracted to the magnetic
332 * dismiss target.
333 *
334 * This is either the stack itself, or an individual bubble.
335 */
336 private MagnetizedObject<?> mMagnetizedObject;
337
338 /**
339 * The action to run when the magnetized object is released in the dismiss target.
340 *
341 * This will actually perform the dismissal of either the stack or an individual bubble.
342 */
343 private Runnable mReleasedInDismissTargetAction;
344
345 /**
346 * The MagneticTarget instance for our circular dismiss view. This is added to the
347 * MagnetizedObject instances for the stack and any dragged-out bubbles.
348 */
349 private MagnetizedObject.MagneticTarget mMagneticTarget;
350
351 /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */
352 private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener =
353 new MagnetizedObject.MagnetListener() {
354 @Override
355 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
356 animateDesaturateAndDarken(
357 mExpandedAnimationController.getDraggedOutBubble(), true);
358 }
359
360 @Override
361 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
362 float velX, float velY, boolean wasFlungOut) {
363 animateDesaturateAndDarken(
364 mExpandedAnimationController.getDraggedOutBubble(), false);
365
366 if (wasFlungOut) {
367 mExpandedAnimationController.snapBubbleBack(
368 mExpandedAnimationController.getDraggedOutBubble(), velX, velY);
369 hideDismissTarget();
370 } else {
371 mExpandedAnimationController.onUnstuckFromTarget();
372 }
373 }
374
375 @Override
376 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
377 mExpandedAnimationController.dismissDraggedOutBubble(
378 mExpandedAnimationController.getDraggedOutBubble(),
379 mReleasedInDismissTargetAction);
380 hideDismissTarget();
381 }
382 };
383
384 /** Magnet listener that handles animating and dismissing the entire stack. */
385 private final MagnetizedObject.MagnetListener mStackMagnetListener =
386 new MagnetizedObject.MagnetListener() {
387 @Override
388 public void onStuckToTarget(
389 @NonNull MagnetizedObject.MagneticTarget target) {
390 animateDesaturateAndDarken(mBubbleContainer, true);
391 }
392
393 @Override
394 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
395 float velX, float velY, boolean wasFlungOut) {
396 animateDesaturateAndDarken(mBubbleContainer, false);
397
398 if (wasFlungOut) {
399 mStackAnimationController.flingStackThenSpringToEdge(
400 mStackAnimationController.getStackPosition().x, velX, velY);
401 hideDismissTarget();
402 } else {
403 mStackAnimationController.onUnstuckFromTarget();
404 }
405 }
406
407 @Override
408 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
409 mStackAnimationController.implodeStack(
410 () -> {
411 resetDesaturationAndDarken();
412 mReleasedInDismissTargetAction.run();
413 }
414 );
415
416 hideDismissTarget();
417 }
418 };
419
420 private ViewGroup mDismissTargetContainer;
421 private PhysicsAnimator<View> mDismissTargetAnimator;
422 private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig(
423 SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
Issei Suzukic0387542019-03-08 17:31:14 +0100424
Lyn Hanf4730312019-06-18 11:18:58 -0700425 private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
Lyn Han3cd75d72020-02-15 19:10:12 -0800426
Joshua Tsujia2433db2020-03-12 17:56:22 -0400427 @Nullable
Lyn Han3cd75d72020-02-15 19:10:12 -0800428 private BubbleOverflow mBubbleOverflow;
Lyn Hanf4730312019-06-18 11:18:58 -0700429
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800430 private boolean mShouldShowUserEducation;
431 private boolean mAnimatingEducationAway;
432 private View mUserEducationView;
433
434 private boolean mShouldShowManageEducation;
435 private BubbleManageEducationView mManageEducationView;
436 private boolean mAnimatingManageEducationAway;
437
Issei Suzukic0387542019-03-08 17:31:14 +0100438 public BubbleStackView(Context context, BubbleData data,
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500439 @Nullable SurfaceSynchronizer synchronizer,
440 FloatingContentCoordinator floatingContentCoordinator) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800441 super(context);
442
Mady Mellorcfd06c12019-02-13 14:32:12 -0800443 mBubbleData = data;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800444 mInflater = LayoutInflater.from(context);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400445 mTouchHandler = new BubbleTouchHandler(this, data, context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800446 setOnTouchListener(mTouchHandler);
447
448 Resources res = getResources();
Mady Mellor70958542019-09-24 17:12:46 -0700449 mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800450 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Mady Mellor70958542019-09-24 17:12:46 -0700451 mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Lyn Han4a8efe32019-05-30 09:43:27 -0700452 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Mady Mellore9371bc2019-07-10 18:50:59 -0700453 mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800454 mExpandedAnimateXDistance =
455 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
456 mExpandedAnimateYDistance =
457 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
Lyn Han5aa27e22019-05-15 10:55:07 -0700458 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
459
Joshua Tsujif44347f2019-02-12 14:28:06 -0500460 mStatusBarHeight =
461 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500462 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800463
464 mDisplaySize = new Point();
465 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Mady Mellor9be3bed2019-08-21 17:26:26 -0700466 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700467 wm.getDefaultDisplay().getRealSize(mDisplaySize);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800468
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400469 mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
470
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700471 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800472 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800473
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400474 mStackAnimationController = new StackAnimationController(
475 floatingContentCoordinator, this::getBubbleCount);
Lyn Hanf4730312019-06-18 11:18:58 -0700476
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700477 mExpandedAnimationController = new ExpandedAnimationController(
Lyn Hanf4730312019-06-18 11:18:58 -0700478 mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation);
Issei Suzukic0387542019-03-08 17:31:14 +0100479 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800480
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800481 setUpUserEducation();
482
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800483 mBubbleContainer = new PhysicsAnimationLayout(context);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400484 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800485 mBubbleContainer.setElevation(elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800486 mBubbleContainer.setClipChildren(false);
487 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
488
Mady Mellor3dff9e62019-02-05 18:12:53 -0800489 mExpandedViewContainer = new FrameLayout(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800490 mExpandedViewContainer.setElevation(elevation);
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700491 mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding,
492 mExpandedViewPadding, mExpandedViewPadding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800493 mExpandedViewContainer.setClipChildren(false);
494 addView(mExpandedViewContainer);
495
Mady Mellor8bfe5412019-07-31 14:56:44 -0700496 setUpFlyout();
Joshua Tsuji6549e702019-05-02 13:13:16 -0400497 mFlyoutTransitionSpring.setSpring(new SpringForce()
Joshua Tsuji14e68552019-06-06 17:17:08 -0400498 .setStiffness(SpringForce.STIFFNESS_LOW)
Joshua Tsuji6549e702019-05-02 13:13:16 -0400499 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
500 mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
501
Joshua Tsujie48c4112020-02-26 14:36:25 -0500502 final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
Joshua Tsuji20103542020-02-18 14:06:28 -0500503 final View targetView = new DismissCircleView(context);
504 final FrameLayout.LayoutParams newParams =
Joshua Tsujie48c4112020-02-26 14:36:25 -0500505 new FrameLayout.LayoutParams(targetSize, targetSize);
Joshua Tsuji20103542020-02-18 14:06:28 -0500506 newParams.gravity = Gravity.CENTER;
507 targetView.setLayoutParams(newParams);
508 mDismissTargetAnimator = PhysicsAnimator.getInstance(targetView);
509
510 mDismissTargetContainer = new FrameLayout(context);
511 mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams(
Joshua Tsuji6549e702019-05-02 13:13:16 -0400512 MATCH_PARENT,
513 getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height),
514 Gravity.BOTTOM));
Joshua Tsuji20103542020-02-18 14:06:28 -0500515 mDismissTargetContainer.setClipChildren(false);
516 mDismissTargetContainer.addView(targetView);
517 mDismissTargetContainer.setVisibility(View.INVISIBLE);
518 addView(mDismissTargetContainer);
519
520 // Start translated down so the target springs up.
521 targetView.setTranslationY(
522 getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height));
523
524 // Save the MagneticTarget instance for the newly set up view - we'll add this to the
525 // MagnetizedObjects.
526 mMagneticTarget = new MagnetizedObject.MagneticTarget(targetView, mBubbleSize * 2);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400527
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800528 mExpandedViewXAnim =
529 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
530 mExpandedViewXAnim.setSpring(
531 new SpringForce()
532 .setStiffness(SpringForce.STIFFNESS_LOW)
533 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
534
535 mExpandedViewYAnim =
536 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y);
537 mExpandedViewYAnim.setSpring(
538 new SpringForce()
539 .setStiffness(SpringForce.STIFFNESS_LOW)
540 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
Mady Mellorbc078c22019-03-26 17:10:34 -0700541 mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> {
Lyn Han3cd75d72020-02-15 19:10:12 -0800542 if (mIsExpanded && mExpandedBubble != null) {
543 mExpandedBubble.getExpandedView().updateView();
Mady Mellorbc078c22019-03-26 17:10:34 -0700544 }
545 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800546
547 setClipChildren(false);
Mady Mellor217b2e92019-02-27 11:44:16 -0800548 setFocusable(true);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500549 mBubbleContainer.bringToFront();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800550
Lyn Hancd4f87e2020-02-19 20:33:45 -0800551 setUpOverflow();
Lyn Hanb58c7562020-01-07 14:29:20 -0800552
Mady Mellor5d8f1402019-02-21 18:23:52 -0800553 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
Mady Mellorbc078c22019-03-26 17:10:34 -0700554 if (!mIsExpanded || mIsExpansionAnimating) {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800555 return view.onApplyWindowInsets(insets);
556 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800557 mExpandedAnimationController.updateYPosition(
558 // Update the insets after we're done translating otherwise position
559 // calculation for them won't be correct.
Lyn Hanb58c7562020-01-07 14:29:20 -0800560 () -> {
Lyn Han3cd75d72020-02-15 19:10:12 -0800561 if (mExpandedBubble != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800562 mExpandedBubble.getExpandedView().updateInsets(insets);
563 }
564 });
Mady Mellor5d8f1402019-02-21 18:23:52 -0800565 return view.onApplyWindowInsets(insets);
566 });
Mark Renouf821e6782019-04-01 14:17:37 -0400567
Lyn Hanf4730312019-06-18 11:18:58 -0700568 mOrientationChangedListener =
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400569 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
Mady Mellor9be3bed2019-08-21 17:26:26 -0700570 mExpandedAnimationController.updateOrientation(mOrientation, mDisplaySize);
571 mStackAnimationController.updateOrientation(mOrientation);
572
573 // Reposition & adjust the height for new orientation
574 if (mIsExpanded) {
575 mExpandedViewContainer.setTranslationY(getExpandedViewY());
Lyn Han3cd75d72020-02-15 19:10:12 -0800576 if (mExpandedBubble != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800577 mExpandedBubble.getExpandedView().updateView();
578 }
Mady Mellor9be3bed2019-08-21 17:26:26 -0700579 }
580
581 // Need to update the padding around the view
582 WindowInsets insets = getRootWindowInsets();
583 int leftPadding = mExpandedViewPadding;
584 int rightPadding = mExpandedViewPadding;
585 if (insets != null) {
586 // Can't have the expanded view overlaying notches
587 int cutoutLeft = 0;
588 int cutoutRight = 0;
589 DisplayCutout cutout = insets.getDisplayCutout();
590 if (cutout != null) {
591 cutoutLeft = cutout.getSafeInsetLeft();
592 cutoutRight = cutout.getSafeInsetRight();
593 }
594 // Or overlaying nav or status bar
595 leftPadding += Math.max(cutoutLeft, insets.getStableInsetLeft());
596 rightPadding += Math.max(cutoutRight, insets.getStableInsetRight());
597 }
598 mExpandedViewContainer.setPadding(leftPadding, mExpandedViewPadding,
599 rightPadding, mExpandedViewPadding);
600
Lyn Hanf4730312019-06-18 11:18:58 -0700601 if (mIsExpanded) {
602 // Re-draw bubble row and pointer for new orientation.
603 mExpandedAnimationController.expandFromStack(() -> {
604 updatePointerPosition();
605 } /* after */);
606 }
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400607 if (mVerticalPosPercentBeforeRotation >= 0) {
608 mStackAnimationController.moveStackToSimilarPositionAfterRotation(
609 mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation);
610 }
Lyn Hanf4730312019-06-18 11:18:58 -0700611 removeOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400612 };
613
Mark Renouf821e6782019-04-01 14:17:37 -0400614 // This must be a separate OnDrawListener since it should be called for every draw.
615 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400616
617 final ColorMatrix animatedMatrix = new ColorMatrix();
618 final ColorMatrix darkenMatrix = new ColorMatrix();
619
620 mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f);
621 mDesaturateAndDarkenAnimator.addUpdateListener(animation -> {
622 final float animatedValue = (float) animation.getAnimatedValue();
623 animatedMatrix.setSaturation(animatedValue);
624
625 final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT;
626 darkenMatrix.setScale(
627 1f - animatedDarkenValue /* red */,
628 1f - animatedDarkenValue /* green */,
629 1f - animatedDarkenValue /* blue */,
630 1f /* alpha */);
631
632 // Concat the matrices so that the animatedMatrix both desaturates and darkens.
633 animatedMatrix.postConcat(darkenMatrix);
634
635 // Update the paint and apply it to the bubble container.
636 mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
637 mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
638 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800639 }
640
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800641 private void setUpUserEducation() {
642 if (mUserEducationView != null) {
643 removeView(mUserEducationView);
644 }
645 mShouldShowUserEducation = shouldShowBubblesEducation();
646 if (DEBUG_USER_EDUCATION) {
647 Log.d(TAG, "shouldShowUserEducation: " + mShouldShowUserEducation);
648 }
649 if (mShouldShowUserEducation) {
650 mUserEducationView = mInflater.inflate(R.layout.bubble_stack_user_education, this,
651 false /* attachToRoot */);
652 mUserEducationView.setVisibility(GONE);
653
654 final TypedArray ta = mContext.obtainStyledAttributes(
655 new int[] {android.R.attr.colorAccent,
656 android.R.attr.textColorPrimaryInverse});
657 final int bgColor = ta.getColor(0, Color.BLACK);
658 int textColor = ta.getColor(1, Color.WHITE);
659 ta.recycle();
660 textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
661
662 TextView title = mUserEducationView.findViewById(R.id.user_education_title);
663 TextView description = mUserEducationView.findViewById(R.id.user_education_description);
664 title.setTextColor(textColor);
665 description.setTextColor(textColor);
666
667 addView(mUserEducationView);
668 }
669
670 if (mManageEducationView != null) {
671 removeView(mManageEducationView);
672 }
673 mShouldShowManageEducation = shouldShowManageEducation();
674 if (DEBUG_USER_EDUCATION) {
675 Log.d(TAG, "shouldShowManageEducation: " + mShouldShowManageEducation);
676 }
677 if (mShouldShowManageEducation) {
678 mManageEducationView = (BubbleManageEducationView)
679 mInflater.inflate(R.layout.bubbles_manage_button_education, this,
680 false /* attachToRoot */);
681 mManageEducationView.setVisibility(GONE);
682 mManageEducationView.setElevation(mBubbleElevation);
683
684 addView(mManageEducationView);
Lyn Hanb58c7562020-01-07 14:29:20 -0800685 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800686 }
687
Mady Mellor8bfe5412019-07-31 14:56:44 -0700688 private void setUpFlyout() {
689 if (mFlyout != null) {
690 removeView(mFlyout);
691 }
692 mFlyout = new BubbleFlyoutView(getContext());
693 mFlyout.setVisibility(GONE);
694 mFlyout.animate()
695 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
696 .setInterpolator(new AccelerateDecelerateInterpolator());
697 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
698 }
699
Lyn Hancd4f87e2020-02-19 20:33:45 -0800700 private void setUpOverflow() {
Lyn Han8cc4bf82020-03-05 16:34:37 -0800701 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
702 return;
703 }
Lyn Hancd4f87e2020-02-19 20:33:45 -0800704 int overflowBtnIndex = 0;
705 if (mBubbleOverflow == null) {
Mady Mellor0122cc92020-02-27 12:15:39 -0800706 mBubbleOverflow = new BubbleOverflow(getContext());
707 mBubbleOverflow.setUpOverflow(mBubbleContainer, this);
Lyn Hancd4f87e2020-02-19 20:33:45 -0800708 } else {
709 mBubbleContainer.removeView(mBubbleOverflow.getBtn());
710 mBubbleOverflow.updateIcon(mContext, this);
711 overflowBtnIndex = mBubbleContainer.getChildCount() - 1;
712 }
713 mBubbleContainer.addView(mBubbleOverflow.getBtn(), overflowBtnIndex,
714 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
715
716 }
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700717 /**
Lyn Han02cca812019-04-02 16:27:32 -0700718 * Handle theme changes.
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700719 */
Lyn Han02cca812019-04-02 16:27:32 -0700720 public void onThemeChanged() {
Mady Mellor8bfe5412019-07-31 14:56:44 -0700721 setUpFlyout();
Lyn Hancd4f87e2020-02-19 20:33:45 -0800722 setUpOverflow();
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800723 setUpUserEducation();
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700724 }
725
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400726 /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
Lyn Hanf4730312019-06-18 11:18:58 -0700727 public void onOrientationChanged(int orientation) {
728 mOrientation = orientation;
729
Mady Mellore19353d2019-08-21 17:25:02 -0700730 // Display size is based on the rotation device was in when requested, we should update it
Mady Mellor9be3bed2019-08-21 17:26:26 -0700731 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700732 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
733 wm.getDefaultDisplay().getRealSize(mDisplaySize);
734
Mady Mellor818eef02019-08-16 16:12:29 -0700735 // Some resources change depending on orientation
736 Resources res = getContext().getResources();
737 mStatusBarHeight = res.getDimensionPixelSize(
738 com.android.internal.R.dimen.status_bar_height);
739 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
740
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400741 final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion();
742 mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide();
743 mVerticalPosPercentBeforeRotation =
744 (mStackAnimationController.getStackPosition().y - allowablePos.top)
745 / (allowablePos.bottom - allowablePos.top);
Lyn Hanf4730312019-06-18 11:18:58 -0700746 addOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400747 hideFlyoutImmediate();
748 }
749
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800750 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800751 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
752 getBoundsOnScreen(outRect);
753 }
754
755 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800756 protected void onDetachedFromWindow() {
757 super.onDetachedFromWindow();
758 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
759 }
760
761 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800762 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
763 super.onInitializeAccessibilityNodeInfoInternal(info);
Lyn Hane68d0912019-05-02 18:28:01 -0700764
765 // Custom actions.
766 AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left,
767 getContext().getResources()
768 .getString(R.string.bubble_accessibility_action_move_top_left));
769 info.addAction(moveTopLeft);
770
771 AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right,
772 getContext().getResources()
773 .getString(R.string.bubble_accessibility_action_move_top_right));
774 info.addAction(moveTopRight);
775
776 AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left,
777 getContext().getResources()
778 .getString(R.string.bubble_accessibility_action_move_bottom_left));
779 info.addAction(moveBottomLeft);
780
781 AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right,
782 getContext().getResources()
783 .getString(R.string.bubble_accessibility_action_move_bottom_right));
784 info.addAction(moveBottomRight);
785
786 // Default actions.
787 info.addAction(AccessibilityAction.ACTION_DISMISS);
Mady Mellor217b2e92019-02-27 11:44:16 -0800788 if (mIsExpanded) {
Lyn Hane68d0912019-05-02 18:28:01 -0700789 info.addAction(AccessibilityAction.ACTION_COLLAPSE);
Mady Mellor217b2e92019-02-27 11:44:16 -0800790 } else {
Lyn Hane68d0912019-05-02 18:28:01 -0700791 info.addAction(AccessibilityAction.ACTION_EXPAND);
Mady Mellor217b2e92019-02-27 11:44:16 -0800792 }
793 }
794
795 @Override
796 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
797 if (super.performAccessibilityActionInternal(action, arguments)) {
798 return true;
799 }
Lyn Hane68d0912019-05-02 18:28:01 -0700800 final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion();
801
802 // R constants are not final so we cannot use switch-case here.
803 if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
804 mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION);
805 return true;
806 } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
807 mBubbleData.setExpanded(false);
808 return true;
809 } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
810 mBubbleData.setExpanded(true);
811 return true;
812 } else if (action == R.id.action_move_top_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500813 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -0700814 return true;
815 } else if (action == R.id.action_move_top_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500816 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -0700817 return true;
818 } else if (action == R.id.action_move_bottom_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500819 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -0700820 return true;
821 } else if (action == R.id.action_move_bottom_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500822 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -0700823 return true;
Mady Mellor217b2e92019-02-27 11:44:16 -0800824 }
825 return false;
826 }
827
Lyn Han6c40fe72019-05-08 14:06:33 -0700828 /**
829 * Update content description for a11y TalkBack.
830 */
831 public void updateContentDescription() {
832 if (mBubbleData.getBubbles().isEmpty()) {
833 return;
834 }
835 Bubble topBubble = mBubbleData.getBubbles().get(0);
836 String appName = topBubble.getAppName();
Ned Burns00b4b2d2019-10-17 22:09:27 -0400837 Notification notification = topBubble.getEntry().getSbn().getNotification();
Lyn Han6c40fe72019-05-08 14:06:33 -0700838 CharSequence titleCharSeq = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
Lyn Han93cd3132020-02-18 18:22:05 -0800839 String titleStr = getResources().getString(R.string.notification_bubble_title);
Lyn Han6c40fe72019-05-08 14:06:33 -0700840 if (titleCharSeq != null) {
841 titleStr = titleCharSeq.toString();
842 }
843 int moreCount = mBubbleContainer.getChildCount() - 1;
844
845 // Example: Title from app name.
846 String singleDescription = getResources().getString(
847 R.string.bubble_content_description_single, titleStr, appName);
848
849 // Example: Title from app name and 4 more.
850 String stackDescription = getResources().getString(
851 R.string.bubble_content_description_stack, titleStr, appName, moreCount);
852
853 if (mIsExpanded) {
854 // TODO(b/129522932) - update content description for each bubble in expanded view.
855 } else {
856 // Collapsed stack.
857 if (moreCount > 0) {
858 mBubbleContainer.setContentDescription(stackDescription);
859 } else {
860 mBubbleContainer.setContentDescription(singleDescription);
861 }
862 }
863 }
864
Mark Renouf821e6782019-04-01 14:17:37 -0400865 private void updateSystemGestureExcludeRects() {
866 // Exclude the region occupied by the first BubbleView in the stack
867 Rect excludeZone = mSystemGestureExclusionRects.get(0);
Lyn Hanc47e1712020-01-28 21:43:34 -0800868 if (getBubbleCount() > 0) {
Mark Renouf821e6782019-04-01 14:17:37 -0400869 View firstBubble = mBubbleContainer.getChildAt(0);
870 excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(),
871 firstBubble.getBottom());
872 excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f),
873 (int) (firstBubble.getTranslationY() + 0.5f));
874 mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects);
875 } else {
876 excludeZone.setEmpty();
877 mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList());
878 }
879 }
880
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800881 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -0800882 * Sets the listener to notify when the bubble stack is expanded.
883 */
884 public void setExpandListener(BubbleController.BubbleExpandListener listener) {
885 mExpandListener = listener;
886 }
887
888 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800889 * Whether the stack of bubbles is expanded or not.
890 */
891 public boolean isExpanded() {
892 return mIsExpanded;
893 }
894
895 /**
Mady Mellor047e24e2019-08-05 11:35:40 -0700896 * Whether the stack of bubbles is animating to or from expansion.
897 */
898 public boolean isExpansionAnimating() {
899 return mIsExpansionAnimating;
900 }
901
902 /**
Mady Mellorb8aaf972019-11-26 10:28:00 -0800903 * The {@link BadgedImageView} that is expanded, null if one does not exist.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800904 */
Lyn Han3cd75d72020-02-15 19:10:12 -0800905 View getExpandedBubbleView() {
Mady Mellored99c272019-06-13 15:58:30 -0700906 return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800907 }
908
909 /**
910 * The {@link Bubble} that is expanded, null if one does not exist.
911 */
Lyn Han3cd75d72020-02-15 19:10:12 -0800912 @Nullable
Lyn Han9f66c3b2020-03-05 23:59:29 -0800913 BubbleViewProvider getExpandedBubble() {
914 return mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800915 }
916
Mark Renouf71a3af62019-04-08 15:02:54 -0400917 // via BubbleData.Listener
918 void addBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200919 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400920 Log.d(TAG, "addBubble: " + bubble);
921 }
Joshua Tsujib35f5912019-07-24 16:15:21 -0400922
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800923 if (getBubbleCount() == 0 && mShouldShowUserEducation) {
924 // Override the default stack position if we're showing user education.
925 mStackAnimationController.setStackPosition(
926 mStackAnimationController.getDefaultStartPosition());
927 }
928
Lyn Hanc47e1712020-01-28 21:43:34 -0800929 if (getBubbleCount() == 0) {
Joshua Tsujib35f5912019-07-24 16:15:21 -0400930 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
931 }
932
Joshua Tsujib35f5912019-07-24 16:15:21 -0400933 // Set the dot position to the opposite of the side the stack is resting on, since the stack
934 // resting slightly off-screen would result in the dot also being off-screen.
935 bubble.getIconView().setDotPosition(
936 !mStackOnLeftOrWillBe /* onLeft */, false /* animate */);
937
Mady Mellored99c272019-06-13 15:58:30 -0700938 mBubbleContainer.addView(bubble.getIconView(), 0,
Mark Renouf71a3af62019-04-08 15:02:54 -0400939 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellored99c272019-06-13 15:58:30 -0700940 ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters);
Mark Renoufba5ab512019-05-02 15:21:01 -0400941 animateInFlyoutForBubble(bubble);
Lyn Hanb58c7562020-01-07 14:29:20 -0800942 updatePointerPosition();
943 updateOverflowBtnVisibility( /*apply */ true);
Mark Renouf71a3af62019-04-08 15:02:54 -0400944 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -0800945 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
Mark Renouf71a3af62019-04-08 15:02:54 -0400946 }
947
948 // via BubbleData.Listener
949 void removeBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200950 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400951 Log.d(TAG, "removeBubble: " + bubble);
952 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400953 // Remove it from the views
Mady Mellored99c272019-06-13 15:58:30 -0700954 int removedIndex = mBubbleContainer.indexOfChild(bubble.getIconView());
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400955 if (removedIndex >= 0) {
956 mBubbleContainer.removeViewAt(removedIndex);
Mark Renoufc19b4732019-06-26 12:08:33 -0400957 bubble.cleanupExpandedState();
Lyn Han1e19d7f2020-02-05 19:10:58 -0800958 bubble.setInflated(false);
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -0800959 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400960 } else {
961 Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
962 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800963 updateOverflowBtnVisibility(/* apply */ true);
964 }
965
966 private void updateOverflowBtnVisibility(boolean apply) {
Lyn Han8cc4bf82020-03-05 16:34:37 -0800967 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
968 return;
969 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800970 if (mIsExpanded) {
971 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Hanc47e1712020-01-28 21:43:34 -0800972 Log.d(TAG, "Show overflow button.");
Lyn Hanb58c7562020-01-07 14:29:20 -0800973 }
Lyn Han3cd75d72020-02-15 19:10:12 -0800974 mBubbleOverflow.setBtnVisible(VISIBLE);
Lyn Hanb58c7562020-01-07 14:29:20 -0800975 if (apply) {
Lyn Hanc47e1712020-01-28 21:43:34 -0800976 mExpandedAnimationController.expandFromStack(() -> {
977 updatePointerPosition();
978 } /* after */);
Lyn Hanb58c7562020-01-07 14:29:20 -0800979 }
980 } else {
981 if (DEBUG_BUBBLE_STACK_VIEW) {
982 Log.d(TAG, "Collapsed. Hide overflow button.");
983 }
Lyn Han3cd75d72020-02-15 19:10:12 -0800984 mBubbleOverflow.setBtnVisible(GONE);
Lyn Hanb58c7562020-01-07 14:29:20 -0800985 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400986 }
987
988 // via BubbleData.Listener
989 void updateBubble(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400990 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400991 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -0800992 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
Mark Renouf71a3af62019-04-08 15:02:54 -0400993 }
994
Mark Renoufba5ab512019-05-02 15:21:01 -0400995 public void updateBubbleOrder(List<Bubble> bubbles) {
996 for (int i = 0; i < bubbles.size(); i++) {
997 Bubble bubble = bubbles.get(i);
Mady Mellored99c272019-06-13 15:58:30 -0700998 mBubbleContainer.reorderView(bubble.getIconView(), i);
Mark Renoufba5ab512019-05-02 15:21:01 -0400999 }
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001000
1001 updateBubbleZOrdersAndDotPosition(false /* animate */);
Mark Renoufba5ab512019-05-02 15:21:01 -04001002 }
1003
Lyn Han99e457f2020-01-31 10:14:19 -08001004 void showOverflow() {
Lyn Han3cd75d72020-02-15 19:10:12 -08001005 setSelectedBubble(mBubbleOverflow);
Lyn Han99e457f2020-01-31 10:14:19 -08001006 }
1007
Mady Melloredd4ee12019-01-18 10:45:11 -08001008 /**
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001009 * Changes the currently selected bubble. If the stack is already expanded, the newly selected
1010 * bubble will be shown immediately. This does not change the expanded state or change the
1011 * position of any bubble.
1012 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001013 // via BubbleData.Listener
Lyn Han3cd75d72020-02-15 19:10:12 -08001014 public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001015 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001016 Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
1017 }
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001018 if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) {
1019 return;
1020 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001021 final BubbleViewProvider previouslySelected = mExpandedBubble;
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001022 mExpandedBubble = bubbleToSelect;
Issei Suzukicac2a502019-04-16 16:52:50 +02001023
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001024 if (mIsExpanded) {
1025 // Make the container of the expanded view transparent before removing the expanded view
1026 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
1027 // expanded view becomes visible on the screen. See b/126856255
1028 mExpandedViewContainer.setAlpha(0.0f);
1029 mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
Lyn Han3cd75d72020-02-15 19:10:12 -08001030 previouslySelected.setContentVisibility(false);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001031 updateExpandedBubble();
1032 updatePointerPosition();
1033 requestUpdate();
Lyn Han3cd75d72020-02-15 19:10:12 -08001034
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001035 logBubbleEvent(previouslySelected,
1036 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
1037 logBubbleEvent(bubbleToSelect, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
Mady Mellor99a302602019-06-14 11:39:56 -07001038 notifyExpansionChanged(previouslySelected, false /* expanded */);
1039 notifyExpansionChanged(bubbleToSelect, true /* expanded */);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001040 });
1041 }
1042 }
1043
1044 /**
1045 * Changes the expanded state of the stack.
1046 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001047 * @param shouldExpand whether the bubble stack should appear expanded
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001048 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001049 // via BubbleData.Listener
1050 public void setExpanded(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001051 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001052 Log.d(TAG, "setExpanded: " + shouldExpand);
1053 }
Lyn Han285ad302019-05-29 19:01:39 -07001054 if (shouldExpand == mIsExpanded) {
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001055 return;
1056 }
Lyn Han285ad302019-05-29 19:01:39 -07001057 if (mIsExpanded) {
1058 animateCollapse();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001059 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001060 } else {
Lyn Han285ad302019-05-29 19:01:39 -07001061 animateExpansion();
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001062 // TODO: move next line to BubbleData
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001063 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
1064 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001065 }
Mady Mellor99a302602019-06-14 11:39:56 -07001066 notifyExpansionChanged(mExpandedBubble, mIsExpanded);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001067 }
1068
1069 /**
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001070 * If necessary, shows the user education view for the bubble stack. This appears the first
1071 * time a user taps on a bubble.
1072 *
1073 * @return true if user education was shown, false otherwise.
1074 */
1075 private boolean maybeShowStackUserEducation() {
1076 if (mShouldShowUserEducation && mUserEducationView.getVisibility() != VISIBLE) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001077 mUserEducationView.setAlpha(0);
1078 mUserEducationView.setVisibility(VISIBLE);
1079 // Post so we have height of mUserEducationView
1080 mUserEducationView.post(() -> {
1081 final int viewHeight = mUserEducationView.getHeight();
1082 PointF stackPosition = mStackAnimationController.getDefaultStartPosition();
1083 final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2);
1084 mUserEducationView.setTranslationY(translationY);
1085 mUserEducationView.animate()
1086 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1087 .setInterpolator(FAST_OUT_SLOW_IN)
1088 .alpha(1);
1089 });
1090 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, true);
1091 return true;
1092 }
1093 return false;
1094 }
1095
1096 /**
1097 * If necessary, hides the user education view for the bubble stack.
1098 *
1099 * @param fromExpansion if true this indicates the hide is happening due to the bubble being
1100 * expanded, false if due to a touch outside of the bubble stack.
1101 */
1102 void hideStackUserEducation(boolean fromExpansion) {
1103 if (mShouldShowUserEducation
1104 && mUserEducationView.getVisibility() == VISIBLE
1105 && !mAnimatingEducationAway) {
1106 mAnimatingEducationAway = true;
1107 mUserEducationView.animate()
1108 .alpha(0)
1109 .setDuration(fromExpansion
1110 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1111 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1112 .withEndAction(() -> {
1113 mAnimatingEducationAway = false;
1114 mShouldShowUserEducation = shouldShowBubblesEducation();
1115 mUserEducationView.setVisibility(GONE);
1116 });
1117 }
1118 }
1119
1120 /**
1121 * If necessary, toggles the user education view for the manage button. This is shown when the
1122 * bubble stack is expanded for the first time.
1123 *
1124 * @param show whether the user education view should show or not.
1125 */
1126 void maybeShowManageEducation(boolean show) {
1127 if (mManageEducationView == null) {
1128 return;
1129 }
1130 if (show
1131 && mShouldShowManageEducation
1132 && mManageEducationView.getVisibility() != VISIBLE
1133 && mIsExpanded) {
1134 mManageEducationView.setAlpha(0);
1135 mManageEducationView.setVisibility(VISIBLE);
1136 mManageEducationView.post(() -> {
1137 final Rect position =
1138 mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen();
1139 final int viewHeight = mManageEducationView.getManageViewHeight();
1140 final int inset = getResources().getDimensionPixelSize(
1141 R.dimen.bubbles_manage_education_top_inset);
1142 mManageEducationView.bringToFront();
1143 mManageEducationView.setManageViewPosition(position.left,
1144 position.top - viewHeight + inset);
1145 mManageEducationView.setPointerPosition(position.centerX() - position.left);
1146 mManageEducationView.animate()
1147 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1148 .setInterpolator(FAST_OUT_SLOW_IN).alpha(1);
1149 });
1150 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, true);
1151 } else if (!show
1152 && mManageEducationView.getVisibility() == VISIBLE
1153 && !mAnimatingManageEducationAway) {
1154 mManageEducationView.animate()
1155 .alpha(0)
1156 .setDuration(mIsExpansionAnimating
1157 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1158 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1159 .withEndAction(() -> {
1160 mAnimatingManageEducationAway = false;
1161 mShouldShowManageEducation = shouldShowManageEducation();
1162 mManageEducationView.setVisibility(GONE);
1163 });
1164 }
1165 }
1166
Joshua Tsuji20103542020-02-18 14:06:28 -05001167 /*
1168 * Sets the action to run to dismiss the currently dragging object (either the stack or an
1169 * individual bubble).
1170 */
1171 public void setReleasedInDismissTargetAction(Runnable action) {
1172 mReleasedInDismissTargetAction = action;
1173 }
1174
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001175 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001176 * Dismiss the stack of bubbles.
Lyn Han1b4f25e2019-06-11 13:56:34 -07001177 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001178 * @deprecated
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001179 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001180 @Deprecated
Mady Mellorc3d7d062019-03-28 16:13:05 -07001181 void stackDismissed(int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001182 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001183 Log.d(TAG, "stackDismissed: reason=" + reason);
1184 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001185 mBubbleData.dismissAll(reason);
Steven Wua254dab2019-01-29 11:30:39 -05001186 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001187 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001188 }
1189
1190 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001191 * @return the view the touch event is on
1192 */
1193 @Nullable
1194 public View getTargetView(MotionEvent event) {
1195 float x = event.getRawX();
1196 float y = event.getRawY();
1197 if (mIsExpanded) {
1198 if (isIntersecting(mBubbleContainer, x, y)) {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001199 if (BubbleExperimentConfig.allowBubbleOverflow(mContext)
1200 && isIntersecting(mBubbleOverflow.getBtn(), x, y)) {
Lyn Han3cd75d72020-02-15 19:10:12 -08001201 return mBubbleOverflow.getBtn();
Lyn Han99e457f2020-01-31 10:14:19 -08001202 }
Mady Mellor47b11e32019-07-11 19:06:21 -07001203 // Could be tapping or dragging a bubble while expanded
Lyn Hanc47e1712020-01-28 21:43:34 -08001204 for (int i = 0; i < getBubbleCount(); i++) {
Mady Mellorb8aaf972019-11-26 10:28:00 -08001205 BadgedImageView view = (BadgedImageView) mBubbleContainer.getChildAt(i);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001206 if (isIntersecting(view, x, y)) {
1207 return view;
1208 }
1209 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001210 }
Mady Mellor47b11e32019-07-11 19:06:21 -07001211 BubbleExpandedView bev = (BubbleExpandedView) mExpandedViewContainer.getChildAt(0);
1212 if (bev.intersectingTouchableContent((int) x, (int) y)) {
1213 return bev;
1214 }
1215 // Outside of the parts we care about.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001216 return null;
Joshua Tsuji6549e702019-05-02 13:13:16 -04001217 } else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001218 return mFlyout;
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001219 } else if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
1220 View bubbleChild = mBubbleContainer.getChildAt(0);
1221 if (isIntersecting(bubbleChild, x, y)) {
1222 return this;
1223 } else if (isIntersecting(mUserEducationView, x, y)) {
1224 return mUserEducationView;
1225 } else {
1226 return null;
1227 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001228 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001229
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001230 // If it wasn't an individual bubble in the expanded state, or the flyout, it's the stack.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001231 return this;
1232 }
1233
Mark Renouf71a3af62019-04-08 15:02:54 -04001234 View getFlyoutView() {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001235 return mFlyout;
1236 }
1237
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001238 /**
Joshua Tsujif5c6a9c2020-02-25 17:47:59 -05001239 * @deprecated use {@link #setExpanded(boolean)} and
1240 * {@link BubbleData#setSelectedBubble(Bubble)}
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001241 */
1242 @Deprecated
1243 @MainThread
Mady Mellor9801e852019-01-22 14:50:28 -08001244 void collapseStack(Runnable endRunnable) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001245 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001246 Log.d(TAG, "collapseStack(endRunnable)");
1247 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001248 mBubbleData.setExpanded(false);
Mady Mellor9801e852019-01-22 14:50:28 -08001249 // TODO - use the runnable at end of animation
1250 endRunnable.run();
1251 }
1252
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001253 void showExpandedViewContents(int displayId) {
1254 if (mExpandedBubble != null
1255 && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
1256 mExpandedBubble.setContentVisibility(true);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001257 }
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001258 }
1259
Lyn Han285ad302019-05-29 19:01:39 -07001260 private void beforeExpandedViewAnimation() {
1261 hideFlyoutImmediate();
1262 updateExpandedBubble();
1263 updateExpandedView();
1264 mIsExpansionAnimating = true;
1265 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001266
Lyn Han285ad302019-05-29 19:01:39 -07001267 private void afterExpandedViewAnimation() {
1268 updateExpandedView();
1269 mIsExpansionAnimating = false;
1270 requestUpdate();
1271 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001272
Lyn Han285ad302019-05-29 19:01:39 -07001273 private void animateCollapse() {
1274 mIsExpanded = false;
Lyn Han3cd75d72020-02-15 19:10:12 -08001275 final BubbleViewProvider previouslySelected = mExpandedBubble;
Lyn Han285ad302019-05-29 19:01:39 -07001276 beforeExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001277 maybeShowManageEducation(false);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001278
Lyn Hanb58c7562020-01-07 14:29:20 -08001279 if (DEBUG_BUBBLE_STACK_VIEW) {
1280 Log.d(TAG, "animateCollapse");
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001281 Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
Lyn Han9f66c3b2020-03-05 23:59:29 -08001282 mExpandedBubble));
Lyn Hanb58c7562020-01-07 14:29:20 -08001283 }
1284 updateOverflowBtnVisibility(/* apply */ false);
Lyn Han285ad302019-05-29 19:01:39 -07001285 mBubbleContainer.cancelAllAnimations();
1286 mExpandedAnimationController.collapseBackToStack(
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001287 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
1288 /* collapseTo */,
Lyn Han285ad302019-05-29 19:01:39 -07001289 () -> {
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001290 mBubbleContainer.setActiveController(mStackAnimationController);
Lyn Han285ad302019-05-29 19:01:39 -07001291 afterExpandedViewAnimation();
Lyn Han3cd75d72020-02-15 19:10:12 -08001292 previouslySelected.setContentVisibility(false);
Lyn Han285ad302019-05-29 19:01:39 -07001293 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001294
Lyn Han285ad302019-05-29 19:01:39 -07001295 mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
1296 mExpandedViewYAnim.animateToFinalPosition(getCollapsedY());
1297 mExpandedViewContainer.animate()
1298 .setDuration(100)
1299 .alpha(0f);
1300 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001301
Lyn Han285ad302019-05-29 19:01:39 -07001302 private void animateExpansion() {
1303 mIsExpanded = true;
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001304 hideStackUserEducation(true /* fromExpansion */);
Lyn Han285ad302019-05-29 19:01:39 -07001305 beforeExpandedViewAnimation();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001306
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001307 mBubbleContainer.setActiveController(mExpandedAnimationController);
Lyn Hanb58c7562020-01-07 14:29:20 -08001308 updateOverflowBtnVisibility(/* apply */ false);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001309 mExpandedAnimationController.expandFromStack(() -> {
1310 updatePointerPosition();
1311 afterExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001312 maybeShowManageEducation(true);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001313 } /* after */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001314
Lyn Han285ad302019-05-29 19:01:39 -07001315 mExpandedViewContainer.setTranslationX(getCollapsedX());
1316 mExpandedViewContainer.setTranslationY(getCollapsedY());
1317 mExpandedViewContainer.setAlpha(0f);
1318
1319 mExpandedViewXAnim.animateToFinalPosition(0f);
1320 mExpandedViewYAnim.animateToFinalPosition(getExpandedViewY());
1321 mExpandedViewContainer.animate()
1322 .setDuration(100)
1323 .alpha(1f);
1324 }
1325
1326 private float getCollapsedX() {
1327 return mStackAnimationController.getStackPosition().x < getWidth() / 2
1328 ? -mExpandedAnimateXDistance
1329 : mExpandedAnimateXDistance;
1330 }
1331
1332 private float getCollapsedY() {
1333 return Math.min(mStackAnimationController.getStackPosition().y,
1334 mExpandedAnimateYDistance);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001335 }
1336
Lyn Han3cd75d72020-02-15 19:10:12 -08001337 private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) {
Mady Mellor99a302602019-06-14 11:39:56 -07001338 if (mExpandListener != null && bubble != null) {
1339 mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey());
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001340 }
1341 }
1342
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001343 /** Return the BubbleView at the given index from the bubble container. */
Mady Mellorb8aaf972019-11-26 10:28:00 -08001344 public BadgedImageView getBubbleAt(int i) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001345 return getBubbleCount() > i
Mady Mellorb8aaf972019-11-26 10:28:00 -08001346 ? (BadgedImageView) mBubbleContainer.getChildAt(i)
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001347 : null;
1348 }
1349
Joshua Tsujia19515f2019-02-13 18:02:29 -05001350 /** Moves the bubbles out of the way if they're going to be over the keyboard. */
1351 public void onImeVisibilityChanged(boolean visible, int height) {
Mady Mellordf611cf2019-08-21 17:28:49 -07001352 mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0);
Joshua Tsuji4b395912019-04-19 17:18:40 -04001353
Joshua Tsujiff6b0f22020-03-09 14:55:19 -04001354 if (!mIsExpanded && getBubbleCount() > 0) {
1355 final float stackDestinationY =
1356 mStackAnimationController.animateForImeVisibility(visible);
1357
1358 // How far the stack is animating due to IME, we'll just animate the flyout by that
1359 // much too.
1360 final float stackDy =
1361 stackDestinationY - mStackAnimationController.getStackPosition().y;
1362
1363 // If the flyout is visible, translate it along with the bubble stack.
1364 if (mFlyout.getVisibility() == VISIBLE) {
1365 PhysicsAnimator.getInstance(mFlyout)
1366 .spring(DynamicAnimation.TRANSLATION_Y,
1367 mFlyout.getTranslationY() + stackDy,
1368 FLYOUT_IME_ANIMATION_SPRING_CONFIG)
1369 .start();
1370 }
Joshua Tsujia19515f2019-02-13 18:02:29 -05001371 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001372 }
1373
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001374 /** Called when the collapsed stack is tapped on. */
1375 void onStackTapped() {
1376 if (!maybeShowStackUserEducation()) {
1377 mBubbleData.setExpanded(true);
1378 }
1379 }
1380
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001381 /** Called when a drag operation on an individual bubble has started. */
Joshua Tsuji442b6272019-02-08 13:23:43 -05001382 public void onBubbleDragStart(View bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001383 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001384 Log.d(TAG, "onBubbleDragStart: bubble=" + bubble);
1385 }
Joshua Tsujibc7744b2020-03-04 15:04:59 -05001386
Joshua Tsujia2433db2020-03-12 17:56:22 -04001387 if (mBubbleOverflow != null && bubble.equals(mBubbleOverflow.getIconView())) {
Joshua Tsujibc7744b2020-03-04 15:04:59 -05001388 return;
1389 }
1390
Joshua Tsuji20103542020-02-18 14:06:28 -05001391 mExpandedAnimationController.prepareForBubbleDrag(bubble, mMagneticTarget);
1392
1393 // We're dragging an individual bubble, so set the magnetized object to the magnetized
1394 // bubble.
1395 mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
1396 mMagnetizedObject.setMagnetListener(mIndividualBubbleMagnetListener);
Joshua Tsujidb1d2e882020-03-10 15:09:08 -04001397
1398 maybeShowManageEducation(false);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001399 }
1400
1401 /** Called with the coordinates to which an individual bubble has been dragged. */
Joshua Tsuji442b6272019-02-08 13:23:43 -05001402 public void onBubbleDragged(View bubble, float x, float y) {
Joshua Tsujia2433db2020-03-12 17:56:22 -04001403 if (!mIsExpanded || mIsExpansionAnimating
1404 || (mBubbleOverflow != null && bubble.equals(mBubbleOverflow.getIconView()))) {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001405 return;
1406 }
1407
1408 mExpandedAnimationController.dragBubbleOut(bubble, x, y);
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001409 springInDismissTarget();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001410 }
1411
1412 /** Called when a drag operation on an individual bubble has finished. */
Joshua Tsuji442b6272019-02-08 13:23:43 -05001413 public void onBubbleDragFinish(
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001414 View bubble, float x, float y, float velX, float velY) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001415 if (DEBUG_BUBBLE_STACK_VIEW) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001416 Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001417 }
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001418
Joshua Tsujia2433db2020-03-12 17:56:22 -04001419 if (!mIsExpanded || mIsExpansionAnimating
1420 || (mBubbleOverflow != null && bubble.equals(mBubbleOverflow.getIconView()))) {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001421 return;
1422 }
1423
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001424 mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
Lyn Han634483c2019-06-28 16:52:47 -07001425 hideDismissTarget();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001426 }
1427
Joshua Tsuji7be4c612020-03-04 14:58:14 -05001428 /** Expands the clicked bubble. */
1429 public void expandBubble(Bubble bubble) {
1430 if (bubble.equals(mBubbleData.getSelectedBubble())) {
1431 // If the bubble we're supposed to expand is the selected bubble, that means the
1432 // overflow bubble is currently expanded. Don't tell BubbleData to set this bubble as
1433 // selected, since it already is. Just call the stack's setSelectedBubble to expand it.
1434 setSelectedBubble(bubble);
1435 } else {
1436 mBubbleData.setSelectedBubble(bubble);
1437 }
1438 }
1439
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001440 void onDragStart() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001441 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001442 Log.d(TAG, "onDragStart()");
1443 }
Mady Mellorbc078c22019-03-26 17:10:34 -07001444 if (mIsExpanded || mIsExpansionAnimating) {
Lyn Hanb58c7562020-01-07 14:29:20 -08001445 if (DEBUG_BUBBLE_STACK_VIEW) {
1446 Log.d(TAG, "mIsExpanded or mIsExpansionAnimating");
1447 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001448 return;
1449 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001450 mStackAnimationController.cancelStackPositionAnimations();
Joshua Tsujic36ee6f2019-05-28 17:00:16 -04001451 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001452 hideFlyoutImmediate();
1453
Joshua Tsuji20103542020-02-18 14:06:28 -05001454 // Since we're dragging the stack, set the magnetized object to the magnetized stack.
1455 mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
1456 mMagnetizedObject.setMagnetListener(mStackMagnetListener);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001457 }
1458
1459 void onDragged(float x, float y) {
Mady Mellorbc078c22019-03-26 17:10:34 -07001460 if (mIsExpanded || mIsExpansionAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001461 return;
1462 }
1463
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001464 hideStackUserEducation(false /* fromExpansion */);
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001465 springInDismissTarget();
1466 mStackAnimationController.moveStackFromTouch(x, y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001467 }
1468
1469 void onDragFinish(float x, float y, float velX, float velY) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001470 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001471 Log.d(TAG, "onDragFinish");
1472 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001473
Mady Mellorbc078c22019-03-26 17:10:34 -07001474 if (mIsExpanded || mIsExpansionAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001475 return;
1476 }
1477
Joshua Tsuji6549e702019-05-02 13:13:16 -04001478 final float newStackX = mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY);
Steven Wua254dab2019-01-29 11:30:39 -05001479 logBubbleEvent(null /* no bubble associated with bubble stack move */,
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001480 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001481
Joshua Tsuji6549e702019-05-02 13:13:16 -04001482 mStackOnLeftOrWillBe = newStackX <= 0;
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001483 updateBubbleZOrdersAndDotPosition(true /* animate */);
Lyn Han634483c2019-06-28 16:52:47 -07001484 hideDismissTarget();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001485 }
1486
Joshua Tsuji6549e702019-05-02 13:13:16 -04001487 void onFlyoutDragStart() {
1488 mFlyout.removeCallbacks(mHideFlyout);
1489 }
1490
1491 void onFlyoutDragged(float deltaX) {
Joshua Tsuji8e05aab2019-08-22 14:57:50 -04001492 // This shouldn't happen, but if it does, just wait until the flyout lays out. This method
1493 // is continually called.
1494 if (mFlyout.getWidth() <= 0) {
1495 return;
1496 }
1497
Joshua Tsuji6549e702019-05-02 13:13:16 -04001498 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1499 mFlyoutDragDeltaX = deltaX;
1500
1501 final float collapsePercent =
1502 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth();
1503 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent)));
1504
Lyn Han61d5d562019-07-01 17:39:38 -07001505 // Calculate how to translate the flyout if it has been dragged too far in either direction.
Joshua Tsuji6549e702019-05-02 13:13:16 -04001506 float overscrollTranslation = 0f;
1507 if (collapsePercent < 0f || collapsePercent > 1f) {
1508 // Whether we are more than 100% transitioned to the dot.
1509 final boolean overscrollingPastDot = collapsePercent > 1f;
1510
1511 // Whether we are overscrolling physically to the left - this can either be pulling the
1512 // flyout away from the stack (if the stack is on the right) or pushing it to the left
1513 // after it has already become the dot.
1514 final boolean overscrollingLeft =
1515 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001516 overscrollTranslation =
1517 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1)
1518 * (overscrollingLeft ? -1 : 1)
1519 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR
Lyn Han522e9ff2019-05-17 13:26:13 -07001520 // Attenuate the smaller dot less than the larger flyout.
1521 / (overscrollingPastDot ? 2 : 1)));
Joshua Tsuji6549e702019-05-02 13:13:16 -04001522 }
1523
1524 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
1525 }
1526
Joshua Tsuji14e68552019-06-06 17:17:08 -04001527 void onFlyoutTapped() {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001528 if (maybeShowStackUserEducation()) {
1529 // If we're showing user education, don't open the bubble show the education first
1530 mBubbleToExpandAfterFlyoutCollapse = null;
1531 } else {
1532 mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
1533 }
Joshua Tsuji14e68552019-06-06 17:17:08 -04001534
1535 mFlyout.removeCallbacks(mHideFlyout);
1536 mHideFlyout.run();
1537 }
1538
1539 /**
Joshua Tsuji6549e702019-05-02 13:13:16 -04001540 * Called when the flyout drag has finished, and returns true if the gesture successfully
1541 * dismissed the flyout.
1542 */
1543 void onFlyoutDragFinished(float deltaX, float velX) {
1544 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1545 final boolean metRequiredVelocity =
1546 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
1547 final boolean metRequiredDeltaX =
1548 onLeft
1549 ? deltaX < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
1550 : deltaX > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
1551 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
1552 final boolean shouldDismiss = metRequiredVelocity || (metRequiredDeltaX && !isCancelFling);
1553
1554 mFlyout.removeCallbacks(mHideFlyout);
1555 animateFlyoutCollapsed(shouldDismiss, velX);
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001556
1557 maybeShowStackUserEducation();
Joshua Tsuji6549e702019-05-02 13:13:16 -04001558 }
1559
1560 /**
1561 * Called when the first touch event of a gesture (stack drag, bubble drag, flyout drag, etc.)
1562 * is received.
1563 */
1564 void onGestureStart() {
1565 mIsGestureInProgress = true;
1566 }
1567
1568 /** Called when a gesture is completed or cancelled. */
1569 void onGestureFinished() {
1570 mIsGestureInProgress = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -04001571
1572 if (mIsExpanded) {
1573 mExpandedAnimationController.onGestureFinished();
1574 }
Joshua Tsuji6549e702019-05-02 13:13:16 -04001575 }
1576
Joshua Tsuji20103542020-02-18 14:06:28 -05001577 /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
1578 boolean passEventToMagnetizedObject(MotionEvent event) {
1579 return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
1580 }
1581
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001582 /** Prepares and starts the desaturate/darken animation on the bubble stack. */
1583 private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
1584 mDesaturateAndDarkenTargetView = targetView;
1585
1586 if (desaturateAndDarken) {
1587 // Use the animated paint for the bubbles.
1588 mDesaturateAndDarkenTargetView.setLayerType(
1589 View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint);
1590 mDesaturateAndDarkenAnimator.removeAllListeners();
1591 mDesaturateAndDarkenAnimator.start();
1592 } else {
1593 mDesaturateAndDarkenAnimator.removeAllListeners();
1594 mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() {
1595 @Override
1596 public void onAnimationEnd(Animator animation) {
1597 super.onAnimationEnd(animation);
1598 // Stop using the animated paint.
1599 resetDesaturationAndDarken();
1600 }
1601 });
1602 mDesaturateAndDarkenAnimator.reverse();
1603 }
1604 }
1605
1606 private void resetDesaturationAndDarken() {
1607 mDesaturateAndDarkenAnimator.removeAllListeners();
1608 mDesaturateAndDarkenAnimator.cancel();
1609 mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
1610 }
1611
Lyn Han634483c2019-06-28 16:52:47 -07001612 /** Animates in the dismiss target. */
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001613 private void springInDismissTarget() {
1614 if (mShowingDismiss) {
1615 return;
1616 }
1617
1618 mShowingDismiss = true;
1619
Joshua Tsuji20103542020-02-18 14:06:28 -05001620 mDismissTargetContainer.bringToFront();
1621 mDismissTargetContainer.setZ(Short.MAX_VALUE - 1);
1622 mDismissTargetContainer.setVisibility(VISIBLE);
1623
1624 mDismissTargetAnimator.cancel();
1625 mDismissTargetAnimator
1626 .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring)
1627 .start();
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001628 }
1629
1630 /**
1631 * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they
1632 * were dragged into the target and encircled.
1633 */
Lyn Han634483c2019-06-28 16:52:47 -07001634 private void hideDismissTarget() {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001635 if (!mShowingDismiss) {
1636 return;
1637 }
1638
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001639 mShowingDismiss = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001640
Joshua Tsuji20103542020-02-18 14:06:28 -05001641 mDismissTargetAnimator
1642 .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(),
1643 mDismissTargetSpring)
1644 .withEndActions(() -> mDismissTargetContainer.setVisibility(View.INVISIBLE))
1645 .start();
Joshua Tsuji19e22e4242019-04-17 13:29:10 -04001646 }
1647
Joshua Tsuji6549e702019-05-02 13:13:16 -04001648 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
1649 private void animateFlyoutCollapsed(boolean collapsed, float velX) {
1650 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001651 // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's
1652 // faster.
1653 mFlyoutTransitionSpring.getSpring().setStiffness(
1654 (mBubbleToExpandAfterFlyoutCollapse != null)
1655 ? SpringForce.STIFFNESS_MEDIUM
1656 : SpringForce.STIFFNESS_LOW);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001657 mFlyoutTransitionSpring
1658 .setStartValue(mFlyoutDragDeltaX)
1659 .setStartVelocity(velX)
1660 .animateToFinalPosition(collapsed
1661 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth())
1662 : 0f);
1663 }
1664
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001665 /**
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001666 * Calculates the y position of the expanded view when it is expanded.
1667 */
Lyn Han285ad302019-05-29 19:01:39 -07001668 float getExpandedViewY() {
Lyn Han4a8efe32019-05-30 09:43:27 -07001669 return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop + mPointerHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001670 }
1671
1672 /**
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001673 * Animates in the flyout for the given bubble, if available, and then hides it after some time.
1674 */
1675 @VisibleForTesting
1676 void animateInFlyoutForBubble(Bubble bubble) {
Mady Mellordf898fd2020-01-09 09:26:36 -08001677 Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001678 final BadgedImageView bubbleView = bubble.getIconView();
Mady Mellordf898fd2020-01-09 09:26:36 -08001679 if (flyoutMessage == null
1680 || flyoutMessage.message == null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001681 || !bubble.showFlyout()
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001682 || (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE)
Mark Renoufc19b4732019-06-26 12:08:33 -04001683 || isExpanded()
1684 || mIsExpansionAnimating
Joshua Tsuji14e68552019-06-06 17:17:08 -04001685 || mIsGestureInProgress
Lyn Hanf1f2c332019-08-23 17:06:56 -07001686 || mBubbleToExpandAfterFlyoutCollapse != null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001687 || bubbleView == null) {
1688 if (bubbleView != null) {
1689 bubbleView.setDotState(DOT_STATE_DEFAULT);
1690 }
Joshua Tsuji14e68552019-06-06 17:17:08 -04001691 // Skip the message if none exists, we're expanded or animating expansion, or we're
Lyn Hanf1f2c332019-08-23 17:06:56 -07001692 // about to expand a bubble from the previous tapped flyout, or if bubble view is null.
Mark Renoufc19b4732019-06-26 12:08:33 -04001693 return;
1694 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001695
Lyn Hanf1f2c332019-08-23 17:06:56 -07001696 mFlyoutDragDeltaX = 0f;
1697 clearFlyoutOnHide();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001698 mAfterFlyoutHidden = () -> {
1699 // Null it out to ensure it runs once.
1700 mAfterFlyoutHidden = null;
1701
1702 if (mBubbleToExpandAfterFlyoutCollapse != null) {
1703 // User tapped on the flyout and we should expand
1704 mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
1705 mBubbleData.setExpanded(true);
1706 mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -04001707 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001708 bubbleView.setDotState(DOT_STATE_DEFAULT);
Lyn Hanf1f2c332019-08-23 17:06:56 -07001709 };
1710 mFlyout.setVisibility(INVISIBLE);
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001711
Mady Mellorb8aaf972019-11-26 10:28:00 -08001712 // Don't show the dot when we're animating the flyout
1713 bubbleView.setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001714
Lyn Hanf1f2c332019-08-23 17:06:56 -07001715 // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
1716 post(() -> {
1717 // An auto-expanding bubble could have been posted during the time it takes to
1718 // layout.
1719 if (isExpanded()) {
1720 return;
1721 }
1722 final Runnable expandFlyoutAfterDelay = () -> {
1723 mAnimateInFlyout = () -> {
1724 mFlyout.setVisibility(VISIBLE);
1725 mFlyoutDragDeltaX =
1726 mStackAnimationController.isStackOnLeftSide()
1727 ? -mFlyout.getWidth()
1728 : mFlyout.getWidth();
1729 animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
1730 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001731 };
Lyn Hanf1f2c332019-08-23 17:06:56 -07001732 mFlyout.postDelayed(mAnimateInFlyout, 200);
1733 };
Mady Mellordf898fd2020-01-09 09:26:36 -08001734 mFlyout.setupFlyoutStartingAsDot(flyoutMessage,
1735 mStackAnimationController.getStackPosition(), getWidth(),
Lyn Hanf1f2c332019-08-23 17:06:56 -07001736 mStackAnimationController.isStackOnLeftSide(),
Mady Mellor05e860b2019-10-30 22:48:15 -07001737 bubble.getIconView().getDotColor() /* dotColor */,
Lyn Hanf1f2c332019-08-23 17:06:56 -07001738 expandFlyoutAfterDelay /* onLayoutComplete */,
Mady Mellorb8aaf972019-11-26 10:28:00 -08001739 mAfterFlyoutHidden,
1740 bubble.getIconView().getDotCenter(),
1741 !bubble.showDot());
Lyn Hanf1f2c332019-08-23 17:06:56 -07001742 mFlyout.bringToFront();
1743 });
Mark Renoufc19b4732019-06-26 12:08:33 -04001744 mFlyout.removeCallbacks(mHideFlyout);
1745 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001746 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001747 }
1748
1749 /** Hide the flyout immediately and cancel any pending hide runnables. */
1750 private void hideFlyoutImmediate() {
Lyn Hanf1f2c332019-08-23 17:06:56 -07001751 clearFlyoutOnHide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001752 mFlyout.removeCallbacks(mAnimateInFlyout);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001753 mFlyout.removeCallbacks(mHideFlyout);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001754 mFlyout.hideFlyout();
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001755 }
1756
Lyn Hanf1f2c332019-08-23 17:06:56 -07001757 private void clearFlyoutOnHide() {
1758 mFlyout.removeCallbacks(mAnimateInFlyout);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001759 if (mAfterFlyoutHidden == null) {
Lyn Hanf1f2c332019-08-23 17:06:56 -07001760 return;
1761 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001762 mAfterFlyoutHidden.run();
1763 mAfterFlyoutHidden = null;
Lyn Hanf1f2c332019-08-23 17:06:56 -07001764 }
1765
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001766 @Override
1767 public void getBoundsOnScreen(Rect outRect) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001768 if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
1769 // When user education shows then capture all touches
1770 outRect.set(0, 0, getWidth(), getHeight());
1771 return;
1772 }
1773
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001774 if (!mIsExpanded) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001775 if (getBubbleCount() > 0) {
Mady Mellor217b2e92019-02-27 11:44:16 -08001776 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
1777 }
Mady Mellore9371bc2019-07-10 18:50:59 -07001778 // Increase the touch target size of the bubble
1779 outRect.top -= mBubbleTouchPadding;
1780 outRect.left -= mBubbleTouchPadding;
1781 outRect.right += mBubbleTouchPadding;
1782 outRect.bottom += mBubbleTouchPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001783 } else {
1784 mBubbleContainer.getBoundsOnScreen(outRect);
1785 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001786
Joshua Tsuji6549e702019-05-02 13:13:16 -04001787 if (mFlyout.getVisibility() == View.VISIBLE) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001788 final Rect flyoutBounds = new Rect();
1789 mFlyout.getBoundsOnScreen(flyoutBounds);
1790 outRect.union(flyoutBounds);
1791 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001792 }
1793
1794 private int getStatusBarHeight() {
1795 if (getRootWindowInsets() != null) {
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001796 WindowInsets insets = getRootWindowInsets();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001797 return Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -05001798 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001799 insets.getDisplayCutout() != null
1800 ? insets.getDisplayCutout().getSafeInsetTop()
1801 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001802 }
1803
1804 return 0;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001805 }
1806
1807 private boolean isIntersecting(View view, float x, float y) {
1808 mTempLoc = view.getLocationOnScreen();
1809 mTempRect.set(mTempLoc[0], mTempLoc[1], mTempLoc[0] + view.getWidth(),
1810 mTempLoc[1] + view.getHeight());
1811 return mTempRect.contains(x, y);
1812 }
1813
1814 private void requestUpdate() {
Mady Mellorbc078c22019-03-26 17:10:34 -07001815 if (mViewUpdatedRequested || mIsExpansionAnimating) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001816 return;
1817 }
1818 mViewUpdatedRequested = true;
1819 getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
1820 invalidate();
1821 }
1822
1823 private void updateExpandedBubble() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001824 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001825 Log.d(TAG, "updateExpandedBubble()");
1826 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001827 mExpandedViewContainer.removeAllViews();
Lyn Han3cd75d72020-02-15 19:10:12 -08001828 if (mIsExpanded && mExpandedBubble != null) {
1829 BubbleExpandedView bev = mExpandedBubble.getExpandedView();
Lyn Hana0bb02e2020-01-28 17:57:27 -08001830 mExpandedViewContainer.addView(bev);
1831 bev.populateExpandedView();
1832 mExpandedViewContainer.setVisibility(VISIBLE);
Issei Suzukic0387542019-03-08 17:31:14 +01001833 mExpandedViewContainer.setAlpha(1.0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001834 }
1835 }
1836
Lyn Han285ad302019-05-29 19:01:39 -07001837 private void updateExpandedView() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001838 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Han285ad302019-05-29 19:01:39 -07001839 Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001840 }
Joshua Tsuji6549e702019-05-02 13:13:16 -04001841
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001842 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -08001843 if (mIsExpanded) {
Lyn Han285ad302019-05-29 19:01:39 -07001844 final float y = getExpandedViewY();
Mady Mellor5d8f1402019-02-21 18:23:52 -08001845 if (!mExpandedViewYAnim.isRunning()) {
1846 // We're not animating so set the value
1847 mExpandedViewContainer.setTranslationY(y);
Lyn Han3cd75d72020-02-15 19:10:12 -08001848 if (mExpandedBubble != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -08001849 mExpandedBubble.getExpandedView().updateView();
1850 }
Mady Mellor5d8f1402019-02-21 18:23:52 -08001851 } else {
Mady Mellorbc078c22019-03-26 17:10:34 -07001852 // We are animating so update the value; there is an end listener on the animator
1853 // that will ensure expandedeView.updateView gets called.
Mady Mellor5d8f1402019-02-21 18:23:52 -08001854 mExpandedViewYAnim.animateToFinalPosition(y);
1855 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001856 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001857
Joshua Tsuji6549e702019-05-02 13:13:16 -04001858 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001859 updateBubbleZOrdersAndDotPosition(false);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001860 }
1861
1862 /** Sets the appropriate Z-order and dot position for each bubble in the stack. */
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001863 private void updateBubbleZOrdersAndDotPosition(boolean animate) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001864 int bubbleCount = getBubbleCount();
Lyn Han1b4f25e2019-06-11 13:56:34 -07001865 for (int i = 0; i < bubbleCount; i++) {
Mady Mellorb8aaf972019-11-26 10:28:00 -08001866 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
Mady Mellor70958542019-09-24 17:12:46 -07001867 bv.setZ((mMaxBubbles * mBubbleElevation) - i);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001868 // If the dot is on the left, and so is the stack, we need to change the dot position.
1869 if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
1870 bv.setDotPosition(!mStackOnLeftOrWillBe, animate);
1871 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001872 }
1873 }
1874
Mady Mellorde2d4d22019-01-29 14:15:34 -08001875 private void updatePointerPosition() {
Lyn Han9f66c3b2020-03-05 23:59:29 -08001876 if (mExpandedBubble == null) {
Lyn Han522e9ff2019-05-17 13:26:13 -07001877 return;
Mady Mellorde2d4d22019-01-29 14:15:34 -08001878 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08001879 int index = getBubbleIndex(mExpandedBubble);
Lyn Han522e9ff2019-05-17 13:26:13 -07001880 float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index);
1881 float halfBubble = mBubbleSize / 2f;
Mady Mellor9be3bed2019-08-21 17:26:26 -07001882 float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble;
1883 // Padding might be adjusted for insets, so get it directly from the view
1884 bubbleCenter -= mExpandedViewContainer.getPaddingLeft();
Lyn Han9f66c3b2020-03-05 23:59:29 -08001885 mExpandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
Mady Mellorde2d4d22019-01-29 14:15:34 -08001886 }
1887
Steven Wua254dab2019-01-29 11:30:39 -05001888 /**
1889 * @return the number of bubbles in the stack view.
1890 */
Steven Wub00225b2019-02-08 14:27:42 -05001891 public int getBubbleCount() {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001892 if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
1893 // Subtract 1 for the overflow button that is always in the bubble container.
1894 return mBubbleContainer.getChildCount() - 1;
1895 }
1896 return mBubbleContainer.getChildCount();
Steven Wua254dab2019-01-29 11:30:39 -05001897 }
1898
1899 /**
1900 * Finds the bubble index within the stack.
1901 *
Lyn Han3cd75d72020-02-15 19:10:12 -08001902 * @param provider the bubble view provider with the bubble to look up.
Steven Wua254dab2019-01-29 11:30:39 -05001903 * @return the index of the bubble view within the bubble stack. The range of the position
1904 * is between 0 and the bubble count minus 1.
1905 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001906 int getBubbleIndex(@Nullable BubbleViewProvider provider) {
Lyn Han9f66c3b2020-03-05 23:59:29 -08001907 if (provider == null) {
Steven Wua62cb6a2019-02-15 17:12:51 -05001908 return 0;
1909 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08001910 return mBubbleContainer.indexOfChild(provider.getIconView());
Steven Wua254dab2019-01-29 11:30:39 -05001911 }
1912
1913 /**
1914 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
1915 */
Steven Wub00225b2019-02-08 14:27:42 -05001916 public float getNormalizedXPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001917 return new BigDecimal(getStackPosition().x / mDisplaySize.x)
Steven Wua254dab2019-01-29 11:30:39 -05001918 .setScale(4, RoundingMode.CEILING.HALF_UP)
1919 .floatValue();
1920 }
1921
1922 /**
1923 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
1924 */
Steven Wub00225b2019-02-08 14:27:42 -05001925 public float getNormalizedYPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001926 return new BigDecimal(getStackPosition().y / mDisplaySize.y)
Steven Wua254dab2019-01-29 11:30:39 -05001927 .setScale(4, RoundingMode.CEILING.HALF_UP)
1928 .floatValue();
1929 }
1930
Joshua Tsujia19515f2019-02-13 18:02:29 -05001931 public PointF getStackPosition() {
1932 return mStackAnimationController.getStackPosition();
1933 }
1934
Steven Wua254dab2019-01-29 11:30:39 -05001935 /**
1936 * Logs the bubble UI event.
1937 *
1938 * @param bubble the bubble that is being interacted on. Null value indicates that
1939 * the user interaction is not specific to one bubble.
1940 * @param action the user interaction enum.
1941 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001942 private void logBubbleEvent(@Nullable BubbleViewProvider bubble, int action) {
1943 if (bubble == null) {
1944 return;
Steven Wua254dab2019-01-29 11:30:39 -05001945 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001946 bubble.logUIEvent(getBubbleCount(), action, getNormalizedXPosition(),
1947 getNormalizedYPosition(), getBubbleIndex(bubble));
Steven Wua254dab2019-01-29 11:30:39 -05001948 }
Mark Renouf041d7262019-02-06 12:09:41 -05001949
1950 /**
1951 * Called when a back gesture should be directed to the Bubbles stack. When expanded,
1952 * a back key down/up event pair is forwarded to the bubble Activity.
1953 */
1954 boolean performBackPressIfNeeded() {
Lyn Han3cd75d72020-02-15 19:10:12 -08001955 if (!isExpanded() || mExpandedBubble == null) {
Mark Renouf041d7262019-02-06 12:09:41 -05001956 return false;
1957 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001958 return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
Mark Renouf041d7262019-02-06 12:09:41 -05001959 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001960
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001961 /** Whether the educational view should appear for bubbles. **/
1962 private boolean shouldShowBubblesEducation() {
1963 return BubbleDebugConfig.forceShowUserEducation(getContext())
1964 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, false);
1965 }
1966
1967 /** Whether the educational view should appear for the expanded view "manage" button. **/
1968 private boolean shouldShowManageEducation() {
1969 return BubbleDebugConfig.forceShowUserEducation(getContext())
1970 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false);
1971 }
1972
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001973 /** For debugging only */
1974 List<Bubble> getBubblesOnScreen() {
1975 List<Bubble> bubbles = new ArrayList<>();
Lyn Hanc47e1712020-01-28 21:43:34 -08001976 for (int i = 0; i < getBubbleCount(); i++) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001977 View child = mBubbleContainer.getChildAt(i);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001978 if (child instanceof BadgedImageView) {
1979 String key = ((BadgedImageView) child).getKey();
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001980 Bubble bubble = mBubbleData.getBubbleWithKey(key);
1981 bubbles.add(bubble);
1982 }
1983 }
1984 return bubbles;
1985 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001986}