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