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.TimeAnimator;
     23 import android.animation.ValueAnimator;
     24 import android.content.Context;
     25 import android.graphics.Canvas;
     26 import android.graphics.Color;
     27 import android.graphics.RectF;
     28 import android.util.AttributeSet;
     29 import android.view.MotionEvent;
     30 import android.view.View;
     31 import android.view.ViewAnimationUtils;
     32 import android.view.ViewConfiguration;
     33 import android.view.accessibility.AccessibilityManager;
     34 import android.view.animation.Interpolator;
     35 import android.view.animation.PathInterpolator;
     36 
     37 import com.android.systemui.Interpolators;
     38 import com.android.systemui.R;
     39 import com.android.systemui.classifier.FalsingManager;
     40 import com.android.systemui.statusbar.notification.FakeShadowView;
     41 import com.android.systemui.statusbar.notification.NotificationUtils;
     42 import com.android.systemui.statusbar.phone.DoubleTapHelper;
     43 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
     44 import com.android.systemui.statusbar.stack.StackStateAnimator;
     45 
     46 /**
     47  * Base class for both {@link ExpandableNotificationRow} and {@link NotificationShelf}
     48  * to implement dimming/activating on Keyguard for the double-tap gesture
     49  */
     50 public abstract class ActivatableNotificationView extends ExpandableOutlineView {
     51 
     52     private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
     53     private static final int ACTIVATE_ANIMATION_LENGTH = 220;
     54     private static final long DARK_ANIMATION_LENGTH = StackStateAnimator.ANIMATION_DURATION_WAKEUP;
     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     /**
     92      * A sentinel value when no color should be used. Can be used with {@link #setTintColor(int)}
     93      * or {@link #setOverrideTintColor(int, float)}.
     94      */
     95     protected static final int NO_COLOR = 0;
     96 
     97     private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
     98             = new PathInterpolator(0.6f, 0, 0.5f, 1);
     99     private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
    100             = new PathInterpolator(0, 0, 0.5f, 1);
    101     private final int mTintedRippleColor;
    102     private final int mLowPriorityRippleColor;
    103     protected final int mNormalRippleColor;
    104     private final AccessibilityManager mAccessibilityManager;
    105     private final DoubleTapHelper mDoubleTapHelper;
    106 
    107     private boolean mDimmed;
    108     private boolean mDark;
    109 
    110     protected int mBgTint = NO_COLOR;
    111     private float mBgAlpha = 1f;
    112 
    113     /**
    114      * Flag to indicate that the notification has been touched once and the second touch will
    115      * click it.
    116      */
    117     private boolean mActivated;
    118 
    119     private OnActivatedListener mOnActivatedListener;
    120 
    121     private final Interpolator mSlowOutFastInInterpolator;
    122     private final Interpolator mSlowOutLinearInInterpolator;
    123     private Interpolator mCurrentAppearInterpolator;
    124     private Interpolator mCurrentAlphaInterpolator;
    125 
    126     private NotificationBackgroundView mBackgroundNormal;
    127     private NotificationBackgroundView mBackgroundDimmed;
    128     private ObjectAnimator mBackgroundAnimator;
    129     private RectF mAppearAnimationRect = new RectF();
    130     private float mAnimationTranslationY;
    131     private boolean mDrawingAppearAnimation;
    132     private ValueAnimator mAppearAnimator;
    133     private ValueAnimator mBackgroundColorAnimator;
    134     private float mAppearAnimationFraction = -1.0f;
    135     private float mAppearAnimationTranslation;
    136     private final int mNormalColor;
    137     private final int mLowPriorityColor;
    138     private boolean mIsBelowSpeedBump;
    139     private FalsingManager mFalsingManager;
    140 
    141     private float mNormalBackgroundVisibilityAmount;
    142     private ValueAnimator mFadeInFromDarkAnimator;
    143     private float mDimmedBackgroundFadeInAmount = -1;
    144     private ValueAnimator.AnimatorUpdateListener mBackgroundVisibilityUpdater
    145             = new ValueAnimator.AnimatorUpdateListener() {
    146         @Override
    147         public void onAnimationUpdate(ValueAnimator animation) {
    148             setNormalBackgroundVisibilityAmount(mBackgroundNormal.getAlpha());
    149             mDimmedBackgroundFadeInAmount = mBackgroundDimmed.getAlpha();
    150         }
    151     };
    152     private AnimatorListenerAdapter mFadeInEndListener = new AnimatorListenerAdapter() {
    153         @Override
    154         public void onAnimationEnd(Animator animation) {
    155             super.onAnimationEnd(animation);
    156             mFadeInFromDarkAnimator = null;
    157             mDimmedBackgroundFadeInAmount = -1;
    158             updateBackground();
    159         }
    160     };
    161     private ValueAnimator.AnimatorUpdateListener mUpdateOutlineListener
    162             = new ValueAnimator.AnimatorUpdateListener() {
    163         @Override
    164         public void onAnimationUpdate(ValueAnimator animation) {
    165             updateOutlineAlpha();
    166         }
    167     };
    168     private float mShadowAlpha = 1.0f;
    169     private FakeShadowView mFakeShadow;
    170     private int mCurrentBackgroundTint;
    171     private int mTargetTint;
    172     private int mStartTint;
    173     private int mOverrideTint;
    174     private float mOverrideAmount;
    175     private boolean mShadowHidden;
    176     private boolean mWasActivatedOnDown;
    177     /**
    178      * Similar to mDimmed but is also true if it's not dimmable but should be
    179      */
    180     private boolean mNeedsDimming;
    181     private int mDimmedAlpha;
    182 
    183     public ActivatableNotificationView(Context context, AttributeSet attrs) {
    184         super(context, attrs);
    185         mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
    186         mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f);
    187         setClipChildren(false);
    188         setClipToPadding(false);
    189         mNormalColor = context.getColor(R.color.notification_material_background_color);
    190         mLowPriorityColor = context.getColor(
    191                 R.color.notification_material_background_low_priority_color);
    192         mTintedRippleColor = context.getColor(
    193                 R.color.notification_ripple_tinted_color);
    194         mLowPriorityRippleColor = context.getColor(
    195                 R.color.notification_ripple_color_low_priority);
    196         mNormalRippleColor = context.getColor(
    197                 R.color.notification_ripple_untinted_color);
    198         mFalsingManager = FalsingManager.getInstance(context);
    199         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
    200 
    201         mDoubleTapHelper = new DoubleTapHelper(this, (active) -> {
    202             if (active) {
    203                 makeActive();
    204             } else {
    205                 makeInactive(true /* animate */);
    206             }
    207         }, this::performClick, this::handleSlideBack, mFalsingManager::onNotificationDoubleTap);
    208     }
    209 
    210     @Override
    211     protected void onFinishInflate() {
    212         super.onFinishInflate();
    213         mBackgroundNormal = findViewById(R.id.backgroundNormal);
    214         mFakeShadow = findViewById(R.id.fake_shadow);
    215         mShadowHidden = mFakeShadow.getVisibility() != VISIBLE;
    216         mBackgroundDimmed = findViewById(R.id.backgroundDimmed);
    217         mDimmedAlpha = Color.alpha(mContext.getColor(
    218                 R.color.notification_material_background_dimmed_color));
    219         initBackground();
    220         updateBackground();
    221         updateBackgroundTint();
    222         updateOutlineAlpha();
    223     }
    224 
    225     /**
    226      * Sets the custom backgrounds on {@link #mBackgroundNormal} and {@link #mBackgroundDimmed}.
    227      * This method can also be used to reload the backgrounds on both of those views, which can
    228      * be useful in a configuration change.
    229      */
    230     protected void initBackground() {
    231         mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg);
    232         mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim);
    233     }
    234 
    235     private final Runnable mTapTimeoutRunnable = new Runnable() {
    236         @Override
    237         public void run() {
    238             makeInactive(true /* animate */);
    239         }
    240     };
    241 
    242     @Override
    243     public boolean onInterceptTouchEvent(MotionEvent ev) {
    244         if (mNeedsDimming && !mActivated && ev.getActionMasked() == MotionEvent.ACTION_DOWN
    245                 && disallowSingleClick(ev) && !isTouchExplorationEnabled()) {
    246             return true;
    247         }
    248         return super.onInterceptTouchEvent(ev);
    249     }
    250 
    251     private boolean isTouchExplorationEnabled() {
    252         return mAccessibilityManager.isTouchExplorationEnabled();
    253     }
    254 
    255     protected boolean disallowSingleClick(MotionEvent ev) {
    256         return false;
    257     }
    258 
    259     protected boolean handleSlideBack() {
    260         return false;
    261     }
    262 
    263     @Override
    264     public boolean onTouchEvent(MotionEvent event) {
    265         boolean result;
    266         if (event.getAction() == MotionEvent.ACTION_DOWN) {
    267             mWasActivatedOnDown = mActivated;
    268         }
    269         if ((mNeedsDimming && !mActivated) && !isTouchExplorationEnabled() && isInteractive()) {
    270             boolean wasActivated = mActivated;
    271             result = handleTouchEventDimmed(event);
    272             if (wasActivated && result && event.getAction() == MotionEvent.ACTION_UP) {
    273                 removeCallbacks(mTapTimeoutRunnable);
    274             }
    275         } else {
    276             result = super.onTouchEvent(event);
    277         }
    278         return result;
    279     }
    280 
    281     /**
    282      * @return whether this view is interactive and can be double tapped
    283      */
    284     protected boolean isInteractive() {
    285         return true;
    286     }
    287 
    288     @Override
    289     public void drawableHotspotChanged(float x, float y) {
    290         if (!mDimmed){
    291             mBackgroundNormal.drawableHotspotChanged(x, y);
    292         }
    293     }
    294 
    295     @Override
    296     protected void drawableStateChanged() {
    297         super.drawableStateChanged();
    298         if (mDimmed) {
    299             mBackgroundDimmed.setState(getDrawableState());
    300         } else {
    301             mBackgroundNormal.setState(getDrawableState());
    302         }
    303     }
    304 
    305     private boolean handleTouchEventDimmed(MotionEvent event) {
    306         if (mNeedsDimming && !mDimmed) {
    307             // We're actually dimmed, but our content isn't dimmable, let's ensure we have a ripple
    308             super.onTouchEvent(event);
    309         }
    310         return mDoubleTapHelper.onTouchEvent(event, getActualHeight());
    311     }
    312 
    313     @Override
    314     public boolean performClick() {
    315         if (mWasActivatedOnDown || !mNeedsDimming || isTouchExplorationEnabled()) {
    316             return super.performClick();
    317         }
    318         return false;
    319     }
    320 
    321     private void makeActive() {
    322         mFalsingManager.onNotificationActive();
    323         startActivateAnimation(false /* reverse */);
    324         mActivated = true;
    325         if (mOnActivatedListener != null) {
    326             mOnActivatedListener.onActivated(this);
    327         }
    328     }
    329 
    330     private void startActivateAnimation(final boolean reverse) {
    331         if (!isAttachedToWindow()) {
    332             return;
    333         }
    334         if (!isDimmable()) {
    335             return;
    336         }
    337         int widthHalf = mBackgroundNormal.getWidth()/2;
    338         int heightHalf = mBackgroundNormal.getActualHeight()/2;
    339         float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf);
    340         Animator animator;
    341         if (reverse) {
    342             animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
    343                     widthHalf, heightHalf, radius, 0);
    344         } else {
    345             animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
    346                     widthHalf, heightHalf, 0, radius);
    347         }
    348         mBackgroundNormal.setVisibility(View.VISIBLE);
    349         Interpolator interpolator;
    350         Interpolator alphaInterpolator;
    351         if (!reverse) {
    352             interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
    353             alphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
    354         } else {
    355             interpolator = ACTIVATE_INVERSE_INTERPOLATOR;
    356             alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR;
    357         }
    358         animator.setInterpolator(interpolator);
    359         animator.setDuration(ACTIVATE_ANIMATION_LENGTH);
    360         if (reverse) {
    361             mBackgroundNormal.setAlpha(1f);
    362             animator.addListener(new AnimatorListenerAdapter() {
    363                 @Override
    364                 public void onAnimationEnd(Animator animation) {
    365                     updateBackground();
    366                 }
    367             });
    368             animator.start();
    369         } else {
    370             mBackgroundNormal.setAlpha(0.4f);
    371             animator.start();
    372         }
    373         mBackgroundNormal.animate()
    374                 .alpha(reverse ? 0f : 1f)
    375                 .setInterpolator(alphaInterpolator)
    376                 .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    377                     @Override
    378                     public void onAnimationUpdate(ValueAnimator animation) {
    379                         float animatedFraction = animation.getAnimatedFraction();
    380                         if (reverse) {
    381                             animatedFraction = 1.0f - animatedFraction;
    382                         }
    383                         setNormalBackgroundVisibilityAmount(animatedFraction);
    384                     }
    385                 })
    386                 .setDuration(ACTIVATE_ANIMATION_LENGTH);
    387     }
    388 
    389     /**
    390      * Cancels the hotspot and makes the notification inactive.
    391      */
    392     public void makeInactive(boolean animate) {
    393         if (mActivated) {
    394             mActivated = false;
    395             if (mDimmed) {
    396                 if (animate) {
    397                     startActivateAnimation(true /* reverse */);
    398                 } else {
    399                     updateBackground();
    400                 }
    401             }
    402         }
    403         if (mOnActivatedListener != null) {
    404             mOnActivatedListener.onActivationReset(this);
    405         }
    406         removeCallbacks(mTapTimeoutRunnable);
    407     }
    408 
    409     public void setDimmed(boolean dimmed, boolean fade) {
    410         mNeedsDimming = dimmed;
    411         dimmed &= isDimmable();
    412         if (mDimmed != dimmed) {
    413             mDimmed = dimmed;
    414             resetBackgroundAlpha();
    415             if (fade) {
    416                 fadeDimmedBackground();
    417             } else {
    418                 updateBackground();
    419             }
    420         }
    421     }
    422 
    423     public boolean isDimmable() {
    424         return true;
    425     }
    426 
    427     public void setDark(boolean dark, boolean fade, long delay) {
    428         super.setDark(dark, fade, delay);
    429         if (mDark == dark) {
    430             return;
    431         }
    432         mDark = dark;
    433         updateBackground();
    434         updateBackgroundTint(false);
    435         if (!dark && fade && !shouldHideBackground()) {
    436             fadeInFromDark(delay);
    437         }
    438         updateOutlineAlpha();
    439     }
    440 
    441     private void updateOutlineAlpha() {
    442         if (mDark) {
    443             setOutlineAlpha(0f);
    444             return;
    445         }
    446         float alpha = NotificationStackScrollLayout.BACKGROUND_ALPHA_DIMMED;
    447         alpha = (alpha + (1.0f - alpha) * mNormalBackgroundVisibilityAmount);
    448         alpha *= mShadowAlpha;
    449         if (mFadeInFromDarkAnimator != null) {
    450             alpha *= mFadeInFromDarkAnimator.getAnimatedFraction();
    451         }
    452         setOutlineAlpha(alpha);
    453     }
    454 
    455     public void setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount) {
    456         mNormalBackgroundVisibilityAmount = normalBackgroundVisibilityAmount;
    457         updateOutlineAlpha();
    458     }
    459 
    460     @Override
    461     public void setBelowSpeedBump(boolean below) {
    462         super.setBelowSpeedBump(below);
    463         if (below != mIsBelowSpeedBump) {
    464             mIsBelowSpeedBump = below;
    465             updateBackgroundTint();
    466             onBelowSpeedBumpChanged();
    467         }
    468     }
    469 
    470     protected void onBelowSpeedBumpChanged() {
    471     }
    472 
    473     /**
    474      * @return whether we are below the speed bump
    475      */
    476     public boolean isBelowSpeedBump() {
    477         return mIsBelowSpeedBump;
    478     }
    479 
    480     /**
    481      * Sets the tint color of the background
    482      */
    483     public void setTintColor(int color) {
    484         setTintColor(color, false);
    485     }
    486 
    487     /**
    488      * Sets the tint color of the background
    489      */
    490     public void setTintColor(int color, boolean animated) {
    491         if (color != mBgTint) {
    492             mBgTint = color;
    493             updateBackgroundTint(animated);
    494         }
    495     }
    496 
    497     /**
    498      * Set an override tint color that is used for the background.
    499      *
    500      * @param color the color that should be used to tint the background.
    501      *              This can be {@link #NO_COLOR} if the tint should be normally computed.
    502      * @param overrideAmount a value from 0 to 1 how much the override tint should be used. The
    503      *                       background color will then be the interpolation between this and the
    504      *                       regular background color, where 1 means the overrideTintColor is fully
    505      *                       used and the background color not at all.
    506      */
    507     public void setOverrideTintColor(int color, float overrideAmount) {
    508         if (mDark) {
    509             color = NO_COLOR;
    510             overrideAmount = 0;
    511         }
    512         mOverrideTint = color;
    513         mOverrideAmount = overrideAmount;
    514         int newColor = calculateBgColor();
    515         setBackgroundTintColor(newColor);
    516         if (!isDimmable() && mNeedsDimming) {
    517            mBackgroundNormal.setDrawableAlpha((int) NotificationUtils.interpolate(255,
    518                    mDimmedAlpha,
    519                    overrideAmount));
    520         } else {
    521             mBackgroundNormal.setDrawableAlpha(255);
    522         }
    523     }
    524 
    525     protected void updateBackgroundTint() {
    526         updateBackgroundTint(false /* animated */);
    527     }
    528 
    529     private void updateBackgroundTint(boolean animated) {
    530         if (mBackgroundColorAnimator != null) {
    531             mBackgroundColorAnimator.cancel();
    532         }
    533         int rippleColor = getRippleColor();
    534         mBackgroundDimmed.setRippleColor(rippleColor);
    535         mBackgroundNormal.setRippleColor(rippleColor);
    536         int color = calculateBgColor();
    537         if (!animated) {
    538             setBackgroundTintColor(color);
    539         } else if (color != mCurrentBackgroundTint) {
    540             mStartTint = mCurrentBackgroundTint;
    541             mTargetTint = color;
    542             mBackgroundColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
    543             mBackgroundColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    544                 @Override
    545                 public void onAnimationUpdate(ValueAnimator animation) {
    546                     int newColor = NotificationUtils.interpolateColors(mStartTint, mTargetTint,
    547                             animation.getAnimatedFraction());
    548                     setBackgroundTintColor(newColor);
    549                 }
    550             });
    551             mBackgroundColorAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
    552             mBackgroundColorAnimator.setInterpolator(Interpolators.LINEAR);
    553             mBackgroundColorAnimator.addListener(new AnimatorListenerAdapter() {
    554                 @Override
    555                 public void onAnimationEnd(Animator animation) {
    556                     mBackgroundColorAnimator = null;
    557                 }
    558             });
    559             mBackgroundColorAnimator.start();
    560         }
    561     }
    562 
    563     private void setBackgroundTintColor(int color) {
    564         if (color != mCurrentBackgroundTint) {
    565             mCurrentBackgroundTint = color;
    566             if (color == mNormalColor) {
    567                 // We don't need to tint a normal notification
    568                 color = 0;
    569             }
    570             mBackgroundDimmed.setTint(color);
    571             mBackgroundNormal.setTint(color);
    572         }
    573     }
    574 
    575     /**
    576      * Fades in the background when exiting dark mode.
    577      */
    578     private void fadeInFromDark(long delay) {
    579         final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal;
    580         background.setAlpha(0f);
    581         mBackgroundVisibilityUpdater.onAnimationUpdate(null);
    582         background.animate()
    583                 .alpha(1f)
    584                 .setDuration(DARK_ANIMATION_LENGTH)
    585                 .setStartDelay(delay)
    586                 .setInterpolator(Interpolators.ALPHA_IN)
    587                 .setListener(new AnimatorListenerAdapter() {
    588                     @Override
    589                     public void onAnimationCancel(Animator animation) {
    590                         // Jump state if we are cancelled
    591                         background.setAlpha(1f);
    592                     }
    593                 })
    594                 .setUpdateListener(mBackgroundVisibilityUpdater)
    595                 .start();
    596         mFadeInFromDarkAnimator = TimeAnimator.ofFloat(0.0f, 1.0f);
    597         mFadeInFromDarkAnimator.setDuration(DARK_ANIMATION_LENGTH);
    598         mFadeInFromDarkAnimator.setStartDelay(delay);
    599         mFadeInFromDarkAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
    600         mFadeInFromDarkAnimator.addListener(mFadeInEndListener);
    601         mFadeInFromDarkAnimator.addUpdateListener(mUpdateOutlineListener);
    602         mFadeInFromDarkAnimator.start();
    603     }
    604 
    605     /**
    606      * Fades the background when the dimmed state changes.
    607      */
    608     private void fadeDimmedBackground() {
    609         mBackgroundDimmed.animate().cancel();
    610         mBackgroundNormal.animate().cancel();
    611         if (mActivated) {
    612             updateBackground();
    613             return;
    614         }
    615         if (!shouldHideBackground()) {
    616             if (mDimmed) {
    617                 mBackgroundDimmed.setVisibility(View.VISIBLE);
    618             } else {
    619                 mBackgroundNormal.setVisibility(View.VISIBLE);
    620             }
    621         }
    622         float startAlpha = mDimmed ? 1f : 0;
    623         float endAlpha = mDimmed ? 0 : 1f;
    624         int duration = BACKGROUND_ANIMATION_LENGTH_MS;
    625         // Check whether there is already a background animation running.
    626         if (mBackgroundAnimator != null) {
    627             startAlpha = (Float) mBackgroundAnimator.getAnimatedValue();
    628             duration = (int) mBackgroundAnimator.getCurrentPlayTime();
    629             mBackgroundAnimator.removeAllListeners();
    630             mBackgroundAnimator.cancel();
    631             if (duration <= 0) {
    632                 updateBackground();
    633                 return;
    634             }
    635         }
    636         mBackgroundNormal.setAlpha(startAlpha);
    637         mBackgroundAnimator =
    638                 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha);
    639         mBackgroundAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    640         mBackgroundAnimator.setDuration(duration);
    641         mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
    642             @Override
    643             public void onAnimationEnd(Animator animation) {
    644                 updateBackground();
    645                 mBackgroundAnimator = null;
    646                 if (mFadeInFromDarkAnimator == null) {
    647                     mDimmedBackgroundFadeInAmount = -1;
    648                 }
    649             }
    650         });
    651         mBackgroundAnimator.addUpdateListener(mBackgroundVisibilityUpdater);
    652         mBackgroundAnimator.start();
    653     }
    654 
    655     protected void updateBackgroundAlpha(float transformationAmount) {
    656         mBgAlpha =  isChildInGroup() && mDimmed ? transformationAmount : 1f;
    657         if (mDimmedBackgroundFadeInAmount != -1) {
    658             mBgAlpha *= mDimmedBackgroundFadeInAmount;
    659         }
    660         mBackgroundDimmed.setAlpha(mBgAlpha);
    661     }
    662 
    663     protected void resetBackgroundAlpha() {
    664         updateBackgroundAlpha(0f /* transformationAmount */);
    665     }
    666 
    667     protected void updateBackground() {
    668         cancelFadeAnimations();
    669         if (shouldHideBackground()) {
    670             mBackgroundDimmed.setVisibility(INVISIBLE);
    671             mBackgroundNormal.setVisibility(mActivated ? VISIBLE : INVISIBLE);
    672         } else if (mDimmed) {
    673             // When groups are animating to the expanded state from the lockscreen, show the
    674             // normal background instead of the dimmed background
    675             final boolean dontShowDimmed = isGroupExpansionChanging() && isChildInGroup();
    676             mBackgroundDimmed.setVisibility(dontShowDimmed ? View.INVISIBLE : View.VISIBLE);
    677             mBackgroundNormal.setVisibility((mActivated || dontShowDimmed)
    678                     ? View.VISIBLE
    679                     : View.INVISIBLE);
    680         } else {
    681             mBackgroundDimmed.setVisibility(View.INVISIBLE);
    682             mBackgroundNormal.setVisibility(View.VISIBLE);
    683             mBackgroundNormal.setAlpha(1f);
    684             removeCallbacks(mTapTimeoutRunnable);
    685             // make in inactive to avoid it sticking around active
    686             makeInactive(false /* animate */);
    687         }
    688         setNormalBackgroundVisibilityAmount(
    689                 mBackgroundNormal.getVisibility() == View.VISIBLE ? 1.0f : 0.0f);
    690     }
    691 
    692     protected boolean shouldHideBackground() {
    693         return mDark;
    694     }
    695 
    696     private void cancelFadeAnimations() {
    697         if (mBackgroundAnimator != null) {
    698             mBackgroundAnimator.cancel();
    699         }
    700         mBackgroundDimmed.animate().cancel();
    701         mBackgroundNormal.animate().cancel();
    702     }
    703 
    704     @Override
    705     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    706         super.onLayout(changed, left, top, right, bottom);
    707         setPivotX(getWidth() / 2);
    708     }
    709 
    710     @Override
    711     public void setActualHeight(int actualHeight, boolean notifyListeners) {
    712         super.setActualHeight(actualHeight, notifyListeners);
    713         setPivotY(actualHeight / 2);
    714         mBackgroundNormal.setActualHeight(actualHeight);
    715         mBackgroundDimmed.setActualHeight(actualHeight);
    716     }
    717 
    718     @Override
    719     public void setClipTopAmount(int clipTopAmount) {
    720         super.setClipTopAmount(clipTopAmount);
    721         mBackgroundNormal.setClipTopAmount(clipTopAmount);
    722         mBackgroundDimmed.setClipTopAmount(clipTopAmount);
    723     }
    724 
    725     @Override
    726     public void setClipBottomAmount(int clipBottomAmount) {
    727         super.setClipBottomAmount(clipBottomAmount);
    728         mBackgroundNormal.setClipBottomAmount(clipBottomAmount);
    729         mBackgroundDimmed.setClipBottomAmount(clipBottomAmount);
    730     }
    731 
    732     @Override
    733     public void performRemoveAnimation(long duration, float translationDirection,
    734             Runnable onFinishedRunnable) {
    735         enableAppearDrawing(true);
    736         if (mDrawingAppearAnimation) {
    737             startAppearAnimation(false /* isAppearing */, translationDirection,
    738                     0, duration, onFinishedRunnable);
    739         } else if (onFinishedRunnable != null) {
    740             onFinishedRunnable.run();
    741         }
    742     }
    743 
    744     @Override
    745     public void performAddAnimation(long delay, long duration) {
    746         enableAppearDrawing(true);
    747         if (mDrawingAppearAnimation) {
    748             startAppearAnimation(true /* isAppearing */, -1.0f, delay, duration, null);
    749         }
    750     }
    751 
    752     private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay,
    753             long duration, final Runnable onFinishedRunnable) {
    754         cancelAppearAnimation();
    755         mAnimationTranslationY = translationDirection * getActualHeight();
    756         if (mAppearAnimationFraction == -1.0f) {
    757             // not initialized yet, we start anew
    758             if (isAppearing) {
    759                 mAppearAnimationFraction = 0.0f;
    760                 mAppearAnimationTranslation = mAnimationTranslationY;
    761             } else {
    762                 mAppearAnimationFraction = 1.0f;
    763                 mAppearAnimationTranslation = 0;
    764             }
    765         }
    766 
    767         float targetValue;
    768         if (isAppearing) {
    769             mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
    770             mCurrentAlphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
    771             targetValue = 1.0f;
    772         } else {
    773             mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN;
    774             mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
    775             targetValue = 0.0f;
    776         }
    777         mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
    778                 targetValue);
    779         mAppearAnimator.setInterpolator(Interpolators.LINEAR);
    780         mAppearAnimator.setDuration(
    781                 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue)));
    782         mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    783             @Override
    784             public void onAnimationUpdate(ValueAnimator animation) {
    785                 mAppearAnimationFraction = (float) animation.getAnimatedValue();
    786                 updateAppearAnimationAlpha();
    787                 updateAppearRect();
    788                 invalidate();
    789             }
    790         });
    791         if (delay > 0) {
    792             // we need to apply the initial state already to avoid drawn frames in the wrong state
    793             updateAppearAnimationAlpha();
    794             updateAppearRect();
    795             mAppearAnimator.setStartDelay(delay);
    796         }
    797         mAppearAnimator.addListener(new AnimatorListenerAdapter() {
    798             private boolean mWasCancelled;
    799 
    800             @Override
    801             public void onAnimationEnd(Animator animation) {
    802                 if (onFinishedRunnable != null) {
    803                     onFinishedRunnable.run();
    804                 }
    805                 if (!mWasCancelled) {
    806                     enableAppearDrawing(false);
    807                     onAppearAnimationFinished(isAppearing);
    808                 }
    809             }
    810 
    811             @Override
    812             public void onAnimationStart(Animator animation) {
    813                 mWasCancelled = false;
    814             }
    815 
    816             @Override
    817             public void onAnimationCancel(Animator animation) {
    818                 mWasCancelled = true;
    819             }
    820         });
    821         mAppearAnimator.start();
    822     }
    823 
    824     protected void onAppearAnimationFinished(boolean wasAppearing) {
    825     }
    826 
    827     private void cancelAppearAnimation() {
    828         if (mAppearAnimator != null) {
    829             mAppearAnimator.cancel();
    830             mAppearAnimator = null;
    831         }
    832     }
    833 
    834     public void cancelAppearDrawing() {
    835         cancelAppearAnimation();
    836         enableAppearDrawing(false);
    837     }
    838 
    839     private void updateAppearRect() {
    840         float inverseFraction = (1.0f - mAppearAnimationFraction);
    841         float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
    842         float translateYTotalAmount = translationFraction * mAnimationTranslationY;
    843         mAppearAnimationTranslation = translateYTotalAmount;
    844 
    845         // handle width animation
    846         float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
    847                 / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
    848         widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
    849         widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
    850         float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) *
    851                 widthFraction);
    852         float right = getWidth() - left;
    853 
    854         // handle top animation
    855         float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
    856                 VERTICAL_ANIMATION_START;
    857         heightFraction = Math.max(0.0f, heightFraction);
    858         heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
    859 
    860         float top;
    861         float bottom;
    862         final int actualHeight = getActualHeight();
    863         if (mAnimationTranslationY > 0.0f) {
    864             bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f
    865                     - translateYTotalAmount;
    866             top = bottom * heightFraction;
    867         } else {
    868             top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f -
    869                     translateYTotalAmount;
    870             bottom = actualHeight * (1 - heightFraction) + top * heightFraction;
    871         }
    872         mAppearAnimationRect.set(left, top, right, bottom);
    873         setOutlineRect(left, top + mAppearAnimationTranslation, right,
    874                 bottom + mAppearAnimationTranslation);
    875     }
    876 
    877     private void updateAppearAnimationAlpha() {
    878         float contentAlphaProgress = mAppearAnimationFraction;
    879         contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
    880         contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
    881         contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
    882         setContentAlpha(contentAlphaProgress);
    883     }
    884 
    885     private void setContentAlpha(float contentAlpha) {
    886         View contentView = getContentView();
    887         if (contentView.hasOverlappingRendering()) {
    888             int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE
    889                     : LAYER_TYPE_HARDWARE;
    890             int currentLayerType = contentView.getLayerType();
    891             if (currentLayerType != layerType) {
    892                 contentView.setLayerType(layerType, null);
    893             }
    894         }
    895         contentView.setAlpha(contentAlpha);
    896     }
    897 
    898     protected abstract View getContentView();
    899 
    900     public int calculateBgColor() {
    901         return calculateBgColor(true /* withTint */, true /* withOverRide */);
    902     }
    903 
    904     /**
    905      * @param withTint should a possible tint be factored in?
    906      * @param withOverRide should the value be interpolated with {@link #mOverrideTint}
    907      * @return the calculated background color
    908      */
    909     private int calculateBgColor(boolean withTint, boolean withOverRide) {
    910         if (withTint && mDark) {
    911             return getContext().getColor(R.color.notification_material_background_dark_color);
    912         }
    913         if (withOverRide && mOverrideTint != NO_COLOR) {
    914             int defaultTint = calculateBgColor(withTint, false);
    915             return NotificationUtils.interpolateColors(defaultTint, mOverrideTint, mOverrideAmount);
    916         }
    917         if (withTint && mBgTint != NO_COLOR) {
    918             return mBgTint;
    919         } else if (mIsBelowSpeedBump) {
    920             return mLowPriorityColor;
    921         } else {
    922             return mNormalColor;
    923         }
    924     }
    925 
    926     protected int getRippleColor() {
    927         if (mBgTint != 0) {
    928             return mTintedRippleColor;
    929         } else if (mIsBelowSpeedBump) {
    930             return mLowPriorityRippleColor;
    931         } else {
    932             return mNormalRippleColor;
    933         }
    934     }
    935 
    936     /**
    937      * When we draw the appear animation, we render the view in a bitmap and render this bitmap
    938      * as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
    939      * such that the normal drawing of the views does not happen anymore.
    940      *
    941      * @param enable Should it be enabled.
    942      */
    943     private void enableAppearDrawing(boolean enable) {
    944         if (enable != mDrawingAppearAnimation) {
    945             mDrawingAppearAnimation = enable;
    946             if (!enable) {
    947                 setContentAlpha(1.0f);
    948                 mAppearAnimationFraction = -1;
    949                 setOutlineRect(null);
    950             }
    951             invalidate();
    952         }
    953     }
    954 
    955     public boolean isDrawingAppearAnimation() {
    956         return mDrawingAppearAnimation;
    957     }
    958 
    959     @Override
    960     protected void dispatchDraw(Canvas canvas) {
    961         if (mDrawingAppearAnimation) {
    962             canvas.save();
    963             canvas.translate(0, mAppearAnimationTranslation);
    964         }
    965         super.dispatchDraw(canvas);
    966         if (mDrawingAppearAnimation) {
    967             canvas.restore();
    968         }
    969     }
    970 
    971     public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
    972         mOnActivatedListener = onActivatedListener;
    973     }
    974 
    975     public boolean hasSameBgColor(ActivatableNotificationView otherView) {
    976         return calculateBgColor() == otherView.calculateBgColor();
    977     }
    978 
    979     @Override
    980     public float getShadowAlpha() {
    981         return mShadowAlpha;
    982     }
    983 
    984     @Override
    985     public void setShadowAlpha(float shadowAlpha) {
    986         if (shadowAlpha != mShadowAlpha) {
    987             mShadowAlpha = shadowAlpha;
    988             updateOutlineAlpha();
    989         }
    990     }
    991 
    992     @Override
    993     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
    994             int outlineTranslation) {
    995         boolean hiddenBefore = mShadowHidden;
    996         mShadowHidden = shadowIntensity == 0.0f;
    997         if (!mShadowHidden || !hiddenBefore) {
    998             mFakeShadow.setFakeShadowTranslationZ(shadowIntensity * (getTranslationZ()
    999                             + FakeShadowView.SHADOW_SIBLING_TRESHOLD), outlineAlpha, shadowYEnd,
   1000                     outlineTranslation);
   1001         }
   1002     }
   1003 
   1004     public int getBackgroundColorWithoutTint() {
   1005         return calculateBgColor(false /* withTint */, false /* withOverride */);
   1006     }
   1007 
   1008     public interface OnActivatedListener {
   1009         void onActivated(ActivatableNotificationView view);
   1010         void onActivationReset(ActivatableNotificationView view);
   1011     }
   1012 }
   1013