Home | History | Annotate | Download | only in phone
      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.phone;
     18 
     19 import static com.android.systemui.statusbar.notification.NotificationUtils.isHapticFeedbackDisabled;
     20 
     21 import android.content.Context;
     22 import android.content.res.Configuration;
     23 import android.graphics.Canvas;
     24 import android.graphics.Color;
     25 import android.graphics.Paint;
     26 import android.graphics.drawable.Icon;
     27 import android.os.AsyncTask;
     28 import android.os.VibrationEffect;
     29 import android.os.Vibrator;
     30 import android.support.v4.util.ArrayMap;
     31 import android.support.v4.util.ArraySet;
     32 import android.util.AttributeSet;
     33 import android.view.View;
     34 
     35 import com.android.internal.statusbar.StatusBarIcon;
     36 import com.android.systemui.Interpolators;
     37 import com.android.systemui.R;
     38 import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
     39 import com.android.systemui.statusbar.StatusBarIconView;
     40 import com.android.systemui.statusbar.stack.AnimationFilter;
     41 import com.android.systemui.statusbar.stack.AnimationProperties;
     42 import com.android.systemui.statusbar.stack.ViewState;
     43 
     44 import java.util.ArrayList;
     45 import java.util.HashMap;
     46 
     47 /**
     48  * A container for notification icons. It handles overflowing icons properly and positions them
     49  * correctly on the screen.
     50  */
     51 public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
     52     /**
     53      * A float value indicating how much before the overflow start the icons should transform into
     54      * a dot. A value of 0 means that they are exactly at the end and a value of 1 means it starts
     55      * 1 icon width early.
     56      */
     57     public static final float OVERFLOW_EARLY_AMOUNT = 0.2f;
     58     private static final int NO_VALUE = Integer.MIN_VALUE;
     59     private static final String TAG = "NotificationIconContainer";
     60     private static final boolean DEBUG = false;
     61     private static final int CANNED_ANIMATION_DURATION = 100;
     62     private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
     63         private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
     64 
     65         @Override
     66         public AnimationFilter getAnimationFilter() {
     67             return mAnimationFilter;
     68         }
     69     }.setDuration(200);
     70 
     71     private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() {
     72         private AnimationFilter mAnimationFilter = new AnimationFilter().animateY().animateAlpha()
     73                 .animateScale();
     74 
     75         @Override
     76         public AnimationFilter getAnimationFilter() {
     77             return mAnimationFilter;
     78         }
     79 
     80     }.setDuration(CANNED_ANIMATION_DURATION)
     81             .setCustomInterpolator(View.TRANSLATION_Y, Interpolators.ICON_OVERSHOT);
     82 
     83     /**
     84      * Temporary AnimationProperties to avoid unnecessary allocations.
     85      */
     86     private static final AnimationProperties sTempProperties = new AnimationProperties() {
     87         private AnimationFilter mAnimationFilter = new AnimationFilter();
     88 
     89         @Override
     90         public AnimationFilter getAnimationFilter() {
     91             return mAnimationFilter;
     92         }
     93     };
     94 
     95     private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
     96         private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
     97 
     98         @Override
     99         public AnimationFilter getAnimationFilter() {
    100             return mAnimationFilter;
    101         }
    102     }.setDuration(200).setDelay(50);
    103 
    104     public static final int MAX_VISIBLE_ICONS_WHEN_DARK = 5;
    105 
    106     private boolean mShowAllIcons = true;
    107     private final HashMap<View, IconState> mIconStates = new HashMap<>();
    108     private int mDotPadding;
    109     private int mStaticDotRadius;
    110     private int mActualLayoutWidth = NO_VALUE;
    111     private float mActualPaddingEnd = NO_VALUE;
    112     private float mActualPaddingStart = NO_VALUE;
    113     private boolean mDark;
    114     private boolean mChangingViewPositions;
    115     private int mAddAnimationStartIndex = -1;
    116     private int mCannedAnimationStartIndex = -1;
    117     private int mSpeedBumpIndex = -1;
    118     private int mIconSize;
    119     private float mOpenedAmount = 0.0f;
    120     private float mVisualOverflowAdaption;
    121     private boolean mDisallowNextAnimation;
    122     private boolean mAnimationsEnabled = true;
    123     private boolean mVibrateOnAnimation;
    124     private Vibrator mVibrator;
    125     private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons;
    126     private int mDarkOffsetX;
    127 
    128     public NotificationIconContainer(Context context, AttributeSet attrs) {
    129         super(context, attrs);
    130         initDimens();
    131         setWillNotDraw(!DEBUG);
    132         mVibrator = mContext.getSystemService(Vibrator.class);
    133     }
    134 
    135     private void initDimens() {
    136         mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
    137         mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
    138     }
    139 
    140     @Override
    141     protected void onDraw(Canvas canvas) {
    142         super.onDraw(canvas);
    143         Paint paint = new Paint();
    144         paint.setColor(Color.RED);
    145         paint.setStyle(Paint.Style.STROKE);
    146         canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint);
    147     }
    148 
    149     @Override
    150     protected void onConfigurationChanged(Configuration newConfig) {
    151         super.onConfigurationChanged(newConfig);
    152         initDimens();
    153     }
    154     @Override
    155     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    156         float centerY = getHeight() / 2.0f;
    157         // we layout all our children on the left at the top
    158         mIconSize = 0;
    159         for (int i = 0; i < getChildCount(); i++) {
    160             View child = getChildAt(i);
    161             // We need to layout all children even the GONE ones, such that the heights are
    162             // calculated correctly as they are used to calculate how many we can fit on the screen
    163             int width = child.getMeasuredWidth();
    164             int height = child.getMeasuredHeight();
    165             int top = (int) (centerY - height / 2.0f);
    166             child.layout(0, top, width, top + height);
    167             if (i == 0) {
    168                 mIconSize = child.getWidth();
    169             }
    170         }
    171         if (mShowAllIcons) {
    172             resetViewStates();
    173             calculateIconTranslations();
    174             applyIconStates();
    175         }
    176     }
    177 
    178     public void applyIconStates() {
    179         for (int i = 0; i < getChildCount(); i++) {
    180             View child = getChildAt(i);
    181             ViewState childState = mIconStates.get(child);
    182             if (childState != null) {
    183                 childState.applyToView(child);
    184             }
    185         }
    186         mAddAnimationStartIndex = -1;
    187         mCannedAnimationStartIndex = -1;
    188         mDisallowNextAnimation = false;
    189     }
    190 
    191     @Override
    192     public void onViewAdded(View child) {
    193         super.onViewAdded(child);
    194         boolean isReplacingIcon = isReplacingIcon(child);
    195         if (!mChangingViewPositions) {
    196             IconState v = new IconState();
    197             if (isReplacingIcon) {
    198                 v.justAdded = false;
    199                 v.justReplaced = true;
    200             }
    201             mIconStates.put(child, v);
    202         }
    203         int childIndex = indexOfChild(child);
    204         if (childIndex < getChildCount() - 1 && !isReplacingIcon
    205             && mIconStates.get(getChildAt(childIndex + 1)).iconAppearAmount > 0.0f) {
    206             if (mAddAnimationStartIndex < 0) {
    207                 mAddAnimationStartIndex = childIndex;
    208             } else {
    209                 mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, childIndex);
    210             }
    211         }
    212         if (mDark && child instanceof StatusBarIconView) {
    213             ((StatusBarIconView) child).setDark(mDark, false, 0);
    214         }
    215     }
    216 
    217     private boolean isReplacingIcon(View child) {
    218         if (mReplacingIcons == null) {
    219             return false;
    220         }
    221         if (!(child instanceof StatusBarIconView)) {
    222             return false;
    223         }
    224         StatusBarIconView iconView = (StatusBarIconView) child;
    225         Icon sourceIcon = iconView.getSourceIcon();
    226         String groupKey = iconView.getNotification().getGroupKey();
    227         ArrayList<StatusBarIcon> statusBarIcons = mReplacingIcons.get(groupKey);
    228         if (statusBarIcons != null) {
    229             StatusBarIcon replacedIcon = statusBarIcons.get(0);
    230             if (sourceIcon.sameAs(replacedIcon.icon)) {
    231                 return true;
    232             }
    233         }
    234         return false;
    235     }
    236 
    237     @Override
    238     public void onViewRemoved(View child) {
    239         super.onViewRemoved(child);
    240         if (child instanceof StatusBarIconView) {
    241             boolean isReplacingIcon = isReplacingIcon(child);
    242             final StatusBarIconView icon = (StatusBarIconView) child;
    243             if (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
    244                     && child.getVisibility() == VISIBLE && isReplacingIcon) {
    245                 int animationStartIndex = findFirstViewIndexAfter(icon.getTranslationX());
    246                 if (mAddAnimationStartIndex < 0) {
    247                     mAddAnimationStartIndex = animationStartIndex;
    248                 } else {
    249                     mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, animationStartIndex);
    250                 }
    251             }
    252             if (!mChangingViewPositions) {
    253                 mIconStates.remove(child);
    254                 if (!isReplacingIcon) {
    255                     addTransientView(icon, 0);
    256                     icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */,
    257                             () -> removeTransientView(icon));
    258                 }
    259             }
    260         }
    261     }
    262 
    263     /**
    264      * Finds the first view with a translation bigger then a given value
    265      */
    266     private int findFirstViewIndexAfter(float translationX) {
    267         for (int i = 0; i < getChildCount(); i++) {
    268             View view = getChildAt(i);
    269             if (view.getTranslationX() > translationX) {
    270                 return i;
    271             }
    272         }
    273         return getChildCount();
    274     }
    275 
    276     public void resetViewStates() {
    277         for (int i = 0; i < getChildCount(); i++) {
    278             View view = getChildAt(i);
    279             ViewState iconState = mIconStates.get(view);
    280             iconState.initFrom(view);
    281             iconState.alpha = 1.0f;
    282             iconState.hidden = false;
    283         }
    284     }
    285 
    286     /**
    287      * Calulate the horizontal translations for each notification based on how much the icons
    288      * are inserted into the notification container.
    289      * If this is not a whole number, the fraction means by how much the icon is appearing.
    290      */
    291     public void calculateIconTranslations() {
    292         float translationX = getActualPaddingStart();
    293         int firstOverflowIndex = -1;
    294         int childCount = getChildCount();
    295         int maxVisibleIcons = mDark ? MAX_VISIBLE_ICONS_WHEN_DARK : childCount;
    296         float layoutEnd = getLayoutEnd();
    297         float overflowStart = layoutEnd - mIconSize * (2 + OVERFLOW_EARLY_AMOUNT);
    298         boolean hasAmbient = mSpeedBumpIndex != -1 && mSpeedBumpIndex < getChildCount();
    299         float visualOverflowStart = 0;
    300         for (int i = 0; i < childCount; i++) {
    301             View view = getChildAt(i);
    302             IconState iconState = mIconStates.get(view);
    303             iconState.xTranslation = translationX;
    304             boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
    305                     && iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
    306             boolean noOverflowAfter = i == childCount - 1;
    307             float drawingScale = mDark && view instanceof StatusBarIconView
    308                     ? ((StatusBarIconView) view).getIconScaleFullyDark()
    309                     : 1f;
    310             if (mOpenedAmount != 0.0f) {
    311                 noOverflowAfter = noOverflowAfter && !hasAmbient && !forceOverflow;
    312             }
    313             iconState.visibleState = StatusBarIconView.STATE_ICON;
    314             if (firstOverflowIndex == -1 && (forceOverflow
    315                     || (translationX >= (noOverflowAfter ? layoutEnd - mIconSize : overflowStart)))) {
    316                 firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i;
    317                 int totalDotLength = mStaticDotRadius * 6 + 2 * mDotPadding;
    318                 visualOverflowStart = overflowStart + mIconSize * (1 + OVERFLOW_EARLY_AMOUNT)
    319                         - totalDotLength / 2
    320                         - mIconSize * 0.5f + mStaticDotRadius;
    321                 if (forceOverflow) {
    322                     visualOverflowStart = Math.min(translationX, visualOverflowStart
    323                             + mStaticDotRadius * 2 + mDotPadding);
    324                 } else {
    325                     visualOverflowStart += (translationX - overflowStart) / mIconSize
    326                             * (mStaticDotRadius * 2 + mDotPadding);
    327                 }
    328                 if (mShowAllIcons) {
    329                     // We want to perfectly position the overflow in the static state, such that
    330                     // it's perfectly centered instead of measuring it from the end.
    331                     mVisualOverflowAdaption = 0;
    332                     if (firstOverflowIndex != -1) {
    333                         View firstOverflowView = getChildAt(i);
    334                         IconState overflowState = mIconStates.get(firstOverflowView);
    335                         float totalAmount = layoutEnd - overflowState.xTranslation;
    336                         float newPosition = overflowState.xTranslation + totalAmount / 2
    337                                 - totalDotLength / 2
    338                                 - mIconSize * 0.5f + mStaticDotRadius;
    339                         mVisualOverflowAdaption = newPosition - visualOverflowStart;
    340                         visualOverflowStart = newPosition;
    341                     }
    342                 } else {
    343                     visualOverflowStart += mVisualOverflowAdaption * (1f - mOpenedAmount);
    344                 }
    345             }
    346             translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
    347         }
    348         if (firstOverflowIndex != -1) {
    349             int numDots = 1;
    350             translationX = visualOverflowStart;
    351             for (int i = firstOverflowIndex; i < childCount; i++) {
    352                 View view = getChildAt(i);
    353                 IconState iconState = mIconStates.get(view);
    354                 int dotWidth = mStaticDotRadius * 2 + mDotPadding;
    355                 iconState.xTranslation = translationX;
    356                 if (numDots <= 3) {
    357                     if (numDots == 1 && iconState.iconAppearAmount < 0.8f) {
    358                         iconState.visibleState = StatusBarIconView.STATE_ICON;
    359                         numDots--;
    360                     } else {
    361                         iconState.visibleState = StatusBarIconView.STATE_DOT;
    362                     }
    363                     translationX += (numDots == 3 ? 3 * dotWidth : dotWidth)
    364                             * iconState.iconAppearAmount;
    365                 } else {
    366                     iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
    367                 }
    368                 numDots++;
    369             }
    370         }
    371         boolean center = mDark;
    372         if (center && translationX < getLayoutEnd()) {
    373             float delta = (getLayoutEnd() - translationX) / 2;
    374             if (firstOverflowIndex != -1) {
    375                 // If we have an overflow, only count those half for centering because the dots
    376                 // don't have a lot of visual weight.
    377                 float deltaIgnoringOverflow = (getLayoutEnd() - visualOverflowStart) / 2;
    378                 delta = (deltaIgnoringOverflow + delta) / 2;
    379             }
    380             for (int i = 0; i < childCount; i++) {
    381                 View view = getChildAt(i);
    382                 IconState iconState = mIconStates.get(view);
    383                 iconState.xTranslation += delta;
    384             }
    385         }
    386 
    387         if (isLayoutRtl()) {
    388             for (int i = 0; i < childCount; i++) {
    389                 View view = getChildAt(i);
    390                 IconState iconState = mIconStates.get(view);
    391                 iconState.xTranslation = getWidth() - iconState.xTranslation - view.getWidth();
    392             }
    393         }
    394 
    395         if (mDark && mDarkOffsetX != 0) {
    396             for (int i = 0; i < childCount; i++) {
    397                 View view = getChildAt(i);
    398                 IconState iconState = mIconStates.get(view);
    399                 iconState.xTranslation += mDarkOffsetX;
    400             }
    401         }
    402     }
    403 
    404     private float getLayoutEnd() {
    405         return getActualWidth() - getActualPaddingEnd();
    406     }
    407 
    408     private float getActualPaddingEnd() {
    409         if (mActualPaddingEnd == NO_VALUE) {
    410             return getPaddingEnd();
    411         }
    412         return mActualPaddingEnd;
    413     }
    414 
    415     private float getActualPaddingStart() {
    416         if (mActualPaddingStart == NO_VALUE) {
    417             return getPaddingStart();
    418         }
    419         return mActualPaddingStart;
    420     }
    421 
    422     /**
    423      * Sets whether the layout should always show all icons.
    424      * If this is true, the icon positions will be updated on layout.
    425      * If this if false, the layout is managed from the outside and layouting won't trigger a
    426      * repositioning of the icons.
    427      */
    428     public void setShowAllIcons(boolean showAllIcons) {
    429         mShowAllIcons = showAllIcons;
    430     }
    431 
    432     public void setActualLayoutWidth(int actualLayoutWidth) {
    433         mActualLayoutWidth = actualLayoutWidth;
    434         if (DEBUG) {
    435             invalidate();
    436         }
    437     }
    438 
    439     public void setActualPaddingEnd(float paddingEnd) {
    440         mActualPaddingEnd = paddingEnd;
    441         if (DEBUG) {
    442             invalidate();
    443         }
    444     }
    445 
    446     public void setActualPaddingStart(float paddingStart) {
    447         mActualPaddingStart = paddingStart;
    448         if (DEBUG) {
    449             invalidate();
    450         }
    451     }
    452 
    453     public int getActualWidth() {
    454         if (mActualLayoutWidth == NO_VALUE) {
    455             return getWidth();
    456         }
    457         return mActualLayoutWidth;
    458     }
    459 
    460     public void setChangingViewPositions(boolean changingViewPositions) {
    461         mChangingViewPositions = changingViewPositions;
    462     }
    463 
    464     public void setDark(boolean dark, boolean fade, long delay) {
    465         mDark = dark;
    466         mDisallowNextAnimation |= !fade;
    467         for (int i = 0; i < getChildCount(); i++) {
    468             View view = getChildAt(i);
    469             if (view instanceof StatusBarIconView) {
    470                 ((StatusBarIconView) view).setDark(dark, fade, delay);
    471             }
    472         }
    473     }
    474 
    475     public IconState getIconState(StatusBarIconView icon) {
    476         return mIconStates.get(icon);
    477     }
    478 
    479     public void setSpeedBumpIndex(int speedBumpIndex) {
    480         mSpeedBumpIndex = speedBumpIndex;
    481     }
    482 
    483     public void setOpenedAmount(float expandAmount) {
    484         mOpenedAmount = expandAmount;
    485     }
    486 
    487     public float getVisualOverflowAdaption() {
    488         return mVisualOverflowAdaption;
    489     }
    490 
    491     public void setVisualOverflowAdaption(float visualOverflowAdaption) {
    492         mVisualOverflowAdaption = visualOverflowAdaption;
    493     }
    494 
    495     public boolean hasOverflow() {
    496         float width = (getChildCount() + OVERFLOW_EARLY_AMOUNT) * mIconSize;
    497         return width - (getWidth() - getActualPaddingStart() - getActualPaddingEnd()) > 0;
    498     }
    499 
    500     public void setVibrateOnAnimation(boolean vibrateOnAnimation) {
    501         mVibrateOnAnimation = vibrateOnAnimation;
    502     }
    503 
    504     public int getIconSize() {
    505         return mIconSize;
    506     }
    507 
    508     public void setAnimationsEnabled(boolean enabled) {
    509         if (!enabled && mAnimationsEnabled) {
    510             for (int i = 0; i < getChildCount(); i++) {
    511                 View child = getChildAt(i);
    512                 ViewState childState = mIconStates.get(child);
    513                 if (childState != null) {
    514                     childState.cancelAnimations(child);
    515                     childState.applyToView(child);
    516                 }
    517             }
    518         }
    519         mAnimationsEnabled = enabled;
    520     }
    521 
    522     public void setDarkOffsetX(int offsetX) {
    523         mDarkOffsetX = offsetX;
    524     }
    525 
    526     public void setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
    527         mReplacingIcons = replacingIcons;
    528     }
    529 
    530     public class IconState extends ViewState {
    531         public static final int NO_VALUE = NotificationIconContainer.NO_VALUE;
    532         public float iconAppearAmount = 1.0f;
    533         public float clampedAppearAmount = 1.0f;
    534         public int visibleState;
    535         public boolean justAdded = true;
    536         private boolean justReplaced;
    537         public boolean needsCannedAnimation;
    538         public boolean useFullTransitionAmount;
    539         public boolean useLinearTransitionAmount;
    540         public boolean translateContent;
    541         public int iconColor = StatusBarIconView.NO_COLOR;
    542         public boolean noAnimations;
    543         public boolean isLastExpandIcon;
    544         public int customTransformHeight = NO_VALUE;
    545 
    546         @Override
    547         public void applyToView(View view) {
    548             if (view instanceof StatusBarIconView) {
    549                 StatusBarIconView icon = (StatusBarIconView) view;
    550                 boolean animate = false;
    551                 AnimationProperties animationProperties = null;
    552                 boolean animationsAllowed = mAnimationsEnabled && !mDisallowNextAnimation
    553                         && !noAnimations;
    554                 if (animationsAllowed) {
    555                     if (justAdded || justReplaced) {
    556                         super.applyToView(icon);
    557                         if (justAdded && iconAppearAmount != 0.0f) {
    558                             icon.setAlpha(0.0f);
    559                             icon.setVisibleState(StatusBarIconView.STATE_HIDDEN,
    560                                     false /* animate */);
    561                             animationProperties = ADD_ICON_PROPERTIES;
    562                             animate = true;
    563                         }
    564                     } else if (visibleState != icon.getVisibleState()) {
    565                         animationProperties = DOT_ANIMATION_PROPERTIES;
    566                         animate = true;
    567                     }
    568                     if (!animate && mAddAnimationStartIndex >= 0
    569                             && indexOfChild(view) >= mAddAnimationStartIndex
    570                             && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
    571                             || visibleState != StatusBarIconView.STATE_HIDDEN)) {
    572                         animationProperties = DOT_ANIMATION_PROPERTIES;
    573                         animate = true;
    574                     }
    575                     if (needsCannedAnimation) {
    576                         AnimationFilter animationFilter = sTempProperties.getAnimationFilter();
    577                         animationFilter.reset();
    578                         animationFilter.combineFilter(
    579                                 ICON_ANIMATION_PROPERTIES.getAnimationFilter());
    580                         sTempProperties.resetCustomInterpolators();
    581                         sTempProperties.combineCustomInterpolators(ICON_ANIMATION_PROPERTIES);
    582                         if (animationProperties != null) {
    583                             animationFilter.combineFilter(animationProperties.getAnimationFilter());
    584                             sTempProperties.combineCustomInterpolators(animationProperties);
    585                         }
    586                         animationProperties = sTempProperties;
    587                         animationProperties.setDuration(CANNED_ANIMATION_DURATION);
    588                         animate = true;
    589                         mCannedAnimationStartIndex = indexOfChild(view);
    590                     }
    591                     if (!animate && mCannedAnimationStartIndex >= 0
    592                             && indexOfChild(view) > mCannedAnimationStartIndex
    593                             && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
    594                             || visibleState != StatusBarIconView.STATE_HIDDEN)) {
    595                         AnimationFilter animationFilter = sTempProperties.getAnimationFilter();
    596                         animationFilter.reset();
    597                         animationFilter.animateX();
    598                         sTempProperties.resetCustomInterpolators();
    599                         animationProperties = sTempProperties;
    600                         animationProperties.setDuration(CANNED_ANIMATION_DURATION);
    601                         animate = true;
    602                     }
    603                 }
    604                 icon.setVisibleState(visibleState, animationsAllowed);
    605                 icon.setIconColor(iconColor, needsCannedAnimation && animationsAllowed);
    606                 if (animate) {
    607                     animateTo(icon, animationProperties);
    608                 } else {
    609                     super.applyToView(view);
    610                 }
    611                 boolean wasInShelf = icon.isInShelf();
    612                 boolean inShelf = iconAppearAmount == 1.0f;
    613                 icon.setIsInShelf(inShelf);
    614                 if (shouldVibrateChange(wasInShelf != inShelf)) {
    615                     AsyncTask.execute(
    616                             () -> mVibrator.vibrate(VibrationEffect.get(
    617                                     VibrationEffect.EFFECT_TICK)));
    618                 }
    619             }
    620             justAdded = false;
    621             justReplaced = false;
    622             needsCannedAnimation = false;
    623         }
    624 
    625         private boolean shouldVibrateChange(boolean inShelfChanged) {
    626             if (!mVibrateOnAnimation) {
    627                 return false;
    628             }
    629             if (justAdded) {
    630                 return false;
    631             }
    632             if (!mAnimationsEnabled) {
    633                 return false;
    634             }
    635             if (!inShelfChanged) {
    636                 return false;
    637             }
    638             if (isHapticFeedbackDisabled(mContext)) {
    639                 return false;
    640             }
    641             return true;
    642         }
    643 
    644         public boolean hasCustomTransformHeight() {
    645             return isLastExpandIcon && customTransformHeight != NO_VALUE;
    646         }
    647 
    648         @Override
    649         public void initFrom(View view) {
    650             super.initFrom(view);
    651             if (view instanceof StatusBarIconView) {
    652                 iconColor = ((StatusBarIconView) view).getStaticDrawableColor();
    653             }
    654         }
    655     }
    656 }
    657