/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar; import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN_REVERSE; import static com.android.systemui.statusbar.phone.NotificationIconContainer.IconState.NO_VALUE; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.os.SystemProperties; import android.util.AttributeSet; import android.util.Log; import android.view.DisplayCutout; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.notification.stack.ViewState; import com.android.systemui.statusbar.phone.NotificationIconContainer; /** * A notification shelf view that is placed inside the notification scroller. It manages the * overflow icons that don't fit into the regular list anymore. */ public class NotificationShelf extends ActivatableNotificationView implements View.OnLayoutChangeListener, StateListener { private static final boolean USE_ANIMATIONS_WHEN_OPENING = SystemProperties.getBoolean("debug.icon_opening_animations", true); private static final boolean ICON_ANMATIONS_WHILE_SCROLLING = SystemProperties.getBoolean("debug.icon_scroll_animations", true); private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag; private static final String TAG = "NotificationShelf"; private static final long SHELF_IN_TRANSLATION_DURATION = 200; private NotificationIconContainer mShelfIcons; private int[] mTmp = new int[2]; private boolean mHideBackground; private int mIconAppearTopPadding; private int mShelfAppearTranslation; private float mDarkShelfPadding; private float mDarkShelfIconSize; private int mStatusBarHeight; private int mStatusBarPaddingStart; private AmbientState mAmbientState; private NotificationStackScrollLayout mHostLayout; private int mMaxLayoutHeight; private int mPaddingBetweenElements; private int mNotGoneIndex; private boolean mHasItemsInStableShelf; private NotificationIconContainer mCollapsedIcons; private int mScrollFastThreshold; private int mIconSize; private int mStatusBarState; private float mMaxShelfEnd; private int mRelativeOffset; private boolean mInteractive; private float mOpenedAmount; private boolean mNoAnimationsInThisFrame; private boolean mAnimationsEnabled = true; private boolean mShowNotificationShelf; private float mFirstElementRoundness; private Rect mClipRect = new Rect(); private int mCutoutHeight; private int mGapHeight; public NotificationShelf(Context context, AttributeSet attrs) { super(context, attrs); } @Override @VisibleForTesting public void onFinishInflate() { super.onFinishInflate(); mShelfIcons = findViewById(R.id.content); mShelfIcons.setClipChildren(false); mShelfIcons.setClipToPadding(false); setClipToActualHeight(false); setClipChildren(false); setClipToPadding(false); mShelfIcons.setIsStaticLayout(false); setBottomRoundness(1.0f, false /* animate */); initDimens(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); ((SysuiStatusBarStateController) Dependency.get(StatusBarStateController.class)) .addCallback(this, SysuiStatusBarStateController.RANK_SHELF); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); Dependency.get(StatusBarStateController.class).removeCallback(this); } public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout) { mAmbientState = ambientState; mHostLayout = hostLayout; } private void initDimens() { Resources res = getResources(); mIconAppearTopPadding = res.getDimensionPixelSize(R.dimen.notification_icon_appear_padding); mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height); mStatusBarPaddingStart = res.getDimensionPixelOffset(R.dimen.status_bar_padding_start); mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height); mShelfAppearTranslation = res.getDimensionPixelSize(R.dimen.shelf_appear_translation); mDarkShelfPadding = res.getDimensionPixelSize(R.dimen.widget_bottom_separator_padding); ViewGroup.LayoutParams layoutParams = getLayoutParams(); layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height); setLayoutParams(layoutParams); int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding); mShelfIcons.setPadding(padding, 0, padding, 0); mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold); mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf); mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size); mDarkShelfIconSize = res.getDimensionPixelOffset(R.dimen.dark_shelf_icon_size); mGapHeight = res.getDimensionPixelSize(R.dimen.qs_notification_padding); if (!mShowNotificationShelf) { setVisibility(GONE); } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); initDimens(); } @Override public void setDark(boolean dark, boolean fade, long delay) { if (mDark == dark) return; super.setDark(dark, fade, delay); mShelfIcons.setDark(dark, fade, delay); updateInteractiveness(); updateOutline(); } /** * Alpha animation with translation played when this view is visible on AOD. */ public void fadeInTranslating() { mShelfIcons.setTranslationY(-mShelfAppearTranslation); mShelfIcons.setAlpha(0); mShelfIcons.animate() .setInterpolator(Interpolators.DECELERATE_QUINT) .translationY(0) .setDuration(SHELF_IN_TRANSLATION_DURATION) .start(); mShelfIcons.animate() .alpha(1) .setInterpolator(Interpolators.LINEAR) .setDuration(SHELF_IN_TRANSLATION_DURATION) .start(); } @Override protected View getContentView() { return mShelfIcons; } public NotificationIconContainer getShelfIcons() { return mShelfIcons; } @Override public ExpandableViewState createExpandableViewState() { return new ShelfState(); } /** Update the state of the shelf. */ public void updateState(AmbientState ambientState) { ExpandableView lastView = ambientState.getLastVisibleBackgroundChild(); ShelfState viewState = (ShelfState) getViewState(); if (mShowNotificationShelf && lastView != null) { float maxShelfEnd = ambientState.getInnerHeight() + ambientState.getTopPadding() + ambientState.getStackTranslation(); ExpandableViewState lastViewState = lastView.getViewState(); float viewEnd = lastViewState.yTranslation + lastViewState.height; viewState.copyFrom(lastViewState); viewState.height = getIntrinsicHeight(); float awakenTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - viewState.height, getFullyClosedTranslation()); float yRatio = mAmbientState.hasPulsingNotifications() ? 0 : mAmbientState.getDarkAmount(); viewState.yTranslation = awakenTranslation + mDarkShelfPadding * yRatio; viewState.zTranslation = ambientState.getBaseZHeight(); // For the small display size, it's not enough to make the icon not covered by // the top cutout so the denominator add the height of cutout. // Totally, (getIntrinsicHeight() * 2 + mCutoutHeight) should be smaller then // mAmbientState.getTopPadding(). float openedAmount = (viewState.yTranslation - getFullyClosedTranslation()) / (getIntrinsicHeight() * 2 + mCutoutHeight); openedAmount = Math.min(1.0f, openedAmount); viewState.openedAmount = openedAmount; viewState.clipTopAmount = 0; viewState.alpha = 1; viewState.belowSpeedBump = mAmbientState.getSpeedBumpIndex() == 0; viewState.hideSensitive = false; viewState.xTranslation = getTranslationX(); if (mNotGoneIndex != -1) { viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex); } viewState.hasItemsInStableShelf = lastViewState.inShelf; viewState.hidden = !mAmbientState.isShadeExpanded() || mAmbientState.isQsCustomizerShowing(); viewState.maxShelfEnd = maxShelfEnd; } else { viewState.hidden = true; viewState.location = ExpandableViewState.LOCATION_GONE; viewState.hasItemsInStableShelf = false; } } /** * Update the shelf appearance based on the other notifications around it. This transforms * the icons from the notification area into the shelf. */ public void updateAppearance() { // If the shelf should not be shown, then there is no need to update anything. if (!mShowNotificationShelf) { return; } mShelfIcons.resetViewStates(); float shelfStart = getTranslationY(); float numViewsInShelf = 0.0f; View lastChild = mAmbientState.getLastVisibleBackgroundChild(); mNotGoneIndex = -1; float interpolationStart = mMaxLayoutHeight - getIntrinsicHeight() * 2; float expandAmount = 0.0f; if (shelfStart >= interpolationStart) { expandAmount = (shelfStart - interpolationStart) / getIntrinsicHeight(); expandAmount = Math.min(1.0f, expandAmount); } // find the first view that doesn't overlap with the shelf int notGoneIndex = 0; int colorOfViewBeforeLast = NO_COLOR; boolean backgroundForceHidden = false; if (mHideBackground && !((ShelfState) getViewState()).hasItemsInStableShelf) { backgroundForceHidden = true; } int colorTwoBefore = NO_COLOR; int previousColor = NO_COLOR; float transitionAmount = 0.0f; float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity(); boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold || (mAmbientState.isExpansionChanging() && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold); boolean scrolling = currentScrollVelocity > 0; boolean expandingAnimated = mAmbientState.isExpansionChanging() && !mAmbientState.isPanelTracking(); int baseZHeight = mAmbientState.getBaseZHeight(); int backgroundTop = 0; int clipTopAmount = 0; float firstElementRoundness = 0.0f; ActivatableNotificationView previousRow = null; for (int i = 0; i < mHostLayout.getChildCount(); i++) { ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i); if (!(child instanceof ActivatableNotificationView) || child.getVisibility() == GONE || child == this) { continue; } ActivatableNotificationView row = (ActivatableNotificationView) child; float notificationClipEnd; boolean aboveShelf = ViewState.getFinalTranslationZ(row) > baseZHeight || row.isPinned(); boolean isLastChild = child == lastChild; float rowTranslationY = row.getTranslationY(); if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) { notificationClipEnd = shelfStart + getIntrinsicHeight(); } else { notificationClipEnd = shelfStart - mPaddingBetweenElements; float height = notificationClipEnd - rowTranslationY; if (!row.isBelowSpeedBump() && height <= getNotificationMergeSize()) { // We want the gap to close when we reached the minimum size and only shrink // before notificationClipEnd = Math.min(shelfStart, rowTranslationY + getNotificationMergeSize()); } } int clipTop = updateNotificationClipHeight(row, notificationClipEnd, notGoneIndex); clipTopAmount = Math.max(clipTop, clipTopAmount); // If the current row is an ExpandableNotificationRow, update its color, roundedness, // and icon state. if (row instanceof ExpandableNotificationRow) { ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) row; float inShelfAmount = updateIconAppearance(expandableRow, expandAmount, scrolling, scrollingFast, expandingAnimated, isLastChild); numViewsInShelf += inShelfAmount; int ownColorUntinted = row.getBackgroundColorWithoutTint(); if (rowTranslationY >= shelfStart && mNotGoneIndex == -1) { mNotGoneIndex = notGoneIndex; setTintColor(previousColor); setOverrideTintColor(colorTwoBefore, transitionAmount); } else if (mNotGoneIndex == -1) { colorTwoBefore = previousColor; transitionAmount = inShelfAmount; } if (isLastChild) { if (colorOfViewBeforeLast == NO_COLOR) { colorOfViewBeforeLast = ownColorUntinted; } row.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount); } else { colorOfViewBeforeLast = ownColorUntinted; row.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */); } if (notGoneIndex != 0 || !aboveShelf) { expandableRow.setAboveShelf(false); } if (notGoneIndex == 0) { StatusBarIconView icon = expandableRow.getEntry().expandedIcon; NotificationIconContainer.IconState iconState = getIconState(icon); // The icon state might be null in rare cases where the notification is actually // added to the layout, but not to the shelf. An example are replied messages, // since they don't show up on AOD if (iconState != null && iconState.clampedAppearAmount == 1.0f) { // only if the first icon is fully in the shelf we want to clip to it! backgroundTop = (int) (row.getTranslationY() - getTranslationY()); firstElementRoundness = row.getCurrentTopRoundness(); } } previousColor = ownColorUntinted; notGoneIndex++; } if (row.isFirstInSection() && previousRow != null && previousRow.isLastInSection()) { // If the top of the shelf is between the view before a gap and the view after a gap // then we need to adjust the shelf's top roundness. float distanceToGapBottom = row.getTranslationY() - getTranslationY(); float distanceToGapTop = getTranslationY() - (previousRow.getTranslationY() + previousRow.getActualHeight()); if (distanceToGapTop > 0) { // We interpolate our top roundness so that it's fully rounded if we're at the // bottom of the gap, and not rounded at all if we're at the top of the gap // (directly up against the bottom of previousRow) // Then we apply the same roundness to the bottom of previousRow so that the // corners join together as the shelf approaches previousRow. firstElementRoundness = (float) Math.min(1.0, distanceToGapTop / mGapHeight); previousRow.setBottomRoundness(firstElementRoundness, false /* don't animate */); backgroundTop = (int) distanceToGapBottom; } } previousRow = row; } clipTransientViews(); setClipTopAmount(clipTopAmount); setBackgroundTop(backgroundTop); setFirstElementRoundness(firstElementRoundness); mShelfIcons.setSpeedBumpIndex(mAmbientState.getSpeedBumpIndex()); mShelfIcons.calculateIconTranslations(); mShelfIcons.applyIconStates(); for (int i = 0; i < mHostLayout.getChildCount(); i++) { View child = mHostLayout.getChildAt(i); if (!(child instanceof ExpandableNotificationRow) || child.getVisibility() == GONE) { continue; } ExpandableNotificationRow row = (ExpandableNotificationRow) child; updateIconClipAmount(row); updateContinuousClipping(row); } boolean hideBackground = numViewsInShelf < 1.0f; setHideBackground(hideBackground || backgroundForceHidden); if (mNotGoneIndex == -1) { mNotGoneIndex = notGoneIndex; } } /** * Clips transient views to the top of the shelf - Transient views are only used for * disappearing views/animations and need to be clipped correctly by the shelf to ensure they * don't show underneath the notification stack when something is animating and the user * swipes quickly. */ private void clipTransientViews() { for (int i = 0; i < mHostLayout.getTransientViewCount(); i++) { View transientView = mHostLayout.getTransientView(i); if (transientView instanceof ExpandableNotificationRow) { ExpandableNotificationRow transientRow = (ExpandableNotificationRow) transientView; updateNotificationClipHeight(transientRow, getTranslationY(), -1); } else { Log.e(TAG, "NotificationShelf.clipTransientViews(): " + "Trying to clip non-row transient view"); } } } private void setFirstElementRoundness(float firstElementRoundness) { if (mFirstElementRoundness != firstElementRoundness) { mFirstElementRoundness = firstElementRoundness; setTopRoundness(firstElementRoundness, false /* animate */); } } private void updateIconClipAmount(ExpandableNotificationRow row) { float maxTop = row.getTranslationY(); if (getClipTopAmount() != 0) { // if the shelf is clipped, lets make sure we also clip the icon maxTop = Math.max(maxTop, getTranslationY() + getClipTopAmount()); } StatusBarIconView icon = row.getEntry().expandedIcon; float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY(); if (shelfIconPosition < maxTop && !mAmbientState.isFullyDark()) { int top = (int) (maxTop - shelfIconPosition); Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight())); icon.setClipBounds(clipRect); } else { icon.setClipBounds(null); } } private void updateContinuousClipping(final ExpandableNotificationRow row) { StatusBarIconView icon = row.getEntry().expandedIcon; boolean needsContinuousClipping = ViewState.isAnimatingY(icon) && !mAmbientState.isDark(); boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null; if (needsContinuousClipping && !isContinuousClipping) { final ViewTreeObserver observer = icon.getViewTreeObserver(); ViewTreeObserver.OnPreDrawListener predrawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { boolean animatingY = ViewState.isAnimatingY(icon); if (!animatingY) { if (observer.isAlive()) { observer.removeOnPreDrawListener(this); } icon.setTag(TAG_CONTINUOUS_CLIPPING, null); return true; } updateIconClipAmount(row); return true; } }; observer.addOnPreDrawListener(predrawListener); icon.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { if (v == icon) { if (observer.isAlive()) { observer.removeOnPreDrawListener(predrawListener); } icon.setTag(TAG_CONTINUOUS_CLIPPING, null); } } }); icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener); } } /** * Update the clipping of this view. * @return the amount that our own top should be clipped */ private int updateNotificationClipHeight(ActivatableNotificationView row, float notificationClipEnd, int childIndex) { float viewEnd = row.getTranslationY() + row.getActualHeight(); boolean isPinned = (row.isPinned() || row.isHeadsUpAnimatingAway()) && !mAmbientState.isDozingAndNotPulsing(row); boolean shouldClipOwnTop = row.showingAmbientPulsing() && !mAmbientState.isFullyDark() || (mAmbientState.isPulseExpanding() && childIndex == 0); if (viewEnd > notificationClipEnd && !shouldClipOwnTop && (mAmbientState.isShadeExpanded() || !isPinned)) { int clipBottomAmount = (int) (viewEnd - notificationClipEnd); if (isPinned) { clipBottomAmount = Math.min(row.getIntrinsicHeight() - row.getCollapsedHeight(), clipBottomAmount); } row.setClipBottomAmount(clipBottomAmount); } else { row.setClipBottomAmount(0); } if (shouldClipOwnTop) { return (int) (viewEnd - getTranslationY()); } else { return 0; } } @Override public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, int outlineTranslation) { if (!mHasItemsInStableShelf) { shadowIntensity = 0.0f; } super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation); } /** * @return the icon amount how much this notification is in the shelf; */ private float updateIconAppearance(ExpandableNotificationRow row, float expandAmount, boolean scrolling, boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) { StatusBarIconView icon = row.getEntry().expandedIcon; NotificationIconContainer.IconState iconState = getIconState(icon); if (iconState == null) { return 0.0f; } // Let calculate how much the view is in the shelf float viewStart = row.getTranslationY(); int fullHeight = row.getActualHeight() + mPaddingBetweenElements; float iconTransformDistance = getIntrinsicHeight() * 1.5f; iconTransformDistance *= NotificationUtils.interpolate(1.f, 1.5f, expandAmount); iconTransformDistance = Math.min(iconTransformDistance, fullHeight); if (isLastChild) { fullHeight = Math.min(fullHeight, row.getMinHeight() - getIntrinsicHeight()); iconTransformDistance = Math.min(iconTransformDistance, row.getMinHeight() - getIntrinsicHeight()); } float viewEnd = viewStart + fullHeight; // TODO: fix this check for anchor scrolling. if (expandingAnimated && mAmbientState.getScrollY() == 0 && !mAmbientState.isOnKeyguard() && !iconState.isLastExpandIcon) { // We are expanding animated. Because we switch to a linear interpolation in this case, // the last icon may be stuck in between the shelf position and the notification // position, which looks pretty bad. We therefore optimize this case by applying a // shorter transition such that the icon is either fully in the notification or we clamp // it into the shelf if it's close enough. // We need to persist this, since after the expansion, the behavior should still be the // same. float position = mAmbientState.getIntrinsicPadding() + mHostLayout.getPositionInLinearLayout(row); int maxShelfStart = mMaxLayoutHeight - getIntrinsicHeight(); if (position < maxShelfStart && position + row.getIntrinsicHeight() >= maxShelfStart && row.getTranslationY() < position) { iconState.isLastExpandIcon = true; iconState.customTransformHeight = NO_VALUE; // Let's check if we're close enough to snap into the shelf boolean forceInShelf = mMaxLayoutHeight - getIntrinsicHeight() - position < getIntrinsicHeight(); if (!forceInShelf) { // We are overlapping the shelf but not enough, so the icon needs to be // repositioned iconState.customTransformHeight = (int) (mMaxLayoutHeight - getIntrinsicHeight() - position); } } } float fullTransitionAmount; float iconTransitionAmount; float shelfStart = getTranslationY(); if (iconState.hasCustomTransformHeight()) { fullHeight = iconState.customTransformHeight; iconTransformDistance = iconState.customTransformHeight; } boolean fullyInOrOut = true; if (viewEnd >= shelfStart && (!mAmbientState.isUnlockHintRunning() || row.isInShelf()) && (mAmbientState.isShadeExpanded() || (!row.isPinned() && !row.isHeadsUpAnimatingAway()))) { if (viewStart < shelfStart) { float fullAmount = (shelfStart - viewStart) / fullHeight; fullAmount = Math.min(1.0f, fullAmount); float interpolatedAmount = Interpolators.ACCELERATE_DECELERATE.getInterpolation( fullAmount); interpolatedAmount = NotificationUtils.interpolate( interpolatedAmount, fullAmount, expandAmount); fullTransitionAmount = 1.0f - interpolatedAmount; iconTransitionAmount = (shelfStart - viewStart) / iconTransformDistance; iconTransitionAmount = Math.min(1.0f, iconTransitionAmount); iconTransitionAmount = 1.0f - iconTransitionAmount; fullyInOrOut = false; } else { fullTransitionAmount = 1.0f; iconTransitionAmount = 1.0f; } } else { fullTransitionAmount = 0.0f; iconTransitionAmount = 0.0f; } if (fullyInOrOut && !expandingAnimated && iconState.isLastExpandIcon) { iconState.isLastExpandIcon = false; iconState.customTransformHeight = NO_VALUE; } updateIconPositioning(row, iconTransitionAmount, fullTransitionAmount, iconTransformDistance, scrolling, scrollingFast, expandingAnimated, isLastChild); return fullTransitionAmount; } private void updateIconPositioning(ExpandableNotificationRow row, float iconTransitionAmount, float fullTransitionAmount, float iconTransformDistance, boolean scrolling, boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) { StatusBarIconView icon = row.getEntry().expandedIcon; NotificationIconContainer.IconState iconState = getIconState(icon); if (iconState == null) { return; } boolean forceInShelf = iconState.isLastExpandIcon && !iconState.hasCustomTransformHeight(); float clampedAmount = iconTransitionAmount > 0.5f ? 1.0f : 0.0f; if (clampedAmount == fullTransitionAmount) { iconState.noAnimations = (scrollingFast || expandingAnimated) && !forceInShelf; iconState.useFullTransitionAmount = iconState.noAnimations || (!ICON_ANMATIONS_WHILE_SCROLLING && fullTransitionAmount == 0.0f && scrolling); iconState.useLinearTransitionAmount = !ICON_ANMATIONS_WHILE_SCROLLING && fullTransitionAmount == 0.0f && !mAmbientState.isExpansionChanging(); iconState.translateContent = mMaxLayoutHeight - getTranslationY() - getIntrinsicHeight() > 0; } if (!forceInShelf && (scrollingFast || (expandingAnimated && iconState.useFullTransitionAmount && !ViewState.isAnimatingY(icon)))) { iconState.cancelAnimations(icon); iconState.useFullTransitionAmount = true; iconState.noAnimations = true; } if (iconState.hasCustomTransformHeight()) { iconState.useFullTransitionAmount = true; } if (iconState.isLastExpandIcon) { iconState.translateContent = false; } float transitionAmount; if (mAmbientState.isDarkAtAll() && !row.isInShelf()) { transitionAmount = mAmbientState.isFullyDark() ? 1 : 0; } else if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount || iconState.useLinearTransitionAmount) { transitionAmount = iconTransitionAmount; } else { // We take the clamped position instead transitionAmount = clampedAmount; iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount && !mNoAnimationsInThisFrame; } iconState.iconAppearAmount = !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount ? fullTransitionAmount : transitionAmount; iconState.clampedAppearAmount = clampedAmount; float contentTransformationAmount = !row.isAboveShelf() && (isLastChild || iconState.translateContent) ? iconTransitionAmount : 0.0f; row.setContentTransformationAmount(contentTransformationAmount, isLastChild); setIconTransformationAmount(row, transitionAmount, iconTransformDistance, clampedAmount != transitionAmount, isLastChild); } private void setIconTransformationAmount(ExpandableNotificationRow row, float transitionAmount, float iconTransformDistance, boolean usingLinearInterpolation, boolean isLastChild) { StatusBarIconView icon = row.getEntry().expandedIcon; NotificationIconContainer.IconState iconState = getIconState(icon); View rowIcon = row.getNotificationIcon(); float notificationIconPosition = row.getTranslationY() + row.getContentTranslation(); boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf(); if (usingLinearInterpolation && !stayingInShelf) { // If we interpolate from the notification position, this might lead to a slightly // odd interpolation, since the notification position changes as well. Let's interpolate // from a fixed distance. We can only do this if we don't animate and the icon is // always in the interpolated positon. notificationIconPosition = getTranslationY() - iconTransformDistance; } float notificationIconSize = 0.0f; int iconTopPadding; if (rowIcon != null) { iconTopPadding = row.getRelativeTopPadding(rowIcon); notificationIconSize = rowIcon.getHeight(); } else { iconTopPadding = mIconAppearTopPadding; } notificationIconPosition += iconTopPadding; float shelfIconPosition = getTranslationY() + icon.getTop(); float iconSize = mDark ? mDarkShelfIconSize : mIconSize; shelfIconPosition += (icon.getHeight() - icon.getIconScale() * iconSize) / 2.0f; float iconYTranslation = NotificationUtils.interpolate( notificationIconPosition - shelfIconPosition, 0, transitionAmount); float shelfIconSize = iconSize * icon.getIconScale(); float alpha = 1.0f; boolean noIcon = !row.isShowingIcon(); if (noIcon) { // The view currently doesn't have an icon, lets transform it in! alpha = transitionAmount; notificationIconSize = shelfIconSize / 2.0f; } // The notification size is different from the size in the shelf / statusbar float newSize = NotificationUtils.interpolate(notificationIconSize, shelfIconSize, transitionAmount); if (iconState != null) { iconState.scaleX = newSize / shelfIconSize; iconState.scaleY = iconState.scaleX; iconState.hidden = transitionAmount == 0.0f && !iconState.isAnimating(icon); boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf(); if (isAppearing) { iconState.hidden = true; iconState.iconAppearAmount = 0.0f; } iconState.alpha = alpha; iconState.yTranslation = iconYTranslation; if (stayingInShelf) { iconState.iconAppearAmount = 1.0f; iconState.alpha = 1.0f; iconState.scaleX = 1.0f; iconState.scaleY = 1.0f; iconState.hidden = false; } if ((row.isAboveShelf() || (!row.isInShelf() && (isLastChild && row.areGutsExposed() || row.getTranslationZ() > mAmbientState.getBaseZHeight()))) && !mAmbientState.isFullyDark()) { iconState.hidden = true; } int backgroundColor = getBackgroundColorWithoutTint(); int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor); if (!noIcon && shelfColor != StatusBarIconView.NO_COLOR) { int iconColor = row.getVisibleNotificationHeader().getOriginalIconColor(); shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor, iconState.iconAppearAmount); } iconState.iconColor = shelfColor; } } private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) { return mShelfIcons.getIconState(icon); } private float getFullyClosedTranslation() { return - (getIntrinsicHeight() - mStatusBarHeight) / 2; } public int getNotificationMergeSize() { return getIntrinsicHeight(); } @Override public boolean hasNoContentHeight() { return true; } private void setHideBackground(boolean hideBackground) { if (mHideBackground != hideBackground) { mHideBackground = hideBackground; updateBackground(); updateOutline(); } } @Override protected boolean needsOutline() { return !mHideBackground && !mDark && super.needsOutline(); } @Override protected boolean shouldHideBackground() { return super.shouldHideBackground() || mHideBackground || mDark; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); updateRelativeOffset(); // we always want to clip to our sides, such that nothing can draw outside of these bounds int height = getResources().getDisplayMetrics().heightPixels; mClipRect.set(0, -height, getWidth(), height); mShelfIcons.setClipBounds(mClipRect); } private void updateRelativeOffset() { mCollapsedIcons.getLocationOnScreen(mTmp); mRelativeOffset = mTmp[0]; getLocationOnScreen(mTmp); mRelativeOffset -= mTmp[0]; } @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { WindowInsets ret = super.onApplyWindowInsets(insets); // NotificationShelf drag from the status bar and the status bar dock on the top // of the display for current design so just focus on the top of ScreenDecorations. // In landscape or multiple window split mode, the NotificationShelf still drag from // the top and the physical notch/cutout goes to the right, left, or both side of the // display so it doesn't matter for the NotificationSelf in landscape. DisplayCutout displayCutout = insets.getDisplayCutout(); mCutoutHeight = displayCutout == null || displayCutout.getSafeInsetTop() < 0 ? 0 : displayCutout.getSafeInsetTop(); return ret; } private void setOpenedAmount(float openedAmount) { mNoAnimationsInThisFrame = openedAmount == 1.0f && mOpenedAmount == 0.0f; mOpenedAmount = openedAmount; if (!mAmbientState.isPanelFullWidth() || mAmbientState.isDark()) { // We don't do a transformation at all, lets just assume we are fully opened openedAmount = 1.0f; } int start = mRelativeOffset; if (isLayoutRtl()) { start = getWidth() - start - mCollapsedIcons.getWidth(); } int width = (int) NotificationUtils.interpolate( start + mCollapsedIcons.getFinalTranslationX(), mShelfIcons.getWidth(), FAST_OUT_SLOW_IN_REVERSE.getInterpolation(openedAmount)); mShelfIcons.setActualLayoutWidth(width); boolean hasOverflow = mCollapsedIcons.hasOverflow(); int collapsedPadding = mCollapsedIcons.getPaddingEnd(); if (!hasOverflow) { // we have to ensure that adding the low priority notification won't lead to an // overflow collapsedPadding -= mCollapsedIcons.getNoOverflowExtraPadding(); } else { // Partial overflow padding will fill enough space to add extra dots collapsedPadding -= mCollapsedIcons.getPartialOverflowExtraPadding(); } float padding = NotificationUtils.interpolate(collapsedPadding, mShelfIcons.getPaddingEnd(), openedAmount); mShelfIcons.setActualPaddingEnd(padding); float paddingStart = NotificationUtils.interpolate(start, mShelfIcons.getPaddingStart(), openedAmount); mShelfIcons.setActualPaddingStart(paddingStart); mShelfIcons.setOpenedAmount(openedAmount); } public void setMaxLayoutHeight(int maxLayoutHeight) { mMaxLayoutHeight = maxLayoutHeight; } /** * @return the index of the notification at which the shelf visually resides */ public int getNotGoneIndex() { return mNotGoneIndex; } private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) { if (mHasItemsInStableShelf != hasItemsInStableShelf) { mHasItemsInStableShelf = hasItemsInStableShelf; updateInteractiveness(); } } /** * @return whether the shelf has any icons in it when a potential animation has finished, i.e * if the current state would be applied right now */ public boolean hasItemsInStableShelf() { return mHasItemsInStableShelf; } public void setCollapsedIcons(NotificationIconContainer collapsedIcons) { mCollapsedIcons = collapsedIcons; mCollapsedIcons.addOnLayoutChangeListener(this); } @Override public void onStateChanged(int newState) { mStatusBarState = newState; updateInteractiveness(); } private void updateInteractiveness() { mInteractive = mStatusBarState == StatusBarState.KEYGUARD && mHasItemsInStableShelf && !mDark; setClickable(mInteractive); setFocusable(mInteractive); setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } @Override protected boolean isInteractive() { return mInteractive; } public void setMaxShelfEnd(float maxShelfEnd) { mMaxShelfEnd = maxShelfEnd; } public void setAnimationsEnabled(boolean enabled) { mAnimationsEnabled = enabled; if (!enabled) { // we need to wait with enabling the animations until the first frame has passed mShelfIcons.setAnimationsEnabled(false); } } @Override public boolean hasOverlappingRendering() { return false; // Shelf only uses alpha for transitions where the difference can't be seen. } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); if (mInteractive) { info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); AccessibilityNodeInfo.AccessibilityAction unlock = new AccessibilityNodeInfo.AccessibilityAction( AccessibilityNodeInfo.ACTION_CLICK, getContext().getString(R.string.accessibility_overflow_action)); info.addAction(unlock); } } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { updateRelativeOffset(); } public void onUiModeChanged() { updateBackgroundColors(); } private class ShelfState extends ExpandableViewState { private float openedAmount; private boolean hasItemsInStableShelf; private float maxShelfEnd; @Override public void applyToView(View view) { if (!mShowNotificationShelf) { return; } super.applyToView(view); setMaxShelfEnd(maxShelfEnd); setOpenedAmount(openedAmount); updateAppearance(); setHasItemsInStableShelf(hasItemsInStableShelf); mShelfIcons.setAnimationsEnabled(mAnimationsEnabled); } @Override public void animateTo(View child, AnimationProperties properties) { if (!mShowNotificationShelf) { return; } super.animateTo(child, properties); setMaxShelfEnd(maxShelfEnd); setOpenedAmount(openedAmount); updateAppearance(); setHasItemsInStableShelf(hasItemsInStableShelf); mShelfIcons.setAnimationsEnabled(mAnimationsEnabled); } } }