Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2016 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 static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN_REVERSE;
     20 import static com.android.systemui.statusbar.phone.NotificationIconContainer.IconState.NO_VALUE;
     21 
     22 import android.content.Context;
     23 import android.content.res.Configuration;
     24 import android.content.res.Resources;
     25 import android.graphics.Rect;
     26 import android.os.SystemProperties;
     27 import android.util.AttributeSet;
     28 import android.util.Log;
     29 import android.view.DisplayCutout;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 import android.view.ViewTreeObserver;
     33 import android.view.WindowInsets;
     34 import android.view.accessibility.AccessibilityNodeInfo;
     35 
     36 import com.android.internal.annotations.VisibleForTesting;
     37 import com.android.systemui.Dependency;
     38 import com.android.systemui.Interpolators;
     39 import com.android.systemui.R;
     40 import com.android.systemui.plugins.statusbar.StatusBarStateController;
     41 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
     42 import com.android.systemui.statusbar.notification.NotificationUtils;
     43 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
     44 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
     45 import com.android.systemui.statusbar.notification.row.ExpandableView;
     46 import com.android.systemui.statusbar.notification.stack.AmbientState;
     47 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
     48 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
     49 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
     50 import com.android.systemui.statusbar.notification.stack.ViewState;
     51 import com.android.systemui.statusbar.phone.NotificationIconContainer;
     52 
     53 /**
     54  * A notification shelf view that is placed inside the notification scroller. It manages the
     55  * overflow icons that don't fit into the regular list anymore.
     56  */
     57 public class NotificationShelf extends ActivatableNotificationView implements
     58         View.OnLayoutChangeListener, StateListener {
     59 
     60     private static final boolean USE_ANIMATIONS_WHEN_OPENING =
     61             SystemProperties.getBoolean("debug.icon_opening_animations", true);
     62     private static final boolean ICON_ANMATIONS_WHILE_SCROLLING
     63             = SystemProperties.getBoolean("debug.icon_scroll_animations", true);
     64     private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag;
     65     private static final String TAG = "NotificationShelf";
     66     private static final long SHELF_IN_TRANSLATION_DURATION = 200;
     67 
     68     private NotificationIconContainer mShelfIcons;
     69     private int[] mTmp = new int[2];
     70     private boolean mHideBackground;
     71     private int mIconAppearTopPadding;
     72     private int mShelfAppearTranslation;
     73     private float mDarkShelfPadding;
     74     private float mDarkShelfIconSize;
     75     private int mStatusBarHeight;
     76     private int mStatusBarPaddingStart;
     77     private AmbientState mAmbientState;
     78     private NotificationStackScrollLayout mHostLayout;
     79     private int mMaxLayoutHeight;
     80     private int mPaddingBetweenElements;
     81     private int mNotGoneIndex;
     82     private boolean mHasItemsInStableShelf;
     83     private NotificationIconContainer mCollapsedIcons;
     84     private int mScrollFastThreshold;
     85     private int mIconSize;
     86     private int mStatusBarState;
     87     private float mMaxShelfEnd;
     88     private int mRelativeOffset;
     89     private boolean mInteractive;
     90     private float mOpenedAmount;
     91     private boolean mNoAnimationsInThisFrame;
     92     private boolean mAnimationsEnabled = true;
     93     private boolean mShowNotificationShelf;
     94     private float mFirstElementRoundness;
     95     private Rect mClipRect = new Rect();
     96     private int mCutoutHeight;
     97     private int mGapHeight;
     98 
     99     public NotificationShelf(Context context, AttributeSet attrs) {
    100         super(context, attrs);
    101     }
    102 
    103     @Override
    104     @VisibleForTesting
    105     public void onFinishInflate() {
    106         super.onFinishInflate();
    107         mShelfIcons = findViewById(R.id.content);
    108         mShelfIcons.setClipChildren(false);
    109         mShelfIcons.setClipToPadding(false);
    110 
    111         setClipToActualHeight(false);
    112         setClipChildren(false);
    113         setClipToPadding(false);
    114         mShelfIcons.setIsStaticLayout(false);
    115         setBottomRoundness(1.0f, false /* animate */);
    116         initDimens();
    117     }
    118 
    119     @Override
    120     protected void onAttachedToWindow() {
    121         super.onAttachedToWindow();
    122         ((SysuiStatusBarStateController) Dependency.get(StatusBarStateController.class))
    123                 .addCallback(this, SysuiStatusBarStateController.RANK_SHELF);
    124     }
    125 
    126     @Override
    127     protected void onDetachedFromWindow() {
    128         super.onDetachedFromWindow();
    129         Dependency.get(StatusBarStateController.class).removeCallback(this);
    130     }
    131 
    132     public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout) {
    133         mAmbientState = ambientState;
    134         mHostLayout = hostLayout;
    135     }
    136 
    137     private void initDimens() {
    138         Resources res = getResources();
    139         mIconAppearTopPadding = res.getDimensionPixelSize(R.dimen.notification_icon_appear_padding);
    140         mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height);
    141         mStatusBarPaddingStart = res.getDimensionPixelOffset(R.dimen.status_bar_padding_start);
    142         mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
    143         mShelfAppearTranslation = res.getDimensionPixelSize(R.dimen.shelf_appear_translation);
    144         mDarkShelfPadding = res.getDimensionPixelSize(R.dimen.widget_bottom_separator_padding);
    145 
    146         ViewGroup.LayoutParams layoutParams = getLayoutParams();
    147         layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
    148         setLayoutParams(layoutParams);
    149 
    150         int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding);
    151         mShelfIcons.setPadding(padding, 0, padding, 0);
    152         mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold);
    153         mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf);
    154         mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
    155         mDarkShelfIconSize = res.getDimensionPixelOffset(R.dimen.dark_shelf_icon_size);
    156         mGapHeight = res.getDimensionPixelSize(R.dimen.qs_notification_padding);
    157 
    158         if (!mShowNotificationShelf) {
    159             setVisibility(GONE);
    160         }
    161     }
    162 
    163     @Override
    164     protected void onConfigurationChanged(Configuration newConfig) {
    165         super.onConfigurationChanged(newConfig);
    166         initDimens();
    167     }
    168 
    169     @Override
    170     public void setDark(boolean dark, boolean fade, long delay) {
    171         if (mDark == dark) return;
    172         super.setDark(dark, fade, delay);
    173         mShelfIcons.setDark(dark, fade, delay);
    174         updateInteractiveness();
    175         updateOutline();
    176     }
    177 
    178     /**
    179      * Alpha animation with translation played when this view is visible on AOD.
    180      */
    181     public void fadeInTranslating() {
    182         mShelfIcons.setTranslationY(-mShelfAppearTranslation);
    183         mShelfIcons.setAlpha(0);
    184         mShelfIcons.animate()
    185                 .setInterpolator(Interpolators.DECELERATE_QUINT)
    186                 .translationY(0)
    187                 .setDuration(SHELF_IN_TRANSLATION_DURATION)
    188                 .start();
    189         mShelfIcons.animate()
    190                 .alpha(1)
    191                 .setInterpolator(Interpolators.LINEAR)
    192                 .setDuration(SHELF_IN_TRANSLATION_DURATION)
    193                 .start();
    194     }
    195 
    196     @Override
    197     protected View getContentView() {
    198         return mShelfIcons;
    199     }
    200 
    201     public NotificationIconContainer getShelfIcons() {
    202         return mShelfIcons;
    203     }
    204 
    205     @Override
    206     public ExpandableViewState createExpandableViewState() {
    207         return new ShelfState();
    208     }
    209 
    210     /** Update the state of the shelf. */
    211     public void updateState(AmbientState ambientState) {
    212         ExpandableView lastView = ambientState.getLastVisibleBackgroundChild();
    213         ShelfState viewState = (ShelfState) getViewState();
    214         if (mShowNotificationShelf && lastView != null) {
    215             float maxShelfEnd = ambientState.getInnerHeight() + ambientState.getTopPadding()
    216                     + ambientState.getStackTranslation();
    217             ExpandableViewState lastViewState = lastView.getViewState();
    218             float viewEnd = lastViewState.yTranslation + lastViewState.height;
    219             viewState.copyFrom(lastViewState);
    220             viewState.height = getIntrinsicHeight();
    221 
    222             float awakenTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - viewState.height,
    223                     getFullyClosedTranslation());
    224             float yRatio = mAmbientState.hasPulsingNotifications() ?
    225                     0 : mAmbientState.getDarkAmount();
    226             viewState.yTranslation = awakenTranslation + mDarkShelfPadding * yRatio;
    227             viewState.zTranslation = ambientState.getBaseZHeight();
    228             // For the small display size, it's not enough to make the icon not covered by
    229             // the top cutout so the denominator add the height of cutout.
    230             // Totally, (getIntrinsicHeight() * 2 + mCutoutHeight) should be smaller then
    231             // mAmbientState.getTopPadding().
    232             float openedAmount = (viewState.yTranslation - getFullyClosedTranslation())
    233                     / (getIntrinsicHeight() * 2 + mCutoutHeight);
    234             openedAmount = Math.min(1.0f, openedAmount);
    235             viewState.openedAmount = openedAmount;
    236             viewState.clipTopAmount = 0;
    237             viewState.alpha = 1;
    238             viewState.belowSpeedBump = mAmbientState.getSpeedBumpIndex() == 0;
    239             viewState.hideSensitive = false;
    240             viewState.xTranslation = getTranslationX();
    241             if (mNotGoneIndex != -1) {
    242                 viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex);
    243             }
    244             viewState.hasItemsInStableShelf = lastViewState.inShelf;
    245             viewState.hidden = !mAmbientState.isShadeExpanded()
    246                     || mAmbientState.isQsCustomizerShowing();
    247             viewState.maxShelfEnd = maxShelfEnd;
    248         } else {
    249             viewState.hidden = true;
    250             viewState.location = ExpandableViewState.LOCATION_GONE;
    251             viewState.hasItemsInStableShelf = false;
    252         }
    253     }
    254 
    255     /**
    256      * Update the shelf appearance based on the other notifications around it. This transforms
    257      * the icons from the notification area into the shelf.
    258      */
    259     public void updateAppearance() {
    260         // If the shelf should not be shown, then there is no need to update anything.
    261         if (!mShowNotificationShelf) {
    262             return;
    263         }
    264 
    265         mShelfIcons.resetViewStates();
    266         float shelfStart = getTranslationY();
    267         float numViewsInShelf = 0.0f;
    268         View lastChild = mAmbientState.getLastVisibleBackgroundChild();
    269         mNotGoneIndex = -1;
    270         float interpolationStart = mMaxLayoutHeight - getIntrinsicHeight() * 2;
    271         float expandAmount = 0.0f;
    272         if (shelfStart >= interpolationStart) {
    273             expandAmount = (shelfStart - interpolationStart) / getIntrinsicHeight();
    274             expandAmount = Math.min(1.0f, expandAmount);
    275         }
    276         //  find the first view that doesn't overlap with the shelf
    277         int notGoneIndex = 0;
    278         int colorOfViewBeforeLast = NO_COLOR;
    279         boolean backgroundForceHidden = false;
    280         if (mHideBackground && !((ShelfState) getViewState()).hasItemsInStableShelf) {
    281             backgroundForceHidden = true;
    282         }
    283         int colorTwoBefore = NO_COLOR;
    284         int previousColor = NO_COLOR;
    285         float transitionAmount = 0.0f;
    286         float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity();
    287         boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold
    288                 || (mAmbientState.isExpansionChanging()
    289                         && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold);
    290         boolean scrolling = currentScrollVelocity > 0;
    291         boolean expandingAnimated = mAmbientState.isExpansionChanging()
    292                 && !mAmbientState.isPanelTracking();
    293         int baseZHeight = mAmbientState.getBaseZHeight();
    294         int backgroundTop = 0;
    295         int clipTopAmount = 0;
    296         float firstElementRoundness = 0.0f;
    297         ActivatableNotificationView previousRow = null;
    298 
    299         for (int i = 0; i < mHostLayout.getChildCount(); i++) {
    300             ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
    301 
    302             if (!(child instanceof ActivatableNotificationView)
    303                     || child.getVisibility() == GONE || child == this) {
    304                 continue;
    305             }
    306 
    307             ActivatableNotificationView row = (ActivatableNotificationView) child;
    308             float notificationClipEnd;
    309             boolean aboveShelf = ViewState.getFinalTranslationZ(row) > baseZHeight
    310                     || row.isPinned();
    311             boolean isLastChild = child == lastChild;
    312             float rowTranslationY = row.getTranslationY();
    313             if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) {
    314                 notificationClipEnd = shelfStart + getIntrinsicHeight();
    315             } else {
    316                 notificationClipEnd = shelfStart - mPaddingBetweenElements;
    317                 float height = notificationClipEnd - rowTranslationY;
    318                 if (!row.isBelowSpeedBump() && height <= getNotificationMergeSize()) {
    319                     // We want the gap to close when we reached the minimum size and only shrink
    320                     // before
    321                     notificationClipEnd = Math.min(shelfStart,
    322                             rowTranslationY + getNotificationMergeSize());
    323                 }
    324             }
    325             int clipTop = updateNotificationClipHeight(row, notificationClipEnd, notGoneIndex);
    326             clipTopAmount = Math.max(clipTop, clipTopAmount);
    327 
    328             // If the current row is an ExpandableNotificationRow, update its color, roundedness,
    329             // and icon state.
    330             if (row instanceof ExpandableNotificationRow) {
    331                 ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) row;
    332 
    333                 float inShelfAmount = updateIconAppearance(expandableRow, expandAmount, scrolling,
    334                         scrollingFast,
    335                         expandingAnimated, isLastChild);
    336                 numViewsInShelf += inShelfAmount;
    337                 int ownColorUntinted = row.getBackgroundColorWithoutTint();
    338                 if (rowTranslationY >= shelfStart && mNotGoneIndex == -1) {
    339                     mNotGoneIndex = notGoneIndex;
    340                     setTintColor(previousColor);
    341                     setOverrideTintColor(colorTwoBefore, transitionAmount);
    342 
    343                 } else if (mNotGoneIndex == -1) {
    344                     colorTwoBefore = previousColor;
    345                     transitionAmount = inShelfAmount;
    346                 }
    347                 if (isLastChild) {
    348                     if (colorOfViewBeforeLast == NO_COLOR) {
    349                         colorOfViewBeforeLast = ownColorUntinted;
    350                     }
    351                     row.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount);
    352                 } else {
    353                     colorOfViewBeforeLast = ownColorUntinted;
    354                     row.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */);
    355                 }
    356                 if (notGoneIndex != 0 || !aboveShelf) {
    357                     expandableRow.setAboveShelf(false);
    358                 }
    359                 if (notGoneIndex == 0) {
    360                     StatusBarIconView icon = expandableRow.getEntry().expandedIcon;
    361                     NotificationIconContainer.IconState iconState = getIconState(icon);
    362                     // The icon state might be null in rare cases where the notification is actually
    363                     // added to the layout, but not to the shelf. An example are replied messages,
    364                     // since they don't show up on AOD
    365                     if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
    366                         // only if the first icon is fully in the shelf we want to clip to it!
    367                         backgroundTop = (int) (row.getTranslationY() - getTranslationY());
    368                         firstElementRoundness = row.getCurrentTopRoundness();
    369                     }
    370                 }
    371 
    372                 previousColor = ownColorUntinted;
    373                 notGoneIndex++;
    374             }
    375 
    376             if (row.isFirstInSection() && previousRow != null && previousRow.isLastInSection()) {
    377                 // If the top of the shelf is between the view before a gap and the view after a gap
    378                 // then we need to adjust the shelf's top roundness.
    379                 float distanceToGapBottom = row.getTranslationY() - getTranslationY();
    380                 float distanceToGapTop = getTranslationY()
    381                         - (previousRow.getTranslationY() + previousRow.getActualHeight());
    382                 if (distanceToGapTop > 0) {
    383                     // We interpolate our top roundness so that it's fully rounded if we're at the
    384                     // bottom of the gap, and not rounded at all if we're at the top of the gap
    385                     // (directly up against the bottom of previousRow)
    386                     // Then we apply the same roundness to the bottom of previousRow so that the
    387                     // corners join together as the shelf approaches previousRow.
    388                     firstElementRoundness = (float) Math.min(1.0, distanceToGapTop / mGapHeight);
    389                     previousRow.setBottomRoundness(firstElementRoundness,
    390                             false /* don't animate */);
    391                     backgroundTop = (int) distanceToGapBottom;
    392                 }
    393             }
    394             previousRow = row;
    395         }
    396         clipTransientViews();
    397 
    398         setClipTopAmount(clipTopAmount);
    399         setBackgroundTop(backgroundTop);
    400         setFirstElementRoundness(firstElementRoundness);
    401         mShelfIcons.setSpeedBumpIndex(mAmbientState.getSpeedBumpIndex());
    402         mShelfIcons.calculateIconTranslations();
    403         mShelfIcons.applyIconStates();
    404         for (int i = 0; i < mHostLayout.getChildCount(); i++) {
    405             View child = mHostLayout.getChildAt(i);
    406             if (!(child instanceof ExpandableNotificationRow)
    407                     || child.getVisibility() == GONE) {
    408                 continue;
    409             }
    410             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
    411             updateIconClipAmount(row);
    412             updateContinuousClipping(row);
    413         }
    414         boolean hideBackground = numViewsInShelf < 1.0f;
    415         setHideBackground(hideBackground || backgroundForceHidden);
    416         if (mNotGoneIndex == -1) {
    417             mNotGoneIndex = notGoneIndex;
    418         }
    419     }
    420 
    421     /**
    422      * Clips transient views to the top of the shelf - Transient views are only used for
    423      * disappearing views/animations and need to be clipped correctly by the shelf to ensure they
    424      * don't show underneath the notification stack when something is animating and the user
    425      * swipes quickly.
    426      */
    427     private void clipTransientViews() {
    428         for (int i = 0; i < mHostLayout.getTransientViewCount(); i++) {
    429             View transientView = mHostLayout.getTransientView(i);
    430             if (transientView instanceof ExpandableNotificationRow) {
    431                 ExpandableNotificationRow transientRow = (ExpandableNotificationRow) transientView;
    432                 updateNotificationClipHeight(transientRow, getTranslationY(), -1);
    433             } else {
    434                 Log.e(TAG, "NotificationShelf.clipTransientViews(): "
    435                         + "Trying to clip non-row transient view");
    436             }
    437         }
    438     }
    439 
    440     private void setFirstElementRoundness(float firstElementRoundness) {
    441         if (mFirstElementRoundness != firstElementRoundness) {
    442             mFirstElementRoundness = firstElementRoundness;
    443             setTopRoundness(firstElementRoundness, false /* animate */);
    444         }
    445     }
    446 
    447     private void updateIconClipAmount(ExpandableNotificationRow row) {
    448         float maxTop = row.getTranslationY();
    449         if (getClipTopAmount() != 0) {
    450             // if the shelf is clipped, lets make sure we also clip the icon
    451             maxTop = Math.max(maxTop, getTranslationY() + getClipTopAmount());
    452         }
    453         StatusBarIconView icon = row.getEntry().expandedIcon;
    454         float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY();
    455         if (shelfIconPosition < maxTop && !mAmbientState.isFullyDark()) {
    456             int top = (int) (maxTop - shelfIconPosition);
    457             Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight()));
    458             icon.setClipBounds(clipRect);
    459         } else {
    460             icon.setClipBounds(null);
    461         }
    462     }
    463 
    464     private void updateContinuousClipping(final ExpandableNotificationRow row) {
    465         StatusBarIconView icon = row.getEntry().expandedIcon;
    466         boolean needsContinuousClipping = ViewState.isAnimatingY(icon) && !mAmbientState.isDark();
    467         boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null;
    468         if (needsContinuousClipping && !isContinuousClipping) {
    469             final ViewTreeObserver observer = icon.getViewTreeObserver();
    470             ViewTreeObserver.OnPreDrawListener predrawListener =
    471                     new ViewTreeObserver.OnPreDrawListener() {
    472                         @Override
    473                         public boolean onPreDraw() {
    474                             boolean animatingY = ViewState.isAnimatingY(icon);
    475                             if (!animatingY) {
    476                                 if (observer.isAlive()) {
    477                                     observer.removeOnPreDrawListener(this);
    478                                 }
    479                                 icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
    480                                 return true;
    481                             }
    482                             updateIconClipAmount(row);
    483                             return true;
    484                         }
    485                     };
    486             observer.addOnPreDrawListener(predrawListener);
    487             icon.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
    488                 @Override
    489                 public void onViewAttachedToWindow(View v) {
    490                 }
    491 
    492                 @Override
    493                 public void onViewDetachedFromWindow(View v) {
    494                     if (v == icon) {
    495                         if (observer.isAlive()) {
    496                             observer.removeOnPreDrawListener(predrawListener);
    497                         }
    498                         icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
    499                     }
    500                 }
    501             });
    502             icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener);
    503         }
    504     }
    505 
    506     /**
    507      * Update the clipping of this view.
    508      * @return the amount that our own top should be clipped
    509      */
    510     private int updateNotificationClipHeight(ActivatableNotificationView row,
    511             float notificationClipEnd, int childIndex) {
    512         float viewEnd = row.getTranslationY() + row.getActualHeight();
    513         boolean isPinned = (row.isPinned() || row.isHeadsUpAnimatingAway())
    514                 && !mAmbientState.isDozingAndNotPulsing(row);
    515         boolean shouldClipOwnTop = row.showingAmbientPulsing() && !mAmbientState.isFullyDark()
    516                 || (mAmbientState.isPulseExpanding() && childIndex == 0);
    517         if (viewEnd > notificationClipEnd && !shouldClipOwnTop
    518                 && (mAmbientState.isShadeExpanded() || !isPinned)) {
    519             int clipBottomAmount = (int) (viewEnd - notificationClipEnd);
    520             if (isPinned) {
    521                 clipBottomAmount = Math.min(row.getIntrinsicHeight() - row.getCollapsedHeight(),
    522                         clipBottomAmount);
    523             }
    524             row.setClipBottomAmount(clipBottomAmount);
    525         } else {
    526             row.setClipBottomAmount(0);
    527         }
    528         if (shouldClipOwnTop) {
    529             return (int) (viewEnd - getTranslationY());
    530         } else {
    531             return 0;
    532         }
    533     }
    534 
    535     @Override
    536     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
    537             int outlineTranslation) {
    538         if (!mHasItemsInStableShelf) {
    539             shadowIntensity = 0.0f;
    540         }
    541         super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation);
    542     }
    543 
    544     /**
    545      * @return the icon amount how much this notification is in the shelf;
    546      */
    547     private float updateIconAppearance(ExpandableNotificationRow row, float expandAmount,
    548             boolean scrolling, boolean scrollingFast, boolean expandingAnimated,
    549             boolean isLastChild) {
    550         StatusBarIconView icon = row.getEntry().expandedIcon;
    551         NotificationIconContainer.IconState iconState = getIconState(icon);
    552         if (iconState == null) {
    553             return 0.0f;
    554         }
    555 
    556         // Let calculate how much the view is in the shelf
    557         float viewStart = row.getTranslationY();
    558         int fullHeight = row.getActualHeight() + mPaddingBetweenElements;
    559         float iconTransformDistance = getIntrinsicHeight() * 1.5f;
    560         iconTransformDistance *= NotificationUtils.interpolate(1.f, 1.5f, expandAmount);
    561         iconTransformDistance = Math.min(iconTransformDistance, fullHeight);
    562         if (isLastChild) {
    563             fullHeight = Math.min(fullHeight, row.getMinHeight() - getIntrinsicHeight());
    564             iconTransformDistance = Math.min(iconTransformDistance, row.getMinHeight()
    565                     - getIntrinsicHeight());
    566         }
    567         float viewEnd = viewStart + fullHeight;
    568         // TODO: fix this check for anchor scrolling.
    569         if (expandingAnimated && mAmbientState.getScrollY() == 0
    570                 && !mAmbientState.isOnKeyguard() && !iconState.isLastExpandIcon) {
    571             // We are expanding animated. Because we switch to a linear interpolation in this case,
    572             // the last icon may be stuck in between the shelf position and the notification
    573             // position, which looks pretty bad. We therefore optimize this case by applying a
    574             // shorter transition such that the icon is either fully in the notification or we clamp
    575             // it into the shelf if it's close enough.
    576             // We need to persist this, since after the expansion, the behavior should still be the
    577             // same.
    578             float position = mAmbientState.getIntrinsicPadding()
    579                     + mHostLayout.getPositionInLinearLayout(row);
    580             int maxShelfStart = mMaxLayoutHeight - getIntrinsicHeight();
    581             if (position < maxShelfStart && position + row.getIntrinsicHeight() >= maxShelfStart
    582                     && row.getTranslationY() < position) {
    583                 iconState.isLastExpandIcon = true;
    584                 iconState.customTransformHeight = NO_VALUE;
    585                 // Let's check if we're close enough to snap into the shelf
    586                 boolean forceInShelf = mMaxLayoutHeight - getIntrinsicHeight() - position
    587                         < getIntrinsicHeight();
    588                 if (!forceInShelf) {
    589                     // We are overlapping the shelf but not enough, so the icon needs to be
    590                     // repositioned
    591                     iconState.customTransformHeight = (int) (mMaxLayoutHeight
    592                             - getIntrinsicHeight() - position);
    593                 }
    594             }
    595         }
    596         float fullTransitionAmount;
    597         float iconTransitionAmount;
    598         float shelfStart = getTranslationY();
    599         if (iconState.hasCustomTransformHeight()) {
    600             fullHeight = iconState.customTransformHeight;
    601             iconTransformDistance = iconState.customTransformHeight;
    602         }
    603         boolean fullyInOrOut = true;
    604         if (viewEnd >= shelfStart && (!mAmbientState.isUnlockHintRunning() || row.isInShelf())
    605                 && (mAmbientState.isShadeExpanded()
    606                         || (!row.isPinned() && !row.isHeadsUpAnimatingAway()))) {
    607             if (viewStart < shelfStart) {
    608                 float fullAmount = (shelfStart - viewStart) / fullHeight;
    609                 fullAmount = Math.min(1.0f, fullAmount);
    610                 float interpolatedAmount =  Interpolators.ACCELERATE_DECELERATE.getInterpolation(
    611                         fullAmount);
    612                 interpolatedAmount = NotificationUtils.interpolate(
    613                         interpolatedAmount, fullAmount, expandAmount);
    614                 fullTransitionAmount = 1.0f - interpolatedAmount;
    615 
    616                 iconTransitionAmount = (shelfStart - viewStart) / iconTransformDistance;
    617                 iconTransitionAmount = Math.min(1.0f, iconTransitionAmount);
    618                 iconTransitionAmount = 1.0f - iconTransitionAmount;
    619                 fullyInOrOut = false;
    620             } else {
    621                 fullTransitionAmount = 1.0f;
    622                 iconTransitionAmount = 1.0f;
    623             }
    624         } else {
    625             fullTransitionAmount = 0.0f;
    626             iconTransitionAmount = 0.0f;
    627         }
    628         if (fullyInOrOut && !expandingAnimated && iconState.isLastExpandIcon) {
    629             iconState.isLastExpandIcon = false;
    630             iconState.customTransformHeight = NO_VALUE;
    631         }
    632         updateIconPositioning(row, iconTransitionAmount, fullTransitionAmount,
    633                 iconTransformDistance, scrolling, scrollingFast, expandingAnimated, isLastChild);
    634         return fullTransitionAmount;
    635     }
    636 
    637     private void updateIconPositioning(ExpandableNotificationRow row, float iconTransitionAmount,
    638             float fullTransitionAmount, float iconTransformDistance, boolean scrolling,
    639             boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) {
    640         StatusBarIconView icon = row.getEntry().expandedIcon;
    641         NotificationIconContainer.IconState iconState = getIconState(icon);
    642         if (iconState == null) {
    643             return;
    644         }
    645         boolean forceInShelf = iconState.isLastExpandIcon && !iconState.hasCustomTransformHeight();
    646         float clampedAmount = iconTransitionAmount > 0.5f ? 1.0f : 0.0f;
    647         if (clampedAmount == fullTransitionAmount) {
    648             iconState.noAnimations = (scrollingFast || expandingAnimated) && !forceInShelf;
    649             iconState.useFullTransitionAmount = iconState.noAnimations
    650                 || (!ICON_ANMATIONS_WHILE_SCROLLING && fullTransitionAmount == 0.0f && scrolling);
    651             iconState.useLinearTransitionAmount = !ICON_ANMATIONS_WHILE_SCROLLING
    652                     && fullTransitionAmount == 0.0f && !mAmbientState.isExpansionChanging();
    653             iconState.translateContent = mMaxLayoutHeight - getTranslationY()
    654                     - getIntrinsicHeight() > 0;
    655         }
    656         if (!forceInShelf && (scrollingFast || (expandingAnimated
    657                 && iconState.useFullTransitionAmount && !ViewState.isAnimatingY(icon)))) {
    658             iconState.cancelAnimations(icon);
    659             iconState.useFullTransitionAmount = true;
    660             iconState.noAnimations = true;
    661         }
    662         if (iconState.hasCustomTransformHeight()) {
    663             iconState.useFullTransitionAmount = true;
    664         }
    665         if (iconState.isLastExpandIcon) {
    666             iconState.translateContent = false;
    667         }
    668         float transitionAmount;
    669         if (mAmbientState.isDarkAtAll() && !row.isInShelf()) {
    670             transitionAmount = mAmbientState.isFullyDark() ? 1 : 0;
    671         } else if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount
    672                 || iconState.useLinearTransitionAmount) {
    673             transitionAmount = iconTransitionAmount;
    674         } else {
    675             // We take the clamped position instead
    676             transitionAmount = clampedAmount;
    677             iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount
    678                     && !mNoAnimationsInThisFrame;
    679         }
    680         iconState.iconAppearAmount = !USE_ANIMATIONS_WHEN_OPENING
    681                     || iconState.useFullTransitionAmount
    682                 ? fullTransitionAmount
    683                 : transitionAmount;
    684         iconState.clampedAppearAmount = clampedAmount;
    685         float contentTransformationAmount = !row.isAboveShelf()
    686                     && (isLastChild || iconState.translateContent)
    687                 ? iconTransitionAmount
    688                 : 0.0f;
    689         row.setContentTransformationAmount(contentTransformationAmount, isLastChild);
    690         setIconTransformationAmount(row, transitionAmount, iconTransformDistance,
    691                 clampedAmount != transitionAmount, isLastChild);
    692     }
    693 
    694     private void setIconTransformationAmount(ExpandableNotificationRow row,
    695             float transitionAmount, float iconTransformDistance, boolean usingLinearInterpolation,
    696             boolean isLastChild) {
    697         StatusBarIconView icon = row.getEntry().expandedIcon;
    698         NotificationIconContainer.IconState iconState = getIconState(icon);
    699 
    700         View rowIcon = row.getNotificationIcon();
    701         float notificationIconPosition = row.getTranslationY() + row.getContentTranslation();
    702         boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf();
    703         if (usingLinearInterpolation && !stayingInShelf) {
    704             // If we interpolate from the notification position, this might lead to a slightly
    705             // odd interpolation, since the notification position changes as well. Let's interpolate
    706             // from a fixed distance. We can only do this if we don't animate and the icon is
    707             // always in the interpolated positon.
    708             notificationIconPosition = getTranslationY() - iconTransformDistance;
    709         }
    710         float notificationIconSize = 0.0f;
    711         int iconTopPadding;
    712         if (rowIcon != null) {
    713             iconTopPadding = row.getRelativeTopPadding(rowIcon);
    714             notificationIconSize = rowIcon.getHeight();
    715         } else {
    716             iconTopPadding = mIconAppearTopPadding;
    717         }
    718         notificationIconPosition += iconTopPadding;
    719         float shelfIconPosition = getTranslationY() + icon.getTop();
    720         float iconSize = mDark ? mDarkShelfIconSize : mIconSize;
    721         shelfIconPosition += (icon.getHeight() - icon.getIconScale() * iconSize) / 2.0f;
    722         float iconYTranslation = NotificationUtils.interpolate(
    723                 notificationIconPosition - shelfIconPosition,
    724                 0,
    725                 transitionAmount);
    726         float shelfIconSize = iconSize * icon.getIconScale();
    727         float alpha = 1.0f;
    728         boolean noIcon = !row.isShowingIcon();
    729         if (noIcon) {
    730             // The view currently doesn't have an icon, lets transform it in!
    731             alpha = transitionAmount;
    732             notificationIconSize = shelfIconSize / 2.0f;
    733         }
    734         // The notification size is different from the size in the shelf / statusbar
    735         float newSize = NotificationUtils.interpolate(notificationIconSize, shelfIconSize,
    736                 transitionAmount);
    737         if (iconState != null) {
    738             iconState.scaleX = newSize / shelfIconSize;
    739             iconState.scaleY = iconState.scaleX;
    740             iconState.hidden = transitionAmount == 0.0f && !iconState.isAnimating(icon);
    741             boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf();
    742             if (isAppearing) {
    743                 iconState.hidden = true;
    744                 iconState.iconAppearAmount = 0.0f;
    745             }
    746             iconState.alpha = alpha;
    747             iconState.yTranslation = iconYTranslation;
    748             if (stayingInShelf) {
    749                 iconState.iconAppearAmount = 1.0f;
    750                 iconState.alpha = 1.0f;
    751                 iconState.scaleX = 1.0f;
    752                 iconState.scaleY = 1.0f;
    753                 iconState.hidden = false;
    754             }
    755             if ((row.isAboveShelf() || (!row.isInShelf() && (isLastChild && row.areGutsExposed()
    756                     || row.getTranslationZ() > mAmbientState.getBaseZHeight())))
    757                         && !mAmbientState.isFullyDark()) {
    758                 iconState.hidden = true;
    759             }
    760             int backgroundColor = getBackgroundColorWithoutTint();
    761             int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor);
    762             if (!noIcon && shelfColor != StatusBarIconView.NO_COLOR) {
    763                 int iconColor = row.getVisibleNotificationHeader().getOriginalIconColor();
    764                 shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor,
    765                         iconState.iconAppearAmount);
    766             }
    767             iconState.iconColor = shelfColor;
    768         }
    769     }
    770 
    771     private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) {
    772         return mShelfIcons.getIconState(icon);
    773     }
    774 
    775     private float getFullyClosedTranslation() {
    776         return - (getIntrinsicHeight() - mStatusBarHeight) / 2;
    777     }
    778 
    779     public int getNotificationMergeSize() {
    780         return getIntrinsicHeight();
    781     }
    782 
    783     @Override
    784     public boolean hasNoContentHeight() {
    785         return true;
    786     }
    787 
    788     private void setHideBackground(boolean hideBackground) {
    789         if (mHideBackground != hideBackground) {
    790             mHideBackground = hideBackground;
    791             updateBackground();
    792             updateOutline();
    793         }
    794     }
    795 
    796     @Override
    797     protected boolean needsOutline() {
    798         return !mHideBackground && !mDark && super.needsOutline();
    799     }
    800 
    801     @Override
    802     protected boolean shouldHideBackground() {
    803         return super.shouldHideBackground() || mHideBackground || mDark;
    804     }
    805 
    806     @Override
    807     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    808         super.onLayout(changed, left, top, right, bottom);
    809         updateRelativeOffset();
    810 
    811         // we always want to clip to our sides, such that nothing can draw outside of these bounds
    812         int height = getResources().getDisplayMetrics().heightPixels;
    813         mClipRect.set(0, -height, getWidth(), height);
    814         mShelfIcons.setClipBounds(mClipRect);
    815     }
    816 
    817     private void updateRelativeOffset() {
    818         mCollapsedIcons.getLocationOnScreen(mTmp);
    819         mRelativeOffset = mTmp[0];
    820         getLocationOnScreen(mTmp);
    821         mRelativeOffset -= mTmp[0];
    822     }
    823 
    824     @Override
    825     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    826         WindowInsets ret = super.onApplyWindowInsets(insets);
    827 
    828         // NotificationShelf drag from the status bar and the status bar dock on the top
    829         // of the display for current design so just focus on the top of ScreenDecorations.
    830         // In landscape or multiple window split mode, the NotificationShelf still drag from
    831         // the top and the physical notch/cutout goes to the right, left, or both side of the
    832         // display so it doesn't matter for the NotificationSelf in landscape.
    833         DisplayCutout displayCutout = insets.getDisplayCutout();
    834         mCutoutHeight = displayCutout == null || displayCutout.getSafeInsetTop() < 0
    835                 ? 0 : displayCutout.getSafeInsetTop();
    836 
    837         return ret;
    838     }
    839 
    840     private void setOpenedAmount(float openedAmount) {
    841         mNoAnimationsInThisFrame = openedAmount == 1.0f && mOpenedAmount == 0.0f;
    842         mOpenedAmount = openedAmount;
    843         if (!mAmbientState.isPanelFullWidth() || mAmbientState.isDark()) {
    844             // We don't do a transformation at all, lets just assume we are fully opened
    845             openedAmount = 1.0f;
    846         }
    847         int start = mRelativeOffset;
    848         if (isLayoutRtl()) {
    849             start = getWidth() - start - mCollapsedIcons.getWidth();
    850         }
    851         int width = (int) NotificationUtils.interpolate(
    852                 start + mCollapsedIcons.getFinalTranslationX(),
    853                 mShelfIcons.getWidth(),
    854                 FAST_OUT_SLOW_IN_REVERSE.getInterpolation(openedAmount));
    855         mShelfIcons.setActualLayoutWidth(width);
    856         boolean hasOverflow = mCollapsedIcons.hasOverflow();
    857         int collapsedPadding = mCollapsedIcons.getPaddingEnd();
    858         if (!hasOverflow) {
    859             // we have to ensure that adding the low priority notification won't lead to an
    860             // overflow
    861             collapsedPadding -= mCollapsedIcons.getNoOverflowExtraPadding();
    862         } else {
    863             // Partial overflow padding will fill enough space to add extra dots
    864             collapsedPadding -= mCollapsedIcons.getPartialOverflowExtraPadding();
    865         }
    866         float padding = NotificationUtils.interpolate(collapsedPadding,
    867                 mShelfIcons.getPaddingEnd(),
    868                 openedAmount);
    869         mShelfIcons.setActualPaddingEnd(padding);
    870         float paddingStart = NotificationUtils.interpolate(start,
    871                 mShelfIcons.getPaddingStart(), openedAmount);
    872         mShelfIcons.setActualPaddingStart(paddingStart);
    873         mShelfIcons.setOpenedAmount(openedAmount);
    874     }
    875 
    876     public void setMaxLayoutHeight(int maxLayoutHeight) {
    877         mMaxLayoutHeight = maxLayoutHeight;
    878     }
    879 
    880     /**
    881      * @return the index of the notification at which the shelf visually resides
    882      */
    883     public int getNotGoneIndex() {
    884         return mNotGoneIndex;
    885     }
    886 
    887     private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) {
    888         if (mHasItemsInStableShelf != hasItemsInStableShelf) {
    889             mHasItemsInStableShelf = hasItemsInStableShelf;
    890             updateInteractiveness();
    891         }
    892     }
    893 
    894     /**
    895      * @return whether the shelf has any icons in it when a potential animation has finished, i.e
    896      *         if the current state would be applied right now
    897      */
    898     public boolean hasItemsInStableShelf() {
    899         return mHasItemsInStableShelf;
    900     }
    901 
    902     public void setCollapsedIcons(NotificationIconContainer collapsedIcons) {
    903         mCollapsedIcons = collapsedIcons;
    904         mCollapsedIcons.addOnLayoutChangeListener(this);
    905     }
    906 
    907     @Override
    908     public void onStateChanged(int newState) {
    909         mStatusBarState = newState;
    910         updateInteractiveness();
    911     }
    912 
    913     private void updateInteractiveness() {
    914         mInteractive = mStatusBarState == StatusBarState.KEYGUARD && mHasItemsInStableShelf
    915                 && !mDark;
    916         setClickable(mInteractive);
    917         setFocusable(mInteractive);
    918         setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
    919                 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
    920     }
    921 
    922     @Override
    923     protected boolean isInteractive() {
    924         return mInteractive;
    925     }
    926 
    927     public void setMaxShelfEnd(float maxShelfEnd) {
    928         mMaxShelfEnd = maxShelfEnd;
    929     }
    930 
    931     public void setAnimationsEnabled(boolean enabled) {
    932         mAnimationsEnabled = enabled;
    933         if (!enabled) {
    934             // we need to wait with enabling the animations until the first frame has passed
    935             mShelfIcons.setAnimationsEnabled(false);
    936         }
    937     }
    938 
    939     @Override
    940     public boolean hasOverlappingRendering() {
    941         return false;  // Shelf only uses alpha for transitions where the difference can't be seen.
    942     }
    943 
    944     @Override
    945     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    946         super.onInitializeAccessibilityNodeInfo(info);
    947         if (mInteractive) {
    948             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
    949             AccessibilityNodeInfo.AccessibilityAction unlock
    950                     = new AccessibilityNodeInfo.AccessibilityAction(
    951                     AccessibilityNodeInfo.ACTION_CLICK,
    952                     getContext().getString(R.string.accessibility_overflow_action));
    953             info.addAction(unlock);
    954         }
    955     }
    956 
    957     @Override
    958     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
    959             int oldTop, int oldRight, int oldBottom) {
    960         updateRelativeOffset();
    961     }
    962 
    963     public void onUiModeChanged() {
    964         updateBackgroundColors();
    965     }
    966 
    967     private class ShelfState extends ExpandableViewState {
    968         private float openedAmount;
    969         private boolean hasItemsInStableShelf;
    970         private float maxShelfEnd;
    971 
    972         @Override
    973         public void applyToView(View view) {
    974             if (!mShowNotificationShelf) {
    975                 return;
    976             }
    977 
    978             super.applyToView(view);
    979             setMaxShelfEnd(maxShelfEnd);
    980             setOpenedAmount(openedAmount);
    981             updateAppearance();
    982             setHasItemsInStableShelf(hasItemsInStableShelf);
    983             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
    984         }
    985 
    986         @Override
    987         public void animateTo(View child, AnimationProperties properties) {
    988             if (!mShowNotificationShelf) {
    989                 return;
    990             }
    991 
    992             super.animateTo(child, properties);
    993             setMaxShelfEnd(maxShelfEnd);
    994             setOpenedAmount(openedAmount);
    995             updateAppearance();
    996             setHasItemsInStableShelf(hasItemsInStableShelf);
    997             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
    998         }
    999     }
   1000 }
   1001