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