• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.bubbles;
18 
19 import static android.graphics.Paint.ANTI_ALIAS_FLAG;
20 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
21 
22 import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_IN;
23 import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_OUT;
24 
25 import android.animation.ArgbEvaluator;
26 import android.content.Context;
27 import android.content.res.Configuration;
28 import android.content.res.Resources;
29 import android.content.res.TypedArray;
30 import android.graphics.Canvas;
31 import android.graphics.Matrix;
32 import android.graphics.Outline;
33 import android.graphics.Paint;
34 import android.graphics.Path;
35 import android.graphics.PointF;
36 import android.graphics.RectF;
37 import android.graphics.drawable.Drawable;
38 import android.graphics.drawable.ShapeDrawable;
39 import android.text.TextUtils;
40 import android.util.TypedValue;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.view.ViewOutlineProvider;
45 import android.widget.FrameLayout;
46 import android.widget.ImageView;
47 import android.widget.TextView;
48 
49 import androidx.annotation.Nullable;
50 
51 import com.android.wm.shell.R;
52 import com.android.wm.shell.shared.TriangleShape;
53 import com.android.wm.shell.shared.TypefaceUtils;
54 
55 /**
56  * Flyout view that appears as a 'chat bubble' alongside the bubble stack. The flyout can visually
57  * transform into the 'new' dot, which is used during flyout dismiss animations/gestures.
58  */
59 public class BubbleFlyoutView extends FrameLayout {
60     /** Translation Y of fade animation. */
61     private static final float FLYOUT_FADE_Y = 40f;
62 
63     private static final long FLYOUT_FADE_OUT_DURATION = 150L;
64     private static final long FLYOUT_FADE_IN_DURATION = 250L;
65 
66     // Whether the flyout view should show a pointer to the bubble.
67     private static final boolean SHOW_POINTER = false;
68 
69     private BubblePositioner mPositioner;
70 
71     private final int mFlyoutPadding;
72     private final int mFlyoutSpaceFromBubble;
73     private final int mPointerSize;
74     private int mBubbleSize;
75 
76     private final int mFlyoutElevation;
77     private final int mBubbleElevation;
78     private int mFloatingBackgroundColor;
79     private final float mCornerRadius;
80 
81     private final ViewGroup mFlyoutTextContainer;
82     private final ImageView mSenderAvatar;
83     private final TextView mSenderText;
84     private final TextView mMessageText;
85 
86     /** Values related to the 'new' dot which we use to figure out where to collapse the flyout. */
87     private float mNewDotRadius;
88     private float mNewDotSize;
89     private float mOriginalDotSize;
90 
91     /**
92      * The paint used to draw the background, whose color changes as the flyout transitions to the
93      * tinted 'new' dot.
94      */
95     private final Paint mBgPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
96     private final ArgbEvaluator mArgbEvaluator = new ArgbEvaluator();
97 
98     /**
99      * Triangular ShapeDrawables used for the triangle that points from the flyout to the bubble
100      * stack (a chat-bubble effect).
101      */
102     private final ShapeDrawable mLeftTriangleShape;
103     private final ShapeDrawable mRightTriangleShape;
104 
105     /** Whether the flyout arrow is on the left (pointing left) or right (pointing right). */
106     private boolean mArrowPointingLeft = true;
107 
108     /** Color of the 'new' dot that the flyout will transform into. */
109     private int mDotColor;
110 
111     /** Keeps last used night mode flags **/
112     private int mNightModeFlags;
113 
114     /** The outline of the triangle, used for elevation shadows. */
115     private final Outline mTriangleOutline = new Outline();
116 
117     /** The bounds of the flyout background, kept up to date as it transitions to the 'new' dot. */
118     private final RectF mBgRect = new RectF();
119 
120     /** The y position of the flyout, relative to the top of the screen. */
121     private float mFlyoutY = 0f;
122 
123     /**
124      * Percent progress in the transition from flyout to 'new' dot. These two values are the inverse
125      * of each other (if we're 40% transitioned to the dot, we're 60% flyout), but it makes the code
126      * much more readable.
127      */
128     private float mPercentTransitionedToDot = 1f;
129     private float mPercentStillFlyout = 0f;
130 
131     /**
132      * The difference in values between the flyout and the dot. These differences are gradually
133      * added over the course of the animation to transform the flyout into the 'new' dot.
134      */
135     private float mFlyoutToDotWidthDelta = 0f;
136     private float mFlyoutToDotHeightDelta = 0f;
137 
138     /** The translation values when the flyout is completely transitioned into the dot. */
139     private float mTranslationXWhenDot = 0f;
140     private float mTranslationYWhenDot = 0f;
141 
142     /**
143      * The current translation values applied to the flyout background as it transitions into the
144      * 'new' dot.
145      */
146     private float mBgTranslationX;
147     private float mBgTranslationY;
148 
149     private float[] mDotCenter;
150 
151     /** The flyout's X translation when at rest (not animating or dragging). */
152     private float mRestingTranslationX = 0f;
153 
154     /** The badge sizes are defined as percentages of the app icon size. Same value as Launcher3. */
155     private static final float SIZE_PERCENTAGE = 0.228f;
156 
157     private static final float DOT_SCALE = 1f;
158 
159     /** Callback to run when the flyout is hidden. */
160     @Nullable private Runnable mOnHide;
161 
BubbleFlyoutView(Context context, BubblePositioner positioner)162     public BubbleFlyoutView(Context context, BubblePositioner positioner) {
163         super(context);
164         mPositioner = positioner;
165 
166         LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true);
167         mFlyoutTextContainer = findViewById(R.id.bubble_flyout_text_container);
168         mSenderText = findViewById(R.id.bubble_flyout_name);
169         TypefaceUtils.setTypeface(mSenderText, TypefaceUtils.FontFamily.GSF_LABEL_LARGE);
170         mSenderAvatar = findViewById(R.id.bubble_flyout_avatar);
171         mMessageText = mFlyoutTextContainer.findViewById(R.id.bubble_flyout_text);
172         TypefaceUtils.setTypeface(mMessageText, TypefaceUtils.FontFamily.GSF_BODY_MEDIUM);
173 
174         final Resources res = getResources();
175         mFlyoutPadding = res.getDimensionPixelSize(R.dimen.bubble_flyout_padding_x);
176         mFlyoutSpaceFromBubble = res.getDimensionPixelSize(R.dimen.bubble_flyout_space_from_bubble);
177         mPointerSize = SHOW_POINTER
178                 ? res.getDimensionPixelSize(R.dimen.bubble_flyout_pointer_size)
179                 : 0;
180 
181         mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
182         mFlyoutElevation = res.getDimensionPixelSize(R.dimen.bubble_flyout_elevation);
183 
184         final TypedArray ta = mContext.obtainStyledAttributes(
185                 new int[] {android.R.attr.dialogCornerRadius});
186         mCornerRadius = ta.getDimensionPixelSize(0, 0);
187         ta.recycle();
188 
189         // Add padding for the pointer on either side, onDraw will draw it in this space.
190         setPadding(mPointerSize, 0, mPointerSize, 0);
191         setWillNotDraw(false);
192         setClipChildren(!SHOW_POINTER);
193         setTranslationZ(mFlyoutElevation);
194         setOutlineProvider(new ViewOutlineProvider() {
195             @Override
196             public void getOutline(View view, Outline outline) {
197                 BubbleFlyoutView.this.getOutline(outline);
198             }
199         });
200 
201         // Use locale direction so the text is aligned correctly.
202         setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
203 
204         mLeftTriangleShape =
205                 new ShapeDrawable(TriangleShape.createHorizontal(
206                         mPointerSize, mPointerSize, true /* isPointingLeft */));
207         mLeftTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize);
208 
209         mRightTriangleShape =
210                 new ShapeDrawable(TriangleShape.createHorizontal(
211                         mPointerSize, mPointerSize, false /* isPointingLeft */));
212         mRightTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize);
213 
214         applyConfigurationColors();
215     }
216 
217     @Override
onDraw(Canvas canvas)218     protected void onDraw(Canvas canvas) {
219         renderBackground(canvas);
220         invalidateOutline();
221         super.onDraw(canvas);
222     }
223 
updateFontSize()224     void updateFontSize() {
225         final float fontSize = mContext.getResources()
226                 .getDimensionPixelSize(com.android.internal.R.dimen.text_size_body_2_material);
227         mMessageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
228         mSenderText.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
229     }
230 
231     /*
232      * Fade animation for consecutive flyouts.
233      */
animateUpdate(Bubble.FlyoutMessage flyoutMessage, PointF stackPos, boolean hideDot, float[] dotCenter, Runnable onHide)234     void animateUpdate(Bubble.FlyoutMessage flyoutMessage, PointF stackPos,
235             boolean hideDot, float[] dotCenter, Runnable onHide) {
236         mOnHide = onHide;
237         mDotCenter = dotCenter;
238         final Runnable afterFadeOut = () -> {
239             updateFlyoutMessage(flyoutMessage);
240             // Wait for TextViews to layout with updated height.
241             post(() -> {
242                 fade(true /* in */, stackPos, hideDot, () -> {} /* after */);
243             } /* after */ );
244         };
245         fade(false /* in */, stackPos, hideDot, afterFadeOut);
246     }
247 
248     @Override
onConfigurationChanged(Configuration newConfig)249     protected void onConfigurationChanged(Configuration newConfig) {
250         if (applyColorsAccordingToConfiguration(newConfig)) {
251             invalidate();
252         }
253     }
254 
255     /*
256      * Fade-out above or fade-in from below.
257      */
fade(boolean in, PointF stackPos, boolean hideDot, Runnable afterFade)258     private void fade(boolean in, PointF stackPos, boolean hideDot, Runnable afterFade) {
259         mFlyoutY = stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f;
260 
261         setAlpha(in ? 0f : 1f);
262         setTranslationY(in ? mFlyoutY + FLYOUT_FADE_Y : mFlyoutY);
263         updateFlyoutX(stackPos.x);
264         setTranslationX(mRestingTranslationX);
265         updateDot(stackPos, hideDot);
266 
267         animate()
268                 .alpha(in ? 1f : 0f)
269                 .setDuration(in ? FLYOUT_FADE_IN_DURATION : FLYOUT_FADE_OUT_DURATION)
270                 .setInterpolator(in ? ALPHA_IN : ALPHA_OUT);
271         animate()
272                 .translationY(in ? mFlyoutY : mFlyoutY - FLYOUT_FADE_Y)
273                 .setDuration(in ? FLYOUT_FADE_IN_DURATION : FLYOUT_FADE_OUT_DURATION)
274                 .setInterpolator(in ? ALPHA_IN : ALPHA_OUT)
275                 .withEndAction(afterFade);
276     }
277 
updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage)278     private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage) {
279         final Drawable senderAvatar = flyoutMessage.senderAvatar;
280         if (senderAvatar != null && flyoutMessage.isGroupChat) {
281             mSenderAvatar.setVisibility(VISIBLE);
282             mSenderAvatar.setImageDrawable(senderAvatar);
283         } else {
284             mSenderAvatar.setVisibility(GONE);
285             mSenderAvatar.setTranslationX(0);
286             mMessageText.setTranslationX(0);
287             mSenderText.setTranslationX(0);
288         }
289 
290         final int maxTextViewWidth = (int) mPositioner.getMaxFlyoutSize() - mFlyoutPadding * 2;
291 
292         // Name visibility
293         if (!TextUtils.isEmpty(flyoutMessage.senderName)) {
294             mSenderText.setMaxWidth(maxTextViewWidth);
295             mSenderText.setText(flyoutMessage.senderName);
296             mSenderText.setVisibility(VISIBLE);
297         } else {
298             mSenderText.setVisibility(GONE);
299         }
300 
301         // Set the flyout TextView's max width in terms of percent, and then subtract out the
302         // padding so that the entire flyout view will be the desired width (rather than the
303         // TextView being the desired width + extra padding).
304         mMessageText.setMaxWidth(maxTextViewWidth);
305         mMessageText.setText(flyoutMessage.message);
306     }
307 
updateFlyoutX(float stackX)308     void updateFlyoutX(float stackX) {
309         // Calculate the translation required to position the flyout next to the bubble stack,
310         // with the desired padding.
311         mRestingTranslationX = mArrowPointingLeft
312                 ? stackX + mBubbleSize + mFlyoutSpaceFromBubble
313                 : stackX - getWidth() - mFlyoutSpaceFromBubble;
314     }
315 
updateDot(PointF stackPos, boolean hideDot)316     void updateDot(PointF stackPos, boolean hideDot) {
317         // Calculate the difference in size between the flyout and the 'dot' so that we can
318         // transform into the dot later.
319         final float newDotSize = hideDot ? 0f : mNewDotSize;
320         mFlyoutToDotWidthDelta = getWidth() - newDotSize;
321         mFlyoutToDotHeightDelta = getHeight() - newDotSize;
322 
323         // Calculate the translation values needed to be in the correct 'new dot' position.
324         final float adjustmentForScaleAway = hideDot ? 0f : (mOriginalDotSize / 2f);
325         final float dotPositionX = stackPos.x + mDotCenter[0] - adjustmentForScaleAway;
326         final float dotPositionY = stackPos.y + mDotCenter[1] - adjustmentForScaleAway;
327 
328         final float distanceFromFlyoutLeftToDotCenterX = mRestingTranslationX - dotPositionX;
329         final float distanceFromLayoutTopToDotCenterY = mFlyoutY - dotPositionY;
330 
331         mTranslationXWhenDot = -distanceFromFlyoutLeftToDotCenterX;
332         mTranslationYWhenDot = -distanceFromLayoutTopToDotCenterY;
333     }
334 
335     /** Configures the flyout, collapsed into dot form. */
setupFlyoutStartingAsDot( Bubble.FlyoutMessage flyoutMessage, PointF stackPos, boolean arrowPointingLeft, int dotColor, @Nullable Runnable onLayoutComplete, @Nullable Runnable onHide, float[] dotCenter, boolean hideDot)336     void setupFlyoutStartingAsDot(
337             Bubble.FlyoutMessage flyoutMessage,
338             PointF stackPos,
339             boolean arrowPointingLeft,
340             int dotColor,
341             @Nullable Runnable onLayoutComplete,
342             @Nullable Runnable onHide,
343             float[] dotCenter,
344             boolean hideDot)  {
345 
346         mBubbleSize = mPositioner.getBubbleSize();
347 
348         mOriginalDotSize = SIZE_PERCENTAGE * mBubbleSize;
349         mNewDotRadius = (DOT_SCALE * mOriginalDotSize) / 2f;
350         mNewDotSize = mNewDotRadius * 2f;
351 
352         updateFlyoutMessage(flyoutMessage);
353 
354         mArrowPointingLeft = arrowPointingLeft;
355         mDotColor = dotColor;
356         mOnHide = onHide;
357         mDotCenter = dotCenter;
358 
359         setCollapsePercent(1f);
360 
361         // Wait for TextViews to layout with updated height.
362         post(() -> {
363             // Flyout is vertically centered with respect to the bubble.
364             mFlyoutY =
365                     stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f;
366             setTranslationY(mFlyoutY);
367             updateFlyoutX(stackPos.x);
368             updateDot(stackPos, hideDot);
369             if (onLayoutComplete != null) {
370                 onLayoutComplete.run();
371             }
372         });
373     }
374 
375     /**
376      * Hides the flyout and runs the optional callback passed into setupFlyoutStartingAsDot.
377      * The flyout has been animated into the 'new' dot by the time we call this, so no animations
378      * are needed.
379      */
hideFlyout()380     void hideFlyout() {
381         if (mOnHide != null) {
382             mOnHide.run();
383             mOnHide = null;
384         }
385 
386         setVisibility(GONE);
387     }
388 
389     /** Sets the percentage that the flyout should be collapsed into dot form. */
setCollapsePercent(float percentCollapsed)390     void setCollapsePercent(float percentCollapsed) {
391         // This is unlikely, but can happen in a race condition where the flyout view hasn't been
392         // laid out and returns 0 for getWidth(). We check for this condition at the sites where
393         // this method is called, but better safe than sorry.
394         if (Float.isNaN(percentCollapsed)) {
395             return;
396         }
397 
398         mPercentTransitionedToDot = Math.max(0f, Math.min(percentCollapsed, 1f));
399         mPercentStillFlyout = (1f - mPercentTransitionedToDot);
400 
401         // Move and fade out the text.
402         final float translationX = mPercentTransitionedToDot
403                 * (mArrowPointingLeft ? -getWidth() : getWidth());
404         final float alpha = clampPercentage(
405                 (mPercentStillFlyout - (1f - BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS))
406                         / BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS);
407 
408         mMessageText.setTranslationX(translationX);
409         mMessageText.setAlpha(alpha);
410 
411         mSenderText.setTranslationX(translationX);
412         mSenderText.setAlpha(alpha);
413 
414         mSenderAvatar.setTranslationX(translationX);
415         mSenderAvatar.setAlpha(alpha);
416 
417         // Reduce the elevation towards that of the topmost bubble.
418         setTranslationZ(
419                 mFlyoutElevation
420                         - (mFlyoutElevation - mBubbleElevation) * mPercentTransitionedToDot);
421         invalidate();
422     }
423 
424     /** Return the flyout's resting X translation (translation when not dragging or animating). */
getRestingTranslationX()425     float getRestingTranslationX() {
426         return mRestingTranslationX;
427     }
428 
429     /** Clamps a float to between 0 and 1. */
clampPercentage(float percent)430     private float clampPercentage(float percent) {
431         return Math.min(1f, Math.max(0f, percent));
432     }
433 
434     /**
435      * Resolving and applying colors according to the ui mode, remembering most recent mode.
436      *
437      * @return {@code true} if night mode setting has changed since the last invocation,
438      * {@code false} otherwise
439      */
applyColorsAccordingToConfiguration(Configuration configuration)440     boolean applyColorsAccordingToConfiguration(Configuration configuration) {
441         int nightModeFlags = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
442         boolean flagsChanged = nightModeFlags != mNightModeFlags;
443         if (flagsChanged) {
444             mNightModeFlags = nightModeFlags;
445             applyConfigurationColors();
446         }
447         return flagsChanged;
448     }
449 
applyConfigurationColors()450     private void applyConfigurationColors() {
451         mFloatingBackgroundColor = mContext.getColor(
452                 com.android.internal.R.color.materialColorSurfaceContainer);
453         mSenderText.setTextColor(
454                 mContext.getColor(com.android.internal.R.color.materialColorOnSurface));
455         mMessageText.setTextColor(
456                 mContext.getColor(com.android.internal.R.color.materialColorOnSurfaceVariant));
457 
458         mBgPaint.setColor(mFloatingBackgroundColor);
459         mLeftTriangleShape.getPaint().setColor(mFloatingBackgroundColor);
460         mRightTriangleShape.getPaint().setColor(mFloatingBackgroundColor);
461 
462     }
463 
464     /**
465      * Renders the background, which is either the rounded 'chat bubble' flyout, or some state
466      * between that and the 'new' dot over the bubbles.
467      */
renderBackground(Canvas canvas)468     private void renderBackground(Canvas canvas) {
469         // Calculate the width, height, and corner radius of the flyout given the current collapsed
470         // percentage.
471         final float width = getWidth() - (mFlyoutToDotWidthDelta * mPercentTransitionedToDot);
472         final float height = getHeight() - (mFlyoutToDotHeightDelta * mPercentTransitionedToDot);
473         final float interpolatedRadius = getInterpolatedRadius();
474 
475         // Translate the flyout background towards the collapsed 'dot' state.
476         mBgTranslationX = mTranslationXWhenDot * mPercentTransitionedToDot;
477         mBgTranslationY = mTranslationYWhenDot * mPercentTransitionedToDot;
478 
479         // Set the bounds of the rounded rectangle that serves as either the flyout background or
480         // the collapsed 'dot'. These bounds will also be used to provide the outline for elevation
481         // shadows. In the expanded flyout state, the left and right bounds leave space for the
482         // pointer triangle - as the flyout collapses, this space is reduced since the triangle
483         // retracts into the flyout.
484         mBgRect.set(
485                 mPointerSize * mPercentStillFlyout /* left */,
486                 0 /* top */,
487                 width - mPointerSize * mPercentStillFlyout /* right */,
488                 height /* bottom */);
489 
490         mBgPaint.setColor(
491                 (int) mArgbEvaluator.evaluate(
492                         mPercentTransitionedToDot, mFloatingBackgroundColor, mDotColor));
493 
494         canvas.save();
495         canvas.translate(mBgTranslationX, mBgTranslationY);
496         renderPointerTriangle(canvas, width, height);
497         canvas.drawRoundRect(mBgRect, interpolatedRadius, interpolatedRadius, mBgPaint);
498         canvas.restore();
499     }
500 
501     /** Renders the 'pointer' triangle that points from the flyout to the bubble stack. */
renderPointerTriangle( Canvas canvas, float currentFlyoutWidth, float currentFlyoutHeight)502     private void renderPointerTriangle(
503             Canvas canvas, float currentFlyoutWidth, float currentFlyoutHeight) {
504         if (!SHOW_POINTER) return;
505         canvas.save();
506 
507         // Translation to apply for the 'retraction' effect as the flyout collapses.
508         final float retractionTranslationX =
509                 (mArrowPointingLeft ? 1 : -1) * (mPercentTransitionedToDot * mPointerSize * 2f);
510 
511         // Place the arrow either at the left side, or the far right, depending on whether the
512         // flyout is on the left or right side.
513         final float arrowTranslationX =
514                 mArrowPointingLeft
515                         ? retractionTranslationX
516                         : currentFlyoutWidth - mPointerSize + retractionTranslationX;
517 
518         // Vertically center the arrow at all times.
519         final float arrowTranslationY = currentFlyoutHeight / 2f - mPointerSize / 2f;
520 
521         // Draw the appropriate direction of arrow.
522         final ShapeDrawable relevantTriangle =
523                 mArrowPointingLeft ? mLeftTriangleShape : mRightTriangleShape;
524         canvas.translate(arrowTranslationX, arrowTranslationY);
525         relevantTriangle.setAlpha((int) (255f * mPercentStillFlyout));
526         relevantTriangle.draw(canvas);
527 
528         // Save the triangle's outline for use in the outline provider, offsetting it to reflect its
529         // current position.
530         relevantTriangle.getOutline(mTriangleOutline);
531         mTriangleOutline.offset((int) arrowTranslationX, (int) arrowTranslationY);
532         canvas.restore();
533     }
534 
535     /** Builds an outline that includes the transformed flyout background and triangle. */
getOutline(Outline outline)536     private void getOutline(Outline outline) {
537         if (!mTriangleOutline.isEmpty() || !SHOW_POINTER) {
538             // Draw the rect into the outline as a path so we can merge the triangle path into it.
539             final Path rectPath = new Path();
540             final float interpolatedRadius = getInterpolatedRadius();
541             rectPath.addRoundRect(mBgRect, interpolatedRadius,
542                     interpolatedRadius, Path.Direction.CW);
543             outline.setPath(rectPath);
544 
545             // Get rid of the triangle path once it has disappeared behind the flyout.
546             if (SHOW_POINTER && mPercentStillFlyout > 0.5f) {
547                 outline.mPath.addPath(mTriangleOutline.mPath);
548             }
549 
550             // Translate the outline to match the background's position.
551             final Matrix outlineMatrix = new Matrix();
552             outlineMatrix.postTranslate(getLeft() + mBgTranslationX, getTop() + mBgTranslationY);
553 
554             // At the very end, retract the outline into the bubble so the shadow will be pulled
555             // into the flyout-dot as it (visually) becomes part of the bubble. We can't do this by
556             // animating translationZ to zero since then it'll go under the bubbles, which have
557             // elevation.
558             if (mPercentTransitionedToDot > 0.98f) {
559                 final float percentBetween99and100 = (mPercentTransitionedToDot - 0.98f) / .02f;
560                 final float percentShadowVisible = 1f - percentBetween99and100;
561 
562                 // Keep it centered.
563                 outlineMatrix.postTranslate(
564                         mNewDotRadius * percentBetween99and100,
565                         mNewDotRadius * percentBetween99and100);
566                 outlineMatrix.preScale(percentShadowVisible, percentShadowVisible);
567             }
568 
569             outline.mPath.transform(outlineMatrix);
570         }
571     }
572 
getInterpolatedRadius()573     private float getInterpolatedRadius() {
574         return mNewDotRadius * mPercentTransitionedToDot
575                 + mCornerRadius * (1 - mPercentTransitionedToDot);
576     }
577 }
578