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