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