Home | History | Annotate | Download | only in affordance
      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 
    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 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 
    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