Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2014 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.systemui.statusbar;
     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.CanvasProperty;
     27 import android.graphics.Color;
     28 import android.graphics.Paint;
     29 import android.graphics.PorterDuff;
     30 import android.graphics.drawable.Drawable;
     31 import android.util.AttributeSet;
     32 import android.view.DisplayListCanvas;
     33 import android.view.RenderNodeAnimator;
     34 import android.view.View;
     35 import android.view.ViewAnimationUtils;
     36 import android.view.animation.Interpolator;
     37 import android.widget.ImageView;
     38 
     39 import com.android.systemui.Interpolators;
     40 import com.android.systemui.R;
     41 import com.android.systemui.statusbar.phone.KeyguardAffordanceHelper;
     42 
     43 /**
     44  * An ImageView which does not have overlapping renderings commands and therefore does not need a
     45  * layer when alpha is changed.
     46  */
     47 public class KeyguardAffordanceView extends ImageView {
     48 
     49     private static final long CIRCLE_APPEAR_DURATION = 80;
     50     private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200;
     51     private static final long NORMAL_ANIMATION_DURATION = 200;
     52     public static final float MAX_ICON_SCALE_AMOUNT = 1.5f;
     53     public static final float MIN_ICON_SCALE_AMOUNT = 0.8f;
     54 
     55     private final int mMinBackgroundRadius;
     56     private final Paint mCirclePaint;
     57     private final int mInverseColor;
     58     private final int mNormalColor;
     59     private final ArgbEvaluator mColorInterpolator;
     60     private final FlingAnimationUtils mFlingAnimationUtils;
     61     private float mCircleRadius;
     62     private int mCenterX;
     63     private int mCenterY;
     64     private ValueAnimator mCircleAnimator;
     65     private ValueAnimator mAlphaAnimator;
     66     private ValueAnimator mScaleAnimator;
     67     private float mCircleStartValue;
     68     private boolean mCircleWillBeHidden;
     69     private int[] mTempPoint = new int[2];
     70     private float mImageScale = 1f;
     71     private int mCircleColor;
     72     private boolean mIsLeft;
     73     private View mPreviewView;
     74     private float mCircleStartRadius;
     75     private float mMaxCircleSize;
     76     private Animator mPreviewClipper;
     77     private float mRestingAlpha = KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT;
     78     private boolean mSupportHardware;
     79     private boolean mFinishing;
     80     private boolean mLaunchingAffordance;
     81 
     82     private CanvasProperty<Float> mHwCircleRadius;
     83     private CanvasProperty<Float> mHwCenterX;
     84     private CanvasProperty<Float> mHwCenterY;
     85     private CanvasProperty<Paint> mHwCirclePaint;
     86 
     87     private AnimatorListenerAdapter mClipEndListener = new AnimatorListenerAdapter() {
     88         @Override
     89         public void onAnimationEnd(Animator animation) {
     90             mPreviewClipper = null;
     91         }
     92     };
     93     private AnimatorListenerAdapter mCircleEndListener = new AnimatorListenerAdapter() {
     94         @Override
     95         public void onAnimationEnd(Animator animation) {
     96             mCircleAnimator = null;
     97         }
     98     };
     99     private AnimatorListenerAdapter mScaleEndListener = new AnimatorListenerAdapter() {
    100         @Override
    101         public void onAnimationEnd(Animator animation) {
    102             mScaleAnimator = null;
    103         }
    104     };
    105     private AnimatorListenerAdapter mAlphaEndListener = new AnimatorListenerAdapter() {
    106         @Override
    107         public void onAnimationEnd(Animator animation) {
    108             mAlphaAnimator = null;
    109         }
    110     };
    111 
    112     public KeyguardAffordanceView(Context context) {
    113         this(context, null);
    114     }
    115 
    116     public KeyguardAffordanceView(Context context, AttributeSet attrs) {
    117         this(context, attrs, 0);
    118     }
    119 
    120     public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) {
    121         this(context, attrs, defStyleAttr, 0);
    122     }
    123 
    124     public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr,
    125             int defStyleRes) {
    126         super(context, attrs, defStyleAttr, defStyleRes);
    127         mCirclePaint = new Paint();
    128         mCirclePaint.setAntiAlias(true);
    129         mCircleColor = 0xffffffff;
    130         mCirclePaint.setColor(mCircleColor);
    131 
    132         mNormalColor = 0xffffffff;
    133         mInverseColor = 0xff000000;
    134         mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
    135                 R.dimen.keyguard_affordance_min_background_radius);
    136         mColorInterpolator = new ArgbEvaluator();
    137         mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.3f);
    138     }
    139 
    140     @Override
    141     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    142         super.onLayout(changed, left, top, right, bottom);
    143         mCenterX = getWidth() / 2;
    144         mCenterY = getHeight() / 2;
    145         mMaxCircleSize = getMaxCircleSize();
    146     }
    147 
    148     @Override
    149     protected void onDraw(Canvas canvas) {
    150         mSupportHardware = canvas.isHardwareAccelerated();
    151         drawBackgroundCircle(canvas);
    152         canvas.save();
    153         canvas.scale(mImageScale, mImageScale, getWidth() / 2, getHeight() / 2);
    154         super.onDraw(canvas);
    155         canvas.restore();
    156     }
    157 
    158     public void setPreviewView(View v) {
    159         View oldPreviewView = mPreviewView;
    160         mPreviewView = v;
    161         if (mPreviewView != null) {
    162             mPreviewView.setVisibility(mLaunchingAffordance
    163                     ? oldPreviewView.getVisibility() : INVISIBLE);
    164         }
    165     }
    166 
    167     private void updateIconColor() {
    168         Drawable drawable = getDrawable().mutate();
    169         float alpha = mCircleRadius / mMinBackgroundRadius;
    170         alpha = Math.min(1.0f, alpha);
    171         int color = (int) mColorInterpolator.evaluate(alpha, mNormalColor, mInverseColor);
    172         drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
    173     }
    174 
    175     private void drawBackgroundCircle(Canvas canvas) {
    176         if (mCircleRadius > 0 || mFinishing) {
    177             if (mFinishing && mSupportHardware && mHwCenterX != null) {
    178                 // Our hardware drawing proparties can be null if the finishing started but we have
    179                 // never drawn before. In that case we are not doing a render thread animation
    180                 // anyway, so we need to use the normal drawing.
    181                 DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas;
    182                 displayListCanvas.drawCircle(mHwCenterX, mHwCenterY, mHwCircleRadius,
    183                         mHwCirclePaint);
    184             } else {
    185                 updateCircleColor();
    186                 canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint);
    187             }
    188         }
    189     }
    190 
    191     private void updateCircleColor() {
    192         float fraction = 0.5f + 0.5f * Math.max(0.0f, Math.min(1.0f,
    193                 (mCircleRadius - mMinBackgroundRadius) / (0.5f * mMinBackgroundRadius)));
    194         if (mPreviewView != null && mPreviewView.getVisibility() == VISIBLE) {
    195             float finishingFraction = 1 - Math.max(0, mCircleRadius - mCircleStartRadius)
    196                     / (mMaxCircleSize - mCircleStartRadius);
    197             fraction *= finishingFraction;
    198         }
    199         int color = Color.argb((int) (Color.alpha(mCircleColor) * fraction),
    200                 Color.red(mCircleColor),
    201                 Color.green(mCircleColor), Color.blue(mCircleColor));
    202         mCirclePaint.setColor(color);
    203     }
    204 
    205     public void finishAnimation(float velocity, final Runnable mAnimationEndRunnable) {
    206         cancelAnimator(mCircleAnimator);
    207         cancelAnimator(mPreviewClipper);
    208         mFinishing = true;
    209         mCircleStartRadius = mCircleRadius;
    210         final float maxCircleSize = getMaxCircleSize();
    211         Animator animatorToRadius;
    212         if (mSupportHardware) {
    213             initHwProperties();
    214             animatorToRadius = getRtAnimatorToRadius(maxCircleSize);
    215             startRtAlphaFadeIn();
    216         } else {
    217             animatorToRadius = getAnimatorToRadius(maxCircleSize);
    218         }
    219         mFlingAnimationUtils.applyDismissing(animatorToRadius, mCircleRadius, maxCircleSize,
    220                 velocity, maxCircleSize);
    221         animatorToRadius.addListener(new AnimatorListenerAdapter() {
    222             @Override
    223             public void onAnimationEnd(Animator animation) {
    224                 mAnimationEndRunnable.run();
    225                 mFinishing = false;
    226                 mCircleRadius = maxCircleSize;
    227                 invalidate();
    228             }
    229         });
    230         animatorToRadius.start();
    231         setImageAlpha(0, true);
    232         if (mPreviewView != null) {
    233             mPreviewView.setVisibility(View.VISIBLE);
    234             mPreviewClipper = ViewAnimationUtils.createCircularReveal(
    235                     mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
    236                     maxCircleSize);
    237             mFlingAnimationUtils.applyDismissing(mPreviewClipper, mCircleRadius, maxCircleSize,
    238                     velocity, maxCircleSize);
    239             mPreviewClipper.addListener(mClipEndListener);
    240             mPreviewClipper.start();
    241             if (mSupportHardware) {
    242                 startRtCircleFadeOut(animatorToRadius.getDuration());
    243             }
    244         }
    245     }
    246 
    247     /**
    248      * Fades in the Circle on the RenderThread. It's used when finishing the circle when it had
    249      * alpha 0 in the beginning.
    250      */
    251     private void startRtAlphaFadeIn() {
    252         if (mCircleRadius == 0 && mPreviewView == null) {
    253             Paint modifiedPaint = new Paint(mCirclePaint);
    254             modifiedPaint.setColor(mCircleColor);
    255             modifiedPaint.setAlpha(0);
    256             mHwCirclePaint = CanvasProperty.createPaint(modifiedPaint);
    257             RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
    258                     RenderNodeAnimator.PAINT_ALPHA, 255);
    259             animator.setTarget(this);
    260             animator.setInterpolator(Interpolators.ALPHA_IN);
    261             animator.setDuration(250);
    262             animator.start();
    263         }
    264     }
    265 
    266     public void instantFinishAnimation() {
    267         cancelAnimator(mPreviewClipper);
    268         if (mPreviewView != null) {
    269             mPreviewView.setClipBounds(null);
    270             mPreviewView.setVisibility(View.VISIBLE);
    271         }
    272         mCircleRadius = getMaxCircleSize();
    273         setImageAlpha(0, false);
    274         invalidate();
    275     }
    276 
    277     private void startRtCircleFadeOut(long duration) {
    278         RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
    279                 RenderNodeAnimator.PAINT_ALPHA, 0);
    280         animator.setDuration(duration);
    281         animator.setInterpolator(Interpolators.ALPHA_OUT);
    282         animator.setTarget(this);
    283         animator.start();
    284     }
    285 
    286     private Animator getRtAnimatorToRadius(float circleRadius) {
    287         RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius);
    288         animator.setTarget(this);
    289         return animator;
    290     }
    291 
    292     private void initHwProperties() {
    293         mHwCenterX = CanvasProperty.createFloat(mCenterX);
    294         mHwCenterY = CanvasProperty.createFloat(mCenterY);
    295         mHwCirclePaint = CanvasProperty.createPaint(mCirclePaint);
    296         mHwCircleRadius = CanvasProperty.createFloat(mCircleRadius);
    297     }
    298 
    299     private float getMaxCircleSize() {
    300         getLocationInWindow(mTempPoint);
    301         float rootWidth = getRootView().getWidth();
    302         float width = mTempPoint[0] + mCenterX;
    303         width = Math.max(rootWidth - width, width);
    304         float height = mTempPoint[1] + mCenterY;
    305         return (float) Math.hypot(width, height);
    306     }
    307 
    308     public void setCircleRadius(float circleRadius) {
    309         setCircleRadius(circleRadius, false, false);
    310     }
    311 
    312     public void setCircleRadius(float circleRadius, boolean slowAnimation) {
    313         setCircleRadius(circleRadius, slowAnimation, false);
    314     }
    315 
    316     public void setCircleRadiusWithoutAnimation(float circleRadius) {
    317         cancelAnimator(mCircleAnimator);
    318         setCircleRadius(circleRadius, false ,true);
    319     }
    320 
    321     private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
    322 
    323         // Check if we need a new animation
    324         boolean radiusHidden = (mCircleAnimator != null && mCircleWillBeHidden)
    325                 || (mCircleAnimator == null && mCircleRadius == 0.0f);
    326         boolean nowHidden = circleRadius == 0.0f;
    327         boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
    328         if (!radiusNeedsAnimation) {
    329             if (mCircleAnimator == null) {
    330                 mCircleRadius = circleRadius;
    331                 updateIconColor();
    332                 invalidate();
    333                 if (nowHidden) {
    334                     if (mPreviewView != null) {
    335                         mPreviewView.setVisibility(View.INVISIBLE);
    336                     }
    337                 }
    338             } else if (!mCircleWillBeHidden) {
    339 
    340                 // We just update the end value
    341                 float diff = circleRadius - mMinBackgroundRadius;
    342                 PropertyValuesHolder[] values = mCircleAnimator.getValues();
    343                 values[0].setFloatValues(mCircleStartValue + diff, circleRadius);
    344                 mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime());
    345             }
    346         } else {
    347             cancelAnimator(mCircleAnimator);
    348             cancelAnimator(mPreviewClipper);
    349             ValueAnimator animator = getAnimatorToRadius(circleRadius);
    350             Interpolator interpolator = circleRadius == 0.0f
    351                     ? Interpolators.FAST_OUT_LINEAR_IN
    352                     : Interpolators.LINEAR_OUT_SLOW_IN;
    353             animator.setInterpolator(interpolator);
    354             long duration = 250;
    355             if (!slowAnimation) {
    356                 float durationFactor = Math.abs(mCircleRadius - circleRadius)
    357                         / (float) mMinBackgroundRadius;
    358                 duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
    359                 duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
    360             }
    361             animator.setDuration(duration);
    362             animator.start();
    363             if (mPreviewView != null && mPreviewView.getVisibility() == View.VISIBLE) {
    364                 mPreviewView.setVisibility(View.VISIBLE);
    365                 mPreviewClipper = ViewAnimationUtils.createCircularReveal(
    366                         mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
    367                         circleRadius);
    368                 mPreviewClipper.setInterpolator(interpolator);
    369                 mPreviewClipper.setDuration(duration);
    370                 mPreviewClipper.addListener(mClipEndListener);
    371                 mPreviewClipper.addListener(new AnimatorListenerAdapter() {
    372                     @Override
    373                     public void onAnimationEnd(Animator animation) {
    374                         mPreviewView.setVisibility(View.INVISIBLE);
    375                     }
    376                 });
    377                 mPreviewClipper.start();
    378             }
    379         }
    380     }
    381 
    382     private ValueAnimator getAnimatorToRadius(float circleRadius) {
    383         ValueAnimator animator = ValueAnimator.ofFloat(mCircleRadius, circleRadius);
    384         mCircleAnimator = animator;
    385         mCircleStartValue = mCircleRadius;
    386         mCircleWillBeHidden = circleRadius == 0.0f;
    387         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    388             @Override
    389             public void onAnimationUpdate(ValueAnimator animation) {
    390                 mCircleRadius = (float) animation.getAnimatedValue();
    391                 updateIconColor();
    392                 invalidate();
    393             }
    394         });
    395         animator.addListener(mCircleEndListener);
    396         return animator;
    397     }
    398 
    399     private void cancelAnimator(Animator animator) {
    400         if (animator != null) {
    401             animator.cancel();
    402         }
    403     }
    404 
    405     public void setImageScale(float imageScale, boolean animate) {
    406         setImageScale(imageScale, animate, -1, null);
    407     }
    408 
    409     /**
    410      * Sets the scale of the containing image
    411      *
    412      * @param imageScale The new Scale.
    413      * @param animate Should an animation be performed
    414      * @param duration If animate, whats the duration? When -1 we take the default duration
    415      * @param interpolator If animate, whats the interpolator? When null we take the default
    416      *                     interpolator.
    417      */
    418     public void setImageScale(float imageScale, boolean animate, long duration,
    419             Interpolator interpolator) {
    420         cancelAnimator(mScaleAnimator);
    421         if (!animate) {
    422             mImageScale = imageScale;
    423             invalidate();
    424         } else {
    425             ValueAnimator animator = ValueAnimator.ofFloat(mImageScale, imageScale);
    426             mScaleAnimator = animator;
    427             animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    428                 @Override
    429                 public void onAnimationUpdate(ValueAnimator animation) {
    430                     mImageScale = (float) animation.getAnimatedValue();
    431                     invalidate();
    432                 }
    433             });
    434             animator.addListener(mScaleEndListener);
    435             if (interpolator == null) {
    436                 interpolator = imageScale == 0.0f
    437                         ? Interpolators.FAST_OUT_LINEAR_IN
    438                         : Interpolators.LINEAR_OUT_SLOW_IN;
    439             }
    440             animator.setInterpolator(interpolator);
    441             if (duration == -1) {
    442                 float durationFactor = Math.abs(mImageScale - imageScale)
    443                         / (1.0f - MIN_ICON_SCALE_AMOUNT);
    444                 durationFactor = Math.min(1.0f, durationFactor);
    445                 duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
    446             }
    447             animator.setDuration(duration);
    448             animator.start();
    449         }
    450     }
    451 
    452     public void setRestingAlpha(float alpha) {
    453         mRestingAlpha = alpha;
    454 
    455         // TODO: Handle the case an animation is playing.
    456         setImageAlpha(alpha, false);
    457     }
    458 
    459     public float getRestingAlpha() {
    460         return mRestingAlpha;
    461     }
    462 
    463     public void setImageAlpha(float alpha, boolean animate) {
    464         setImageAlpha(alpha, animate, -1, null, null);
    465     }
    466 
    467     /**
    468      * Sets the alpha of the containing image
    469      *
    470      * @param alpha The new alpha.
    471      * @param animate Should an animation be performed
    472      * @param duration If animate, whats the duration? When -1 we take the default duration
    473      * @param interpolator If animate, whats the interpolator? When null we take the default
    474      *                     interpolator.
    475      */
    476     public void setImageAlpha(float alpha, boolean animate, long duration,
    477             Interpolator interpolator, Runnable runnable) {
    478         cancelAnimator(mAlphaAnimator);
    479         alpha = mLaunchingAffordance ? 0 : alpha;
    480         int endAlpha = (int) (alpha * 255);
    481         final Drawable background = getBackground();
    482         if (!animate) {
    483             if (background != null) background.mutate().setAlpha(endAlpha);
    484             setImageAlpha(endAlpha);
    485         } else {
    486             int currentAlpha = getImageAlpha();
    487             ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha);
    488             mAlphaAnimator = animator;
    489             animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    490                 @Override
    491                 public void onAnimationUpdate(ValueAnimator animation) {
    492                     int alpha = (int) animation.getAnimatedValue();
    493                     if (background != null) background.mutate().setAlpha(alpha);
    494                     setImageAlpha(alpha);
    495                 }
    496             });
    497             animator.addListener(mAlphaEndListener);
    498             if (interpolator == null) {
    499                 interpolator = alpha == 0.0f
    500                         ? Interpolators.FAST_OUT_LINEAR_IN
    501                         : Interpolators.LINEAR_OUT_SLOW_IN;
    502             }
    503             animator.setInterpolator(interpolator);
    504             if (duration == -1) {
    505                 float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f;
    506                 durationFactor = Math.min(1.0f, durationFactor);
    507                 duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
    508             }
    509             animator.setDuration(duration);
    510             if (runnable != null) {
    511                 animator.addListener(getEndListener(runnable));
    512             }
    513             animator.start();
    514         }
    515     }
    516 
    517     private Animator.AnimatorListener getEndListener(final Runnable runnable) {
    518         return new AnimatorListenerAdapter() {
    519             boolean mCancelled;
    520             @Override
    521             public void onAnimationCancel(Animator animation) {
    522                 mCancelled = true;
    523             }
    524 
    525             @Override
    526             public void onAnimationEnd(Animator animation) {
    527                 if (!mCancelled) {
    528                     runnable.run();
    529                 }
    530             }
    531         };
    532     }
    533 
    534     public float getCircleRadius() {
    535         return mCircleRadius;
    536     }
    537 
    538     @Override
    539     public boolean performClick() {
    540         if (isClickable()) {
    541             return super.performClick();
    542         } else {
    543             return false;
    544         }
    545     }
    546 
    547     public void setLaunchingAffordance(boolean launchingAffordance) {
    548         mLaunchingAffordance = launchingAffordance;
    549     }
    550 }
    551