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