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 = false;//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) {
    178                 DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas;
    179                 displayListCanvas.drawCircle(mHwCenterX, mHwCenterY, mHwCircleRadius,
    180                         mHwCirclePaint);
    181             } else {
    182                 updateCircleColor();
    183                 canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint);
    184             }
    185         }
    186     }
    187 
    188     private void updateCircleColor() {
    189         float fraction = 0.5f + 0.5f * Math.max(0.0f, Math.min(1.0f,
    190                 (mCircleRadius - mMinBackgroundRadius) / (0.5f * mMinBackgroundRadius)));
    191         if (mPreviewView != null && mPreviewView.getVisibility() == VISIBLE) {
    192             float finishingFraction = 1 - Math.max(0, mCircleRadius - mCircleStartRadius)
    193                     / (mMaxCircleSize - mCircleStartRadius);
    194             fraction *= finishingFraction;
    195         }
    196         int color = Color.argb((int) (Color.alpha(mCircleColor) * fraction),
    197                 Color.red(mCircleColor),
    198                 Color.green(mCircleColor), Color.blue(mCircleColor));
    199         mCirclePaint.setColor(color);
    200     }
    201 
    202     public void finishAnimation(float velocity, final Runnable mAnimationEndRunnable) {
    203         cancelAnimator(mCircleAnimator);
    204         cancelAnimator(mPreviewClipper);
    205         mFinishing = true;
    206         mCircleStartRadius = mCircleRadius;
    207         final float maxCircleSize = getMaxCircleSize();
    208         Animator animatorToRadius;
    209         if (mSupportHardware) {
    210             initHwProperties();
    211             animatorToRadius = getRtAnimatorToRadius(maxCircleSize);
    212             startRtAlphaFadeIn();
    213         } else {
    214             animatorToRadius = getAnimatorToRadius(maxCircleSize);
    215         }
    216         mFlingAnimationUtils.applyDismissing(animatorToRadius, mCircleRadius, maxCircleSize,
    217                 velocity, maxCircleSize);
    218         animatorToRadius.addListener(new AnimatorListenerAdapter() {
    219             @Override
    220             public void onAnimationEnd(Animator animation) {
    221                 mAnimationEndRunnable.run();
    222                 mFinishing = false;
    223                 mCircleRadius = maxCircleSize;
    224                 invalidate();
    225             }
    226         });
    227         animatorToRadius.start();
    228         setImageAlpha(0, true);
    229         if (mPreviewView != null) {
    230             mPreviewView.setVisibility(View.VISIBLE);
    231             mPreviewClipper = ViewAnimationUtils.createCircularReveal(
    232                     mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
    233                     maxCircleSize);
    234             mFlingAnimationUtils.applyDismissing(mPreviewClipper, mCircleRadius, maxCircleSize,
    235                     velocity, maxCircleSize);
    236             mPreviewClipper.addListener(mClipEndListener);
    237             mPreviewClipper.start();
    238             if (mSupportHardware) {
    239                 startRtCircleFadeOut(animatorToRadius.getDuration());
    240             }
    241         }
    242     }
    243 
    244     /**
    245      * Fades in the Circle on the RenderThread. It's used when finishing the circle when it had
    246      * alpha 0 in the beginning.
    247      */
    248     private void startRtAlphaFadeIn() {
    249         if (mCircleRadius == 0 && mPreviewView == null) {
    250             Paint modifiedPaint = new Paint(mCirclePaint);
    251             modifiedPaint.setColor(mCircleColor);
    252             modifiedPaint.setAlpha(0);
    253             mHwCirclePaint = CanvasProperty.createPaint(modifiedPaint);
    254             RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
    255                     RenderNodeAnimator.PAINT_ALPHA, 255);
    256             animator.setTarget(this);
    257             animator.setInterpolator(Interpolators.ALPHA_IN);
    258             animator.setDuration(250);
    259             animator.start();
    260         }
    261     }
    262 
    263     public void instantFinishAnimation() {
    264         cancelAnimator(mPreviewClipper);
    265         if (mPreviewView != null) {
    266             mPreviewView.setClipBounds(null);
    267             mPreviewView.setVisibility(View.VISIBLE);
    268         }
    269         mCircleRadius = getMaxCircleSize();
    270         setImageAlpha(0, false);
    271         invalidate();
    272     }
    273 
    274     private void startRtCircleFadeOut(long duration) {
    275         RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
    276                 RenderNodeAnimator.PAINT_ALPHA, 0);
    277         animator.setDuration(duration);
    278         animator.setInterpolator(Interpolators.ALPHA_OUT);
    279         animator.setTarget(this);
    280         animator.start();
    281     }
    282 
    283     private Animator getRtAnimatorToRadius(float circleRadius) {
    284         RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius);
    285         animator.setTarget(this);
    286         return animator;
    287     }
    288 
    289     private void initHwProperties() {
    290         mHwCenterX = CanvasProperty.createFloat(mCenterX);
    291         mHwCenterY = CanvasProperty.createFloat(mCenterY);
    292         mHwCirclePaint = CanvasProperty.createPaint(mCirclePaint);
    293         mHwCircleRadius = CanvasProperty.createFloat(mCircleRadius);
    294     }
    295 
    296     private float getMaxCircleSize() {
    297         getLocationInWindow(mTempPoint);
    298         float rootWidth = getRootView().getWidth();
    299         float width = mTempPoint[0] + mCenterX;
    300         width = Math.max(rootWidth - width, width);
    301         float height = mTempPoint[1] + mCenterY;
    302         return (float) Math.hypot(width, height);
    303     }
    304 
    305     public void setCircleRadius(float circleRadius) {
    306         setCircleRadius(circleRadius, false, false);
    307     }
    308 
    309     public void setCircleRadius(float circleRadius, boolean slowAnimation) {
    310         setCircleRadius(circleRadius, slowAnimation, false);
    311     }
    312 
    313     public void setCircleRadiusWithoutAnimation(float circleRadius) {
    314         cancelAnimator(mCircleAnimator);
    315         setCircleRadius(circleRadius, false ,true);
    316     }
    317 
    318     private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
    319 
    320         // Check if we need a new animation
    321         boolean radiusHidden = (mCircleAnimator != null && mCircleWillBeHidden)
    322                 || (mCircleAnimator == null && mCircleRadius == 0.0f);
    323         boolean nowHidden = circleRadius == 0.0f;
    324         boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
    325         if (!radiusNeedsAnimation) {
    326             if (mCircleAnimator == null) {
    327                 mCircleRadius = circleRadius;
    328                 updateIconColor();
    329                 invalidate();
    330                 if (nowHidden) {
    331                     if (mPreviewView != null) {
    332                         mPreviewView.setVisibility(View.INVISIBLE);
    333                     }
    334                 }
    335             } else if (!mCircleWillBeHidden) {
    336 
    337                 // We just update the end value
    338                 float diff = circleRadius - mMinBackgroundRadius;
    339                 PropertyValuesHolder[] values = mCircleAnimator.getValues();
    340                 values[0].setFloatValues(mCircleStartValue + diff, circleRadius);
    341                 mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime());
    342             }
    343         } else {
    344             cancelAnimator(mCircleAnimator);
    345             cancelAnimator(mPreviewClipper);
    346             ValueAnimator animator = getAnimatorToRadius(circleRadius);
    347             Interpolator interpolator = circleRadius == 0.0f
    348                     ? Interpolators.FAST_OUT_LINEAR_IN
    349                     : Interpolators.LINEAR_OUT_SLOW_IN;
    350             animator.setInterpolator(interpolator);
    351             long duration = 250;
    352             if (!slowAnimation) {
    353                 float durationFactor = Math.abs(mCircleRadius - circleRadius)
    354                         / (float) mMinBackgroundRadius;
    355                 duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
    356                 duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
    357             }
    358             animator.setDuration(duration);
    359             animator.start();
    360             if (mPreviewView != null && mPreviewView.getVisibility() == View.VISIBLE) {
    361                 mPreviewView.setVisibility(View.VISIBLE);
    362                 mPreviewClipper = ViewAnimationUtils.createCircularReveal(
    363                         mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
    364                         circleRadius);
    365                 mPreviewClipper.setInterpolator(interpolator);
    366                 mPreviewClipper.setDuration(duration);
    367                 mPreviewClipper.addListener(mClipEndListener);
    368                 mPreviewClipper.addListener(new AnimatorListenerAdapter() {
    369                     @Override
    370                     public void onAnimationEnd(Animator animation) {
    371                         mPreviewView.setVisibility(View.INVISIBLE);
    372                     }
    373                 });
    374                 mPreviewClipper.start();
    375             }
    376         }
    377     }
    378 
    379     private ValueAnimator getAnimatorToRadius(float circleRadius) {
    380         ValueAnimator animator = ValueAnimator.ofFloat(mCircleRadius, circleRadius);
    381         mCircleAnimator = animator;
    382         mCircleStartValue = mCircleRadius;
    383         mCircleWillBeHidden = circleRadius == 0.0f;
    384         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    385             @Override
    386             public void onAnimationUpdate(ValueAnimator animation) {
    387                 mCircleRadius = (float) animation.getAnimatedValue();
    388                 updateIconColor();
    389                 invalidate();
    390             }
    391         });
    392         animator.addListener(mCircleEndListener);
    393         return animator;
    394     }
    395 
    396     private void cancelAnimator(Animator animator) {
    397         if (animator != null) {
    398             animator.cancel();
    399         }
    400     }
    401 
    402     public void setImageScale(float imageScale, boolean animate) {
    403         setImageScale(imageScale, animate, -1, null);
    404     }
    405 
    406     /**
    407      * Sets the scale of the containing image
    408      *
    409      * @param imageScale The new Scale.
    410      * @param animate Should an animation be performed
    411      * @param duration If animate, whats the duration? When -1 we take the default duration
    412      * @param interpolator If animate, whats the interpolator? When null we take the default
    413      *                     interpolator.
    414      */
    415     public void setImageScale(float imageScale, boolean animate, long duration,
    416             Interpolator interpolator) {
    417         cancelAnimator(mScaleAnimator);
    418         if (!animate) {
    419             mImageScale = imageScale;
    420             invalidate();
    421         } else {
    422             ValueAnimator animator = ValueAnimator.ofFloat(mImageScale, imageScale);
    423             mScaleAnimator = animator;
    424             animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    425                 @Override
    426                 public void onAnimationUpdate(ValueAnimator animation) {
    427                     mImageScale = (float) animation.getAnimatedValue();
    428                     invalidate();
    429                 }
    430             });
    431             animator.addListener(mScaleEndListener);
    432             if (interpolator == null) {
    433                 interpolator = imageScale == 0.0f
    434                         ? Interpolators.FAST_OUT_LINEAR_IN
    435                         : Interpolators.LINEAR_OUT_SLOW_IN;
    436             }
    437             animator.setInterpolator(interpolator);
    438             if (duration == -1) {
    439                 float durationFactor = Math.abs(mImageScale - imageScale)
    440                         / (1.0f - MIN_ICON_SCALE_AMOUNT);
    441                 durationFactor = Math.min(1.0f, durationFactor);
    442                 duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
    443             }
    444             animator.setDuration(duration);
    445             animator.start();
    446         }
    447     }
    448 
    449     public void setRestingAlpha(float alpha) {
    450         mRestingAlpha = alpha;
    451 
    452         // TODO: Handle the case an animation is playing.
    453         setImageAlpha(alpha, false);
    454     }
    455 
    456     public float getRestingAlpha() {
    457         return mRestingAlpha;
    458     }
    459 
    460     public void setImageAlpha(float alpha, boolean animate) {
    461         setImageAlpha(alpha, animate, -1, null, null);
    462     }
    463 
    464     /**
    465      * Sets the alpha of the containing image
    466      *
    467      * @param alpha The new alpha.
    468      * @param animate Should an animation be performed
    469      * @param duration If animate, whats the duration? When -1 we take the default duration
    470      * @param interpolator If animate, whats the interpolator? When null we take the default
    471      *                     interpolator.
    472      */
    473     public void setImageAlpha(float alpha, boolean animate, long duration,
    474             Interpolator interpolator, Runnable runnable) {
    475         cancelAnimator(mAlphaAnimator);
    476         alpha = mLaunchingAffordance ? 0 : alpha;
    477         int endAlpha = (int) (alpha * 255);
    478         final Drawable background = getBackground();
    479         if (!animate) {
    480             if (background != null) background.mutate().setAlpha(endAlpha);
    481             setImageAlpha(endAlpha);
    482         } else {
    483             int currentAlpha = getImageAlpha();
    484             ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha);
    485             mAlphaAnimator = animator;
    486             animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    487                 @Override
    488                 public void onAnimationUpdate(ValueAnimator animation) {
    489                     int alpha = (int) animation.getAnimatedValue();
    490                     if (background != null) background.mutate().setAlpha(alpha);
    491                     setImageAlpha(alpha);
    492                 }
    493             });
    494             animator.addListener(mAlphaEndListener);
    495             if (interpolator == null) {
    496                 interpolator = alpha == 0.0f
    497                         ? Interpolators.FAST_OUT_LINEAR_IN
    498                         : Interpolators.LINEAR_OUT_SLOW_IN;
    499             }
    500             animator.setInterpolator(interpolator);
    501             if (duration == -1) {
    502                 float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f;
    503                 durationFactor = Math.min(1.0f, durationFactor);
    504                 duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
    505             }
    506             animator.setDuration(duration);
    507             if (runnable != null) {
    508                 animator.addListener(getEndListener(runnable));
    509             }
    510             animator.start();
    511         }
    512     }
    513 
    514     private Animator.AnimatorListener getEndListener(final Runnable runnable) {
    515         return new AnimatorListenerAdapter() {
    516             boolean mCancelled;
    517             @Override
    518             public void onAnimationCancel(Animator animation) {
    519                 mCancelled = true;
    520             }
    521 
    522             @Override
    523             public void onAnimationEnd(Animator animation) {
    524                 if (!mCancelled) {
    525                     runnable.run();
    526                 }
    527             }
    528         };
    529     }
    530 
    531     public float getCircleRadius() {
    532         return mCircleRadius;
    533     }
    534 
    535     @Override
    536     public boolean performClick() {
    537         if (isClickable()) {
    538             return super.performClick();
    539         } else {
    540             return false;
    541         }
    542     }
    543 
    544     public void setLaunchingAffordance(boolean launchingAffordance) {
    545         mLaunchingAffordance = launchingAffordance;
    546     }
    547 }
    548