1 /*
2  * Copyright (C) 2016 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.incallui.answer.impl.affordance;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ArgbEvaluator;
22 import android.animation.PropertyValuesHolder;
23 import android.animation.ValueAnimator;
24 import android.content.Context;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Paint;
28 import android.graphics.PorterDuff;
29 import android.graphics.drawable.Drawable;
30 import android.support.annotation.Nullable;
31 import android.util.AttributeSet;
32 import android.view.View;
33 import android.view.ViewAnimationUtils;
34 import android.view.animation.Interpolator;
35 import android.widget.ImageView;
36 import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
37 import com.android.incallui.answer.impl.utils.Interpolators;
38 
39 /** Button that allows swiping to trigger */
40 public class SwipeButtonView extends ImageView {
41 
42   private static final long CIRCLE_APPEAR_DURATION = 80;
43   private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200;
44   private static final long NORMAL_ANIMATION_DURATION = 200;
45   public static final float MAX_ICON_SCALE_AMOUNT = 1.5f;
46   public static final float MIN_ICON_SCALE_AMOUNT = 0.8f;
47 
48   private final int minBackgroundRadius;
49   private final Paint circlePaint;
50   private final int inverseColor;
51   private final int normalColor;
52   private final ArgbEvaluator colorInterpolator;
53   private final FlingAnimationUtils flingAnimationUtils;
54   private float circleRadius;
55   private int centerX;
56   private int centerY;
57   private ValueAnimator circleAnimator;
58   private ValueAnimator alphaAnimator;
59   private ValueAnimator scaleAnimator;
60   private float circleStartValue;
61   private boolean circleWillBeHidden;
62   private int[] tempPoint = new int[2];
63   private float tmageScale = 1f;
64   private int circleColor;
65   private View previewView;
66   private float circleStartRadius;
67   private float maxCircleSize;
68   private Animator previewClipper;
69   private float restingAlpha = SwipeButtonHelper.SWIPE_RESTING_ALPHA_AMOUNT;
70   private boolean finishing;
71   private boolean launchingAffordance;
72 
73   private AnimatorListenerAdapter clipEndListener =
74       new AnimatorListenerAdapter() {
75         @Override
76         public void onAnimationEnd(Animator animation) {
77           previewClipper = null;
78         }
79       };
80   private AnimatorListenerAdapter circleEndListener =
81       new AnimatorListenerAdapter() {
82         @Override
83         public void onAnimationEnd(Animator animation) {
84           circleAnimator = null;
85         }
86       };
87   private AnimatorListenerAdapter scaleEndListener =
88       new AnimatorListenerAdapter() {
89         @Override
90         public void onAnimationEnd(Animator animation) {
91           scaleAnimator = null;
92         }
93       };
94   private AnimatorListenerAdapter alphaEndListener =
95       new AnimatorListenerAdapter() {
96         @Override
97         public void onAnimationEnd(Animator animation) {
98           alphaAnimator = null;
99         }
100       };
101 
SwipeButtonView(Context context)102   public SwipeButtonView(Context context) {
103     this(context, null);
104   }
105 
SwipeButtonView(Context context, AttributeSet attrs)106   public SwipeButtonView(Context context, AttributeSet attrs) {
107     this(context, attrs, 0);
108   }
109 
SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr)110   public SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr) {
111     this(context, attrs, defStyleAttr, 0);
112   }
113 
SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)114   public SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
115     super(context, attrs, defStyleAttr, defStyleRes);
116     circlePaint = new Paint();
117     circlePaint.setAntiAlias(true);
118     circleColor = 0xffffffff;
119     circlePaint.setColor(circleColor);
120 
121     normalColor = 0xffffffff;
122     inverseColor = 0xff000000;
123     minBackgroundRadius =
124         context
125             .getResources()
126             .getDimensionPixelSize(R.dimen.answer_affordance_min_background_radius);
127     colorInterpolator = new ArgbEvaluator();
128     flingAnimationUtils = new FlingAnimationUtils(context, 0.3f);
129   }
130 
131   @Override
onLayout(boolean changed, int left, int top, int right, int bottom)132   protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
133     super.onLayout(changed, left, top, right, bottom);
134     centerX = getWidth() / 2;
135     centerY = getHeight() / 2;
136     maxCircleSize = getMaxCircleSize();
137   }
138 
139   @Override
onDraw(Canvas canvas)140   protected void onDraw(Canvas canvas) {
141     drawBackgroundCircle(canvas);
142     canvas.save();
143     canvas.scale(tmageScale, tmageScale, getWidth() / 2, getHeight() / 2);
144     super.onDraw(canvas);
145     canvas.restore();
146   }
147 
setPreviewView(@ullable View v)148   public void setPreviewView(@Nullable View v) {
149     View oldPreviewView = previewView;
150     previewView = v;
151     if (previewView != null) {
152       previewView.setVisibility(launchingAffordance ? oldPreviewView.getVisibility() : INVISIBLE);
153     }
154   }
155 
updateIconColor()156   private void updateIconColor() {
157     Drawable drawable = getDrawable().mutate();
158     float alpha = circleRadius / minBackgroundRadius;
159     alpha = Math.min(1.0f, alpha);
160     int color = (int) colorInterpolator.evaluate(alpha, normalColor, inverseColor);
161     drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
162   }
163 
drawBackgroundCircle(Canvas canvas)164   private void drawBackgroundCircle(Canvas canvas) {
165     if (circleRadius > 0 || finishing) {
166       updateCircleColor();
167       canvas.drawCircle(centerX, centerY, circleRadius, circlePaint);
168     }
169   }
170 
updateCircleColor()171   private void updateCircleColor() {
172     float fraction =
173         0.5f
174             + 0.5f
175                 * Math.max(
176                     0.0f,
177                     Math.min(
178                         1.0f, (circleRadius - minBackgroundRadius) / (0.5f * minBackgroundRadius)));
179     if (previewView != null && previewView.getVisibility() == VISIBLE) {
180       float finishingFraction =
181           1 - Math.max(0, circleRadius - circleStartRadius) / (maxCircleSize - circleStartRadius);
182       fraction *= finishingFraction;
183     }
184     int color =
185         Color.argb(
186             (int) (Color.alpha(circleColor) * fraction),
187             Color.red(circleColor),
188             Color.green(circleColor),
189             Color.blue(circleColor));
190     circlePaint.setColor(color);
191   }
192 
finishAnimation(float velocity, @Nullable final Runnable mAnimationEndRunnable)193   public void finishAnimation(float velocity, @Nullable final Runnable mAnimationEndRunnable) {
194     cancelAnimator(circleAnimator);
195     cancelAnimator(previewClipper);
196     finishing = true;
197     circleStartRadius = circleRadius;
198     final float maxCircleSize = getMaxCircleSize();
199     Animator animatorToRadius;
200     animatorToRadius = getAnimatorToRadius(maxCircleSize);
201     flingAnimationUtils.applyDismissing(
202         animatorToRadius, circleRadius, maxCircleSize, velocity, maxCircleSize);
203     animatorToRadius.addListener(
204         new AnimatorListenerAdapter() {
205           @Override
206           public void onAnimationEnd(Animator animation) {
207             if (mAnimationEndRunnable != null) {
208               mAnimationEndRunnable.run();
209             }
210             finishing = false;
211             circleRadius = maxCircleSize;
212             invalidate();
213           }
214         });
215     animatorToRadius.start();
216     setImageAlpha(0, true);
217     if (previewView != null) {
218       previewView.setVisibility(View.VISIBLE);
219       previewClipper =
220           ViewAnimationUtils.createCircularReveal(
221               previewView, getLeft() + centerX, getTop() + centerY, circleRadius, maxCircleSize);
222       flingAnimationUtils.applyDismissing(
223           previewClipper, circleRadius, maxCircleSize, velocity, maxCircleSize);
224       previewClipper.addListener(clipEndListener);
225       previewClipper.start();
226     }
227   }
228 
instantFinishAnimation()229   public void instantFinishAnimation() {
230     cancelAnimator(previewClipper);
231     if (previewView != null) {
232       previewView.setClipBounds(null);
233       previewView.setVisibility(View.VISIBLE);
234     }
235     circleRadius = getMaxCircleSize();
236     setImageAlpha(0, false);
237     invalidate();
238   }
239 
getMaxCircleSize()240   private float getMaxCircleSize() {
241     getLocationInWindow(tempPoint);
242     float rootWidth = getRootView().getWidth();
243     float width = tempPoint[0] + centerX;
244     width = Math.max(rootWidth - width, width);
245     float height = tempPoint[1] + centerY;
246     return (float) Math.hypot(width, height);
247   }
248 
setCircleRadius(float circleRadius)249   public void setCircleRadius(float circleRadius) {
250     setCircleRadius(circleRadius, false, false);
251   }
252 
setCircleRadius(float circleRadius, boolean slowAnimation)253   public void setCircleRadius(float circleRadius, boolean slowAnimation) {
254     setCircleRadius(circleRadius, slowAnimation, false);
255   }
256 
setCircleRadiusWithoutAnimation(float circleRadius)257   public void setCircleRadiusWithoutAnimation(float circleRadius) {
258     cancelAnimator(circleAnimator);
259     setCircleRadius(circleRadius, false, true);
260   }
261 
setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation)262   private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
263 
264     // Check if we need a new animation
265     boolean radiusHidden =
266         (circleAnimator != null && circleWillBeHidden)
267             || (circleAnimator == null && this.circleRadius == 0.0f);
268     boolean nowHidden = circleRadius == 0.0f;
269     boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
270     if (!radiusNeedsAnimation) {
271       if (circleAnimator == null) {
272         this.circleRadius = circleRadius;
273         updateIconColor();
274         invalidate();
275         if (nowHidden) {
276           if (previewView != null) {
277             previewView.setVisibility(View.INVISIBLE);
278           }
279         }
280       } else if (!circleWillBeHidden) {
281 
282         // We just update the end value
283         float diff = circleRadius - minBackgroundRadius;
284         PropertyValuesHolder[] values = circleAnimator.getValues();
285         values[0].setFloatValues(circleStartValue + diff, circleRadius);
286         circleAnimator.setCurrentPlayTime(circleAnimator.getCurrentPlayTime());
287       }
288     } else {
289       cancelAnimator(circleAnimator);
290       cancelAnimator(previewClipper);
291       ValueAnimator animator = getAnimatorToRadius(circleRadius);
292       Interpolator interpolator =
293           circleRadius == 0.0f
294               ? Interpolators.FAST_OUT_LINEAR_IN
295               : Interpolators.LINEAR_OUT_SLOW_IN;
296       animator.setInterpolator(interpolator);
297       long duration = 250;
298       if (!slowAnimation) {
299         float durationFactor =
300             Math.abs(this.circleRadius - circleRadius) / (float) minBackgroundRadius;
301         duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
302         duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
303       }
304       animator.setDuration(duration);
305       animator.start();
306       if (previewView != null && previewView.getVisibility() == View.VISIBLE) {
307         previewView.setVisibility(View.VISIBLE);
308         previewClipper =
309             ViewAnimationUtils.createCircularReveal(
310                 previewView,
311                 getLeft() + centerX,
312                 getTop() + centerY,
313                 this.circleRadius,
314                 circleRadius);
315         previewClipper.setInterpolator(interpolator);
316         previewClipper.setDuration(duration);
317         previewClipper.addListener(clipEndListener);
318         previewClipper.addListener(
319             new AnimatorListenerAdapter() {
320               @Override
321               public void onAnimationEnd(Animator animation) {
322                 previewView.setVisibility(View.INVISIBLE);
323               }
324             });
325         previewClipper.start();
326       }
327     }
328   }
329 
getAnimatorToRadius(float circleRadius)330   private ValueAnimator getAnimatorToRadius(float circleRadius) {
331     ValueAnimator animator = ValueAnimator.ofFloat(this.circleRadius, circleRadius);
332     circleAnimator = animator;
333     circleStartValue = this.circleRadius;
334     circleWillBeHidden = circleRadius == 0.0f;
335     animator.addUpdateListener(
336         new ValueAnimator.AnimatorUpdateListener() {
337           @Override
338           public void onAnimationUpdate(ValueAnimator animation) {
339             SwipeButtonView.this.circleRadius = (float) animation.getAnimatedValue();
340             updateIconColor();
341             invalidate();
342           }
343         });
344     animator.addListener(circleEndListener);
345     return animator;
346   }
347 
cancelAnimator(Animator animator)348   private void cancelAnimator(Animator animator) {
349     if (animator != null) {
350       animator.cancel();
351     }
352   }
353 
setImageScale(float imageScale, boolean animate)354   public void setImageScale(float imageScale, boolean animate) {
355     setImageScale(imageScale, animate, -1, null);
356   }
357 
358   /**
359    * Sets the scale of the containing image
360    *
361    * @param imageScale The new Scale.
362    * @param animate Should an animation be performed
363    * @param duration If animate, whats the duration? When -1 we take the default duration
364    * @param interpolator If animate, whats the interpolator? When null we take the default
365    *     interpolator.
366    */
setImageScale( float imageScale, boolean animate, long duration, @Nullable Interpolator interpolator)367   public void setImageScale(
368       float imageScale, boolean animate, long duration, @Nullable Interpolator interpolator) {
369     cancelAnimator(scaleAnimator);
370     if (!animate) {
371       tmageScale = imageScale;
372       invalidate();
373     } else {
374       ValueAnimator animator = ValueAnimator.ofFloat(tmageScale, imageScale);
375       scaleAnimator = animator;
376       animator.addUpdateListener(
377           new ValueAnimator.AnimatorUpdateListener() {
378             @Override
379             public void onAnimationUpdate(ValueAnimator animation) {
380               tmageScale = (float) animation.getAnimatedValue();
381               invalidate();
382             }
383           });
384       animator.addListener(scaleEndListener);
385       if (interpolator == null) {
386         interpolator =
387             imageScale == 0.0f
388                 ? Interpolators.FAST_OUT_LINEAR_IN
389                 : Interpolators.LINEAR_OUT_SLOW_IN;
390       }
391       animator.setInterpolator(interpolator);
392       if (duration == -1) {
393         float durationFactor = Math.abs(tmageScale - imageScale) / (1.0f - MIN_ICON_SCALE_AMOUNT);
394         durationFactor = Math.min(1.0f, durationFactor);
395         duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
396       }
397       animator.setDuration(duration);
398       animator.start();
399     }
400   }
401 
setRestingAlpha(float alpha)402   public void setRestingAlpha(float alpha) {
403     restingAlpha = alpha;
404 
405     // TODO: Handle the case an animation is playing.
406     setImageAlpha(alpha, false);
407   }
408 
getRestingAlpha()409   public float getRestingAlpha() {
410     return restingAlpha;
411   }
412 
setImageAlpha(float alpha, boolean animate)413   public void setImageAlpha(float alpha, boolean animate) {
414     setImageAlpha(alpha, animate, -1, null, null);
415   }
416 
417   /**
418    * Sets the alpha of the containing image
419    *
420    * @param alpha The new alpha.
421    * @param animate Should an animation be performed
422    * @param duration If animate, whats the duration? When -1 we take the default duration
423    * @param interpolator If animate, whats the interpolator? When null we take the default
424    *     interpolator.
425    */
setImageAlpha( float alpha, boolean animate, long duration, @Nullable Interpolator interpolator, @Nullable Runnable runnable)426   public void setImageAlpha(
427       float alpha,
428       boolean animate,
429       long duration,
430       @Nullable Interpolator interpolator,
431       @Nullable Runnable runnable) {
432     cancelAnimator(alphaAnimator);
433     alpha = launchingAffordance ? 0 : alpha;
434     int endAlpha = (int) (alpha * 255);
435     final Drawable background = getBackground();
436     if (!animate) {
437       if (background != null) {
438         background.mutate().setAlpha(endAlpha);
439       }
440       setImageAlpha(endAlpha);
441     } else {
442       int currentAlpha = getImageAlpha();
443       ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha);
444       alphaAnimator = animator;
445       animator.addUpdateListener(
446           new ValueAnimator.AnimatorUpdateListener() {
447             @Override
448             public void onAnimationUpdate(ValueAnimator animation) {
449               int alpha = (int) animation.getAnimatedValue();
450               if (background != null) {
451                 background.mutate().setAlpha(alpha);
452               }
453               setImageAlpha(alpha);
454             }
455           });
456       animator.addListener(alphaEndListener);
457       if (interpolator == null) {
458         interpolator =
459             alpha == 0.0f ? Interpolators.FAST_OUT_LINEAR_IN : Interpolators.LINEAR_OUT_SLOW_IN;
460       }
461       animator.setInterpolator(interpolator);
462       if (duration == -1) {
463         float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f;
464         durationFactor = Math.min(1.0f, durationFactor);
465         duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
466       }
467       animator.setDuration(duration);
468       if (runnable != null) {
469         animator.addListener(getEndListener(runnable));
470       }
471       animator.start();
472     }
473   }
474 
getEndListener(final Runnable runnable)475   private Animator.AnimatorListener getEndListener(final Runnable runnable) {
476     return new AnimatorListenerAdapter() {
477       boolean cancelled;
478 
479       @Override
480       public void onAnimationCancel(Animator animation) {
481         cancelled = true;
482       }
483 
484       @Override
485       public void onAnimationEnd(Animator animation) {
486         if (!cancelled) {
487           runnable.run();
488         }
489       }
490     };
491   }
492 
getCircleRadius()493   public float getCircleRadius() {
494     return circleRadius;
495   }
496 
497   @Override
performClick()498   public boolean performClick() {
499     return isClickable() && super.performClick();
500   }
501 
setLaunchingAffordance(boolean launchingAffordance)502   public void setLaunchingAffordance(boolean launchingAffordance) {
503     this.launchingAffordance = launchingAffordance;
504   }
505 }
506