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