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.ObjectAnimator;
     22 import android.animation.ValueAnimator;
     23 import android.content.Context;
     24 import android.graphics.Bitmap;
     25 import android.graphics.BitmapShader;
     26 import android.graphics.Canvas;
     27 import android.graphics.Color;
     28 import android.graphics.Paint;
     29 import android.graphics.PorterDuff;
     30 import android.graphics.PorterDuffColorFilter;
     31 import android.graphics.RectF;
     32 import android.graphics.Shader;
     33 import android.util.AttributeSet;
     34 import android.view.MotionEvent;
     35 import android.view.View;
     36 import android.view.ViewAnimationUtils;
     37 import android.view.ViewConfiguration;
     38 import android.view.animation.AnimationUtils;
     39 import android.view.animation.Interpolator;
     40 import android.view.animation.LinearInterpolator;
     41 import android.view.animation.PathInterpolator;
     42 
     43 import com.android.systemui.R;
     44 
     45 /**
     46  * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer}
     47  * to implement dimming/activating on Keyguard for the double-tap gesture
     48  */
     49 public abstract class ActivatableNotificationView extends ExpandableOutlineView {
     50 
     51     private static final long DOUBLETAP_TIMEOUT_MS = 1200;
     52     private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
     53     private static final int ACTIVATE_ANIMATION_LENGTH = 220;
     54     private static final int DARK_ANIMATION_LENGTH = 170;
     55 
     56     /**
     57      * The amount of width, which is kept in the end when performing a disappear animation (also
     58      * the amount from which the horizontal appearing begins)
     59      */
     60     private static final float HORIZONTAL_COLLAPSED_REST_PARTIAL = 0.05f;
     61 
     62     /**
     63      * At which point from [0,1] does the horizontal collapse animation end (or start when
     64      * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
     65      */
     66     private static final float HORIZONTAL_ANIMATION_END = 0.2f;
     67 
     68     /**
     69      * At which point from [0,1] does the alpha animation end (or start when
     70      * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
     71      */
     72     private static final float ALPHA_ANIMATION_END = 0.0f;
     73 
     74     /**
     75      * At which point from [0,1] does the horizontal collapse animation start (or start when
     76      * expanding)? 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
     77      */
     78     private static final float HORIZONTAL_ANIMATION_START = 1.0f;
     79 
     80     /**
     81      * At which point from [0,1] does the vertical collapse animation start (or end when
     82      * expanding) 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
     83      */
     84     private static final float VERTICAL_ANIMATION_START = 1.0f;
     85 
     86     /**
     87      * Scale for the background to animate from when exiting dark mode.
     88      */
     89     private static final float DARK_EXIT_SCALE_START = 0.93f;
     90 
     91     private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
     92             = new PathInterpolator(0.6f, 0, 0.5f, 1);
     93     private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
     94             = new PathInterpolator(0, 0, 0.5f, 1);
     95     private final int mTintedRippleColor;
     96     private final int mLowPriorityRippleColor;
     97     private final int mNormalRippleColor;
     98 
     99     private boolean mDimmed;
    100     private boolean mDark;
    101 
    102     private int mBgTint = 0;
    103     private final int mRoundedRectCornerRadius;
    104 
    105     /**
    106      * Flag to indicate that the notification has been touched once and the second touch will
    107      * click it.
    108      */
    109     private boolean mActivated;
    110 
    111     private float mDownX;
    112     private float mDownY;
    113     private final float mTouchSlop;
    114 
    115     private OnActivatedListener mOnActivatedListener;
    116 
    117     private final Interpolator mLinearOutSlowInInterpolator;
    118     private final Interpolator mFastOutSlowInInterpolator;
    119     private final Interpolator mSlowOutFastInInterpolator;
    120     private final Interpolator mSlowOutLinearInInterpolator;
    121     private final Interpolator mLinearInterpolator;
    122     private Interpolator mCurrentAppearInterpolator;
    123     private Interpolator mCurrentAlphaInterpolator;
    124 
    125     private NotificationBackgroundView mBackgroundNormal;
    126     private NotificationBackgroundView mBackgroundDimmed;
    127     private ObjectAnimator mBackgroundAnimator;
    128     private RectF mAppearAnimationRect = new RectF();
    129     private PorterDuffColorFilter mAppearAnimationFilter;
    130     private float mAnimationTranslationY;
    131     private boolean mDrawingAppearAnimation;
    132     private Paint mAppearPaint = new Paint();
    133     private ValueAnimator mAppearAnimator;
    134     private float mAppearAnimationFraction = -1.0f;
    135     private float mAppearAnimationTranslation;
    136     private boolean mShowingLegacyBackground;
    137     private final int mLegacyColor;
    138     private final int mNormalColor;
    139     private final int mLowPriorityColor;
    140     private boolean mIsBelowSpeedBump;
    141 
    142     public ActivatableNotificationView(Context context, AttributeSet attrs) {
    143         super(context, attrs);
    144         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    145         mFastOutSlowInInterpolator =
    146                 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
    147         mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
    148         mLinearOutSlowInInterpolator =
    149                 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
    150         mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f);
    151         mLinearInterpolator = new LinearInterpolator();
    152         setClipChildren(false);
    153         setClipToPadding(false);
    154         mAppearAnimationFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
    155         mRoundedRectCornerRadius = getResources().getDimensionPixelSize(
    156                 R.dimen.notification_material_rounded_rect_radius);
    157         mLegacyColor = getResources().getColor(R.color.notification_legacy_background_color);
    158         mNormalColor = getResources().getColor(R.color.notification_material_background_color);
    159         mLowPriorityColor = getResources().getColor(
    160                 R.color.notification_material_background_low_priority_color);
    161         mTintedRippleColor = context.getResources().getColor(
    162                 R.color.notification_ripple_tinted_color);
    163         mLowPriorityRippleColor = context.getResources().getColor(
    164                 R.color.notification_ripple_color_low_priority);
    165         mNormalRippleColor = context.getResources().getColor(
    166                 R.color.notification_ripple_untinted_color);
    167     }
    168 
    169     @Override
    170     protected void onFinishInflate() {
    171         super.onFinishInflate();
    172         mBackgroundNormal = (NotificationBackgroundView) findViewById(R.id.backgroundNormal);
    173         mBackgroundDimmed = (NotificationBackgroundView) findViewById(R.id.backgroundDimmed);
    174         mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg);
    175         mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim);
    176         updateBackground();
    177         updateBackgroundTint();
    178     }
    179 
    180     private final Runnable mTapTimeoutRunnable = new Runnable() {
    181         @Override
    182         public void run() {
    183             makeInactive(true /* animate */);
    184         }
    185     };
    186 
    187     @Override
    188     public boolean onTouchEvent(MotionEvent event) {
    189         if (mDimmed) {
    190             return handleTouchEventDimmed(event);
    191         } else {
    192             return super.onTouchEvent(event);
    193         }
    194     }
    195 
    196     @Override
    197     public void drawableHotspotChanged(float x, float y) {
    198         if (!mDimmed){
    199             mBackgroundNormal.drawableHotspotChanged(x, y);
    200         }
    201     }
    202 
    203     @Override
    204     protected void drawableStateChanged() {
    205         super.drawableStateChanged();
    206         if (mDimmed) {
    207             mBackgroundDimmed.setState(getDrawableState());
    208         } else {
    209             mBackgroundNormal.setState(getDrawableState());
    210         }
    211     }
    212 
    213     private boolean handleTouchEventDimmed(MotionEvent event) {
    214         int action = event.getActionMasked();
    215         switch (action) {
    216             case MotionEvent.ACTION_DOWN:
    217                 mDownX = event.getX();
    218                 mDownY = event.getY();
    219                 if (mDownY > getActualHeight()) {
    220                     return false;
    221                 }
    222                 break;
    223             case MotionEvent.ACTION_MOVE:
    224                 if (!isWithinTouchSlop(event)) {
    225                     makeInactive(true /* animate */);
    226                     return false;
    227                 }
    228                 break;
    229             case MotionEvent.ACTION_UP:
    230                 if (isWithinTouchSlop(event)) {
    231                     if (!mActivated) {
    232                         makeActive();
    233                         postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS);
    234                     } else {
    235                         boolean performed = performClick();
    236                         if (performed) {
    237                             removeCallbacks(mTapTimeoutRunnable);
    238                         }
    239                     }
    240                 } else {
    241                     makeInactive(true /* animate */);
    242                 }
    243                 break;
    244             case MotionEvent.ACTION_CANCEL:
    245                 makeInactive(true /* animate */);
    246                 break;
    247             default:
    248                 break;
    249         }
    250         return true;
    251     }
    252 
    253     private void makeActive() {
    254         startActivateAnimation(false /* reverse */);
    255         mActivated = true;
    256         if (mOnActivatedListener != null) {
    257             mOnActivatedListener.onActivated(this);
    258         }
    259     }
    260 
    261     private void startActivateAnimation(boolean reverse) {
    262         if (!isAttachedToWindow()) {
    263             return;
    264         }
    265         int widthHalf = mBackgroundNormal.getWidth()/2;
    266         int heightHalf = mBackgroundNormal.getActualHeight()/2;
    267         float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf);
    268         Animator animator;
    269         if (reverse) {
    270             animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
    271                     widthHalf, heightHalf, radius, 0);
    272         } else {
    273             animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
    274                     widthHalf, heightHalf, 0, radius);
    275         }
    276         mBackgroundNormal.setVisibility(View.VISIBLE);
    277         Interpolator interpolator;
    278         Interpolator alphaInterpolator;
    279         if (!reverse) {
    280             interpolator = mLinearOutSlowInInterpolator;
    281             alphaInterpolator = mLinearOutSlowInInterpolator;
    282         } else {
    283             interpolator = ACTIVATE_INVERSE_INTERPOLATOR;
    284             alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR;
    285         }
    286         animator.setInterpolator(interpolator);
    287         animator.setDuration(ACTIVATE_ANIMATION_LENGTH);
    288         if (reverse) {
    289             mBackgroundNormal.setAlpha(1f);
    290             animator.addListener(new AnimatorListenerAdapter() {
    291                 @Override
    292                 public void onAnimationEnd(Animator animation) {
    293                     if (mDimmed) {
    294                         mBackgroundNormal.setVisibility(View.INVISIBLE);
    295                     }
    296                 }
    297             });
    298             animator.start();
    299         } else {
    300             mBackgroundNormal.setAlpha(0.4f);
    301             animator.start();
    302         }
    303         mBackgroundNormal.animate()
    304                 .alpha(reverse ? 0f : 1f)
    305                 .setInterpolator(alphaInterpolator)
    306                 .setDuration(ACTIVATE_ANIMATION_LENGTH);
    307     }
    308 
    309     /**
    310      * Cancels the hotspot and makes the notification inactive.
    311      */
    312     public void makeInactive(boolean animate) {
    313         if (mActivated) {
    314             if (mDimmed) {
    315                 if (animate) {
    316                     startActivateAnimation(true /* reverse */);
    317                 } else {
    318                     mBackgroundNormal.setVisibility(View.INVISIBLE);
    319                 }
    320             }
    321             mActivated = false;
    322         }
    323         if (mOnActivatedListener != null) {
    324             mOnActivatedListener.onActivationReset(this);
    325         }
    326         removeCallbacks(mTapTimeoutRunnable);
    327     }
    328 
    329     private boolean isWithinTouchSlop(MotionEvent event) {
    330         return Math.abs(event.getX() - mDownX) < mTouchSlop
    331                 && Math.abs(event.getY() - mDownY) < mTouchSlop;
    332     }
    333 
    334     public void setDimmed(boolean dimmed, boolean fade) {
    335         if (mDimmed != dimmed) {
    336             mDimmed = dimmed;
    337             if (fade) {
    338                 fadeDimmedBackground();
    339             } else {
    340                 updateBackground();
    341             }
    342         }
    343     }
    344 
    345     public void setDark(boolean dark, boolean fade, long delay) {
    346         super.setDark(dark, fade, delay);
    347         if (mDark == dark) {
    348             return;
    349         }
    350         mDark = dark;
    351         if (!dark && fade) {
    352             if (mActivated) {
    353                 mBackgroundDimmed.setVisibility(View.VISIBLE);
    354                 mBackgroundNormal.setVisibility(View.VISIBLE);
    355             } else if (mDimmed) {
    356                 mBackgroundDimmed.setVisibility(View.VISIBLE);
    357                 mBackgroundNormal.setVisibility(View.INVISIBLE);
    358             } else {
    359                 mBackgroundDimmed.setVisibility(View.INVISIBLE);
    360                 mBackgroundNormal.setVisibility(View.VISIBLE);
    361             }
    362             fadeInFromDark(delay);
    363         } else {
    364             updateBackground();
    365         }
    366      }
    367 
    368     public void setShowingLegacyBackground(boolean showing) {
    369         mShowingLegacyBackground = showing;
    370         updateBackgroundTint();
    371     }
    372 
    373     @Override
    374     public void setBelowSpeedBump(boolean below) {
    375         super.setBelowSpeedBump(below);
    376         if (below != mIsBelowSpeedBump) {
    377             mIsBelowSpeedBump = below;
    378             updateBackgroundTint();
    379         }
    380     }
    381 
    382     /**
    383      * Sets the tint color of the background
    384      */
    385     public void setTintColor(int color) {
    386         mBgTint = color;
    387         updateBackgroundTint();
    388     }
    389 
    390     private void updateBackgroundTint() {
    391         int color = getBackgroundColor();
    392         int rippleColor = getRippleColor();
    393         if (color == mNormalColor) {
    394             // We don't need to tint a normal notification
    395             color = 0;
    396         }
    397         mBackgroundDimmed.setTint(color);
    398         mBackgroundNormal.setTint(color);
    399         mBackgroundDimmed.setRippleColor(rippleColor);
    400         mBackgroundNormal.setRippleColor(rippleColor);
    401     }
    402 
    403     /**
    404      * Fades in the background when exiting dark mode.
    405      */
    406     private void fadeInFromDark(long delay) {
    407         final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal;
    408         background.setAlpha(0f);
    409         background.setPivotX(mBackgroundDimmed.getWidth() / 2f);
    410         background.setPivotY(getActualHeight() / 2f);
    411         background.setScaleX(DARK_EXIT_SCALE_START);
    412         background.setScaleY(DARK_EXIT_SCALE_START);
    413         background.animate()
    414                 .alpha(1f)
    415                 .scaleX(1f)
    416                 .scaleY(1f)
    417                 .setDuration(DARK_ANIMATION_LENGTH)
    418                 .setStartDelay(delay)
    419                 .setInterpolator(mLinearOutSlowInInterpolator)
    420                 .setListener(new AnimatorListenerAdapter() {
    421                     @Override
    422                     public void onAnimationCancel(Animator animation) {
    423                         // Jump state if we are cancelled
    424                         background.setScaleX(1f);
    425                         background.setScaleY(1f);
    426                         background.setAlpha(1f);
    427                     }
    428                 })
    429                 .start();
    430     }
    431 
    432     /**
    433      * Fades the background when the dimmed state changes.
    434      */
    435     private void fadeDimmedBackground() {
    436         mBackgroundDimmed.animate().cancel();
    437         mBackgroundNormal.animate().cancel();
    438         if (mDimmed) {
    439             mBackgroundDimmed.setVisibility(View.VISIBLE);
    440         } else {
    441             mBackgroundNormal.setVisibility(View.VISIBLE);
    442         }
    443         float startAlpha = mDimmed ? 1f : 0;
    444         float endAlpha = mDimmed ? 0 : 1f;
    445         int duration = BACKGROUND_ANIMATION_LENGTH_MS;
    446         // Check whether there is already a background animation running.
    447         if (mBackgroundAnimator != null) {
    448             startAlpha = (Float) mBackgroundAnimator.getAnimatedValue();
    449             duration = (int) mBackgroundAnimator.getCurrentPlayTime();
    450             mBackgroundAnimator.removeAllListeners();
    451             mBackgroundAnimator.cancel();
    452             if (duration <= 0) {
    453                 updateBackground();
    454                 return;
    455             }
    456         }
    457         mBackgroundNormal.setAlpha(startAlpha);
    458         mBackgroundAnimator =
    459                 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha);
    460         mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator);
    461         mBackgroundAnimator.setDuration(duration);
    462         mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
    463             @Override
    464             public void onAnimationEnd(Animator animation) {
    465                 if (mDimmed) {
    466                     mBackgroundNormal.setVisibility(View.INVISIBLE);
    467                 } else {
    468                     mBackgroundDimmed.setVisibility(View.INVISIBLE);
    469                 }
    470                 mBackgroundAnimator = null;
    471             }
    472         });
    473         mBackgroundAnimator.start();
    474     }
    475 
    476     private void updateBackground() {
    477         cancelFadeAnimations();
    478         if (mDark) {
    479             mBackgroundDimmed.setVisibility(View.INVISIBLE);
    480             mBackgroundNormal.setVisibility(View.INVISIBLE);
    481         } else if (mDimmed) {
    482             mBackgroundDimmed.setVisibility(View.VISIBLE);
    483             mBackgroundNormal.setVisibility(View.INVISIBLE);
    484         } else {
    485             mBackgroundDimmed.setVisibility(View.INVISIBLE);
    486             mBackgroundNormal.setVisibility(View.VISIBLE);
    487             mBackgroundNormal.setAlpha(1f);
    488             removeCallbacks(mTapTimeoutRunnable);
    489         }
    490     }
    491 
    492     private void cancelFadeAnimations() {
    493         if (mBackgroundAnimator != null) {
    494             mBackgroundAnimator.cancel();
    495         }
    496         mBackgroundDimmed.animate().cancel();
    497         mBackgroundNormal.animate().cancel();
    498     }
    499 
    500     @Override
    501     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    502         super.onLayout(changed, left, top, right, bottom);
    503         setPivotX(getWidth() / 2);
    504     }
    505 
    506     @Override
    507     public void setActualHeight(int actualHeight, boolean notifyListeners) {
    508         super.setActualHeight(actualHeight, notifyListeners);
    509         setPivotY(actualHeight / 2);
    510         mBackgroundNormal.setActualHeight(actualHeight);
    511         mBackgroundDimmed.setActualHeight(actualHeight);
    512     }
    513 
    514     @Override
    515     public void setClipTopAmount(int clipTopAmount) {
    516         super.setClipTopAmount(clipTopAmount);
    517         mBackgroundNormal.setClipTopAmount(clipTopAmount);
    518         mBackgroundDimmed.setClipTopAmount(clipTopAmount);
    519     }
    520 
    521     @Override
    522     public void performRemoveAnimation(long duration, float translationDirection,
    523             Runnable onFinishedRunnable) {
    524         enableAppearDrawing(true);
    525         if (mDrawingAppearAnimation) {
    526             startAppearAnimation(false /* isAppearing */, translationDirection,
    527                     0, duration, onFinishedRunnable);
    528         } else if (onFinishedRunnable != null) {
    529             onFinishedRunnable.run();
    530         }
    531     }
    532 
    533     @Override
    534     public void performAddAnimation(long delay, long duration) {
    535         enableAppearDrawing(true);
    536         if (mDrawingAppearAnimation) {
    537             startAppearAnimation(true /* isAppearing */, -1.0f, delay, duration, null);
    538         }
    539     }
    540 
    541     private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay,
    542             long duration, final Runnable onFinishedRunnable) {
    543         if (mAppearAnimator != null) {
    544             mAppearAnimator.cancel();
    545         }
    546         mAnimationTranslationY = translationDirection * getActualHeight();
    547         if (mAppearAnimationFraction == -1.0f) {
    548             // not initialized yet, we start anew
    549             if (isAppearing) {
    550                 mAppearAnimationFraction = 0.0f;
    551                 mAppearAnimationTranslation = mAnimationTranslationY;
    552             } else {
    553                 mAppearAnimationFraction = 1.0f;
    554                 mAppearAnimationTranslation = 0;
    555             }
    556         }
    557 
    558         float targetValue;
    559         if (isAppearing) {
    560             mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
    561             mCurrentAlphaInterpolator = mLinearOutSlowInInterpolator;
    562             targetValue = 1.0f;
    563         } else {
    564             mCurrentAppearInterpolator = mFastOutSlowInInterpolator;
    565             mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
    566             targetValue = 0.0f;
    567         }
    568         mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
    569                 targetValue);
    570         mAppearAnimator.setInterpolator(mLinearInterpolator);
    571         mAppearAnimator.setDuration(
    572                 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue)));
    573         mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    574             @Override
    575             public void onAnimationUpdate(ValueAnimator animation) {
    576                 mAppearAnimationFraction = (float) animation.getAnimatedValue();
    577                 updateAppearAnimationAlpha();
    578                 updateAppearRect();
    579                 invalidate();
    580             }
    581         });
    582         if (delay > 0) {
    583             // we need to apply the initial state already to avoid drawn frames in the wrong state
    584             updateAppearAnimationAlpha();
    585             updateAppearRect();
    586             mAppearAnimator.setStartDelay(delay);
    587         }
    588         mAppearAnimator.addListener(new AnimatorListenerAdapter() {
    589             private boolean mWasCancelled;
    590 
    591             @Override
    592             public void onAnimationEnd(Animator animation) {
    593                 if (onFinishedRunnable != null) {
    594                     onFinishedRunnable.run();
    595                 }
    596                 if (!mWasCancelled) {
    597                     mAppearAnimationFraction = -1;
    598                     setOutlineRect(null);
    599                     enableAppearDrawing(false);
    600                 }
    601             }
    602 
    603             @Override
    604             public void onAnimationStart(Animator animation) {
    605                 mWasCancelled = false;
    606             }
    607 
    608             @Override
    609             public void onAnimationCancel(Animator animation) {
    610                 mWasCancelled = true;
    611             }
    612         });
    613         mAppearAnimator.start();
    614     }
    615 
    616     private void updateAppearRect() {
    617         float inverseFraction = (1.0f - mAppearAnimationFraction);
    618         float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
    619         float translateYTotalAmount = translationFraction * mAnimationTranslationY;
    620         mAppearAnimationTranslation = translateYTotalAmount;
    621 
    622         // handle width animation
    623         float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
    624                 / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
    625         widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
    626         widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
    627         float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) *
    628                 widthFraction);
    629         float right = getWidth() - left;
    630 
    631         // handle top animation
    632         float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
    633                 VERTICAL_ANIMATION_START;
    634         heightFraction = Math.max(0.0f, heightFraction);
    635         heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
    636 
    637         float top;
    638         float bottom;
    639         final int actualHeight = getActualHeight();
    640         if (mAnimationTranslationY > 0.0f) {
    641             bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f
    642                     - translateYTotalAmount;
    643             top = bottom * heightFraction;
    644         } else {
    645             top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f -
    646                     translateYTotalAmount;
    647             bottom = actualHeight * (1 - heightFraction) + top * heightFraction;
    648         }
    649         mAppearAnimationRect.set(left, top, right, bottom);
    650         setOutlineRect(left, top + mAppearAnimationTranslation, right,
    651                 bottom + mAppearAnimationTranslation);
    652     }
    653 
    654     private void updateAppearAnimationAlpha() {
    655         int backgroundColor = getBackgroundColor();
    656         if (backgroundColor != -1) {
    657             float contentAlphaProgress = mAppearAnimationFraction;
    658             contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
    659             contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
    660             contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
    661             int sourceColor = Color.argb((int) (255 * (1.0f - contentAlphaProgress)),
    662                     Color.red(backgroundColor), Color.green(backgroundColor),
    663                     Color.blue(backgroundColor));
    664             mAppearAnimationFilter.setColor(sourceColor);
    665             mAppearPaint.setColorFilter(mAppearAnimationFilter);
    666         }
    667     }
    668 
    669     private int getBackgroundColor() {
    670         if (mBgTint != 0) {
    671             return mBgTint;
    672         } else if (mShowingLegacyBackground) {
    673             return mLegacyColor;
    674         } else if (mIsBelowSpeedBump) {
    675             return mLowPriorityColor;
    676         } else {
    677             return mNormalColor;
    678         }
    679     }
    680 
    681     private int getRippleColor() {
    682         if (mBgTint != 0) {
    683             return mTintedRippleColor;
    684         } else if (mShowingLegacyBackground) {
    685             return mTintedRippleColor;
    686         } else if (mIsBelowSpeedBump) {
    687             return mLowPriorityRippleColor;
    688         } else {
    689             return mNormalRippleColor;
    690         }
    691     }
    692 
    693     /**
    694      * When we draw the appear animation, we render the view in a bitmap and render this bitmap
    695      * as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
    696      * such that the normal drawing of the views does not happen anymore.
    697      *
    698      * @param enable Should it be enabled.
    699      */
    700     private void enableAppearDrawing(boolean enable) {
    701         if (enable != mDrawingAppearAnimation) {
    702             if (enable) {
    703                 if (getWidth() == 0 || getActualHeight() == 0) {
    704                     // TODO: This should not happen, but it can during expansion. Needs
    705                     // investigation
    706                     return;
    707                 }
    708                 Bitmap bitmap = Bitmap.createBitmap(getWidth(), getActualHeight(),
    709                         Bitmap.Config.ARGB_8888);
    710                 Canvas canvas = new Canvas(bitmap);
    711                 draw(canvas);
    712                 mAppearPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP,
    713                         Shader.TileMode.CLAMP));
    714             } else {
    715                 mAppearPaint.setShader(null);
    716             }
    717             mDrawingAppearAnimation = enable;
    718             invalidate();
    719         }
    720     }
    721 
    722     @Override
    723     protected void dispatchDraw(Canvas canvas) {
    724         if (!mDrawingAppearAnimation) {
    725             super.dispatchDraw(canvas);
    726         } else {
    727             drawAppearRect(canvas);
    728         }
    729     }
    730 
    731     private void drawAppearRect(Canvas canvas) {
    732         canvas.save();
    733         canvas.translate(0, mAppearAnimationTranslation);
    734         canvas.drawRoundRect(mAppearAnimationRect, mRoundedRectCornerRadius,
    735                 mRoundedRectCornerRadius, mAppearPaint);
    736         canvas.restore();
    737     }
    738 
    739     public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
    740         mOnActivatedListener = onActivatedListener;
    741     }
    742 
    743     public void reset() {
    744         setTintColor(0);
    745         setShowingLegacyBackground(false);
    746         setBelowSpeedBump(false);
    747     }
    748 
    749     public interface OnActivatedListener {
    750         void onActivated(ActivatableNotificationView view);
    751         void onActivationReset(ActivatableNotificationView view);
    752     }
    753 }
    754