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