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