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
17package com.android.incallui.answer.impl.affordance;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ArgbEvaluator;
22import android.animation.PropertyValuesHolder;
23import android.animation.ValueAnimator;
24import android.content.Context;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Paint;
28import android.graphics.PorterDuff;
29import android.graphics.drawable.Drawable;
30import android.support.annotation.Nullable;
31import android.util.AttributeSet;
32import android.view.View;
33import android.view.ViewAnimationUtils;
34import android.view.animation.Interpolator;
35import android.widget.ImageView;
36import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
37import com.android.incallui.answer.impl.utils.Interpolators;
38
39/** Button that allows swiping to trigger */
40public 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
102  public SwipeButtonView(Context context) {
103    this(context, null);
104  }
105
106  public SwipeButtonView(Context context, AttributeSet attrs) {
107    this(context, attrs, 0);
108  }
109
110  public SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr) {
111    this(context, attrs, defStyleAttr, 0);
112  }
113
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
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
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
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
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
164  private void drawBackgroundCircle(Canvas canvas) {
165    if (circleRadius > 0 || finishing) {
166      updateCircleColor();
167      canvas.drawCircle(centerX, centerY, circleRadius, circlePaint);
168    }
169  }
170
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
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
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
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
249  public void setCircleRadius(float circleRadius) {
250    setCircleRadius(circleRadius, false, false);
251  }
252
253  public void setCircleRadius(float circleRadius, boolean slowAnimation) {
254    setCircleRadius(circleRadius, slowAnimation, false);
255  }
256
257  public void setCircleRadiusWithoutAnimation(float circleRadius) {
258    cancelAnimator(circleAnimator);
259    setCircleRadius(circleRadius, false, true);
260  }
261
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
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
348  private void cancelAnimator(Animator animator) {
349    if (animator != null) {
350      animator.cancel();
351    }
352  }
353
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   */
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
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
409  public float getRestingAlpha() {
410    return restingAlpha;
411  }
412
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   */
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
475  private Animator.AnimatorListener getEndListener(final Runnable runnable) {
476    return new AnimatorListenerAdapter() {
477      boolean mCancelled;
478
479      @Override
480      public void onAnimationCancel(Animator animation) {
481        mCancelled = true;
482      }
483
484      @Override
485      public void onAnimationEnd(Animator animation) {
486        if (!mCancelled) {
487          runnable.run();
488        }
489      }
490    };
491  }
492
493  public float getCircleRadius() {
494    return circleRadius;
495  }
496
497  @Override
498  public boolean performClick() {
499    return isClickable() && super.performClick();
500  }
501
502  public void setLaunchingAffordance(boolean launchingAffordance) {
503    this.launchingAffordance = launchingAffordance;
504  }
505}
506