Home | History | Annotate | Download | only in stack
      1 /*
      2  * Copyright (C) 2015 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.stack;
     18 
     19 import android.app.Notification;
     20 import android.content.Context;
     21 import android.content.res.Configuration;
     22 import android.graphics.drawable.ColorDrawable;
     23 import android.service.notification.StatusBarNotification;
     24 import android.util.AttributeSet;
     25 import android.view.LayoutInflater;
     26 import android.view.NotificationHeaderView;
     27 import android.view.View;
     28 import android.view.ViewGroup;
     29 import android.widget.RemoteViews;
     30 import android.widget.TextView;
     31 
     32 import com.android.systemui.R;
     33 import com.android.systemui.ViewInvertHelper;
     34 import com.android.systemui.statusbar.CrossFadeHelper;
     35 import com.android.systemui.statusbar.ExpandableNotificationRow;
     36 import com.android.systemui.statusbar.NotificationHeaderUtil;
     37 import com.android.systemui.statusbar.notification.HybridGroupManager;
     38 import com.android.systemui.statusbar.notification.HybridNotificationView;
     39 import com.android.systemui.statusbar.notification.NotificationUtils;
     40 import com.android.systemui.statusbar.notification.NotificationViewWrapper;
     41 import com.android.systemui.statusbar.phone.NotificationPanelView;
     42 
     43 import java.util.ArrayList;
     44 import java.util.List;
     45 
     46 /**
     47  * A container containing child notifications
     48  */
     49 public class NotificationChildrenContainer extends ViewGroup {
     50 
     51     private static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2;
     52     private static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5;
     53     private static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8;
     54 
     55     private final List<View> mDividers = new ArrayList<>();
     56     private final List<ExpandableNotificationRow> mChildren = new ArrayList<>();
     57     private final HybridGroupManager mHybridGroupManager;
     58     private int mChildPadding;
     59     private int mDividerHeight;
     60     private int mMaxNotificationHeight;
     61     private int mNotificationHeaderMargin;
     62     private int mNotificatonTopPadding;
     63     private float mCollapsedBottompadding;
     64     private ViewInvertHelper mOverflowInvertHelper;
     65     private boolean mChildrenExpanded;
     66     private ExpandableNotificationRow mNotificationParent;
     67     private TextView mOverflowNumber;
     68     private ViewState mGroupOverFlowState;
     69     private int mRealHeight;
     70     private boolean mUserLocked;
     71     private int mActualHeight;
     72     private boolean mNeverAppliedGroupState;
     73     private int mHeaderHeight;
     74 
     75     private NotificationHeaderView mNotificationHeader;
     76     private NotificationViewWrapper mNotificationHeaderWrapper;
     77     private NotificationHeaderUtil mHeaderUtil;
     78     private ViewState mHeaderViewState;
     79 
     80     public NotificationChildrenContainer(Context context) {
     81         this(context, null);
     82     }
     83 
     84     public NotificationChildrenContainer(Context context, AttributeSet attrs) {
     85         this(context, attrs, 0);
     86     }
     87 
     88     public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) {
     89         this(context, attrs, defStyleAttr, 0);
     90     }
     91 
     92     public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
     93             int defStyleRes) {
     94         super(context, attrs, defStyleAttr, defStyleRes);
     95         initDimens();
     96         mHybridGroupManager = new HybridGroupManager(getContext(), this);
     97     }
     98 
     99     private void initDimens() {
    100         mChildPadding = getResources().getDimensionPixelSize(
    101                 R.dimen.notification_children_padding);
    102         mDividerHeight = Math.max(1, getResources().getDimensionPixelSize(
    103                 R.dimen.notification_divider_height));
    104         mHeaderHeight = getResources().getDimensionPixelSize(R.dimen.notification_header_height);
    105         mMaxNotificationHeight = getResources().getDimensionPixelSize(
    106                 R.dimen.notification_max_height);
    107         mNotificationHeaderMargin = getResources().getDimensionPixelSize(
    108                 com.android.internal.R.dimen.notification_content_margin_top);
    109         mNotificatonTopPadding = getResources().getDimensionPixelSize(
    110                 R.dimen.notification_children_container_top_padding);
    111         mCollapsedBottompadding = getResources().getDimensionPixelSize(
    112                 com.android.internal.R.dimen.notification_content_margin_bottom);
    113     }
    114 
    115     @Override
    116     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    117         int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
    118         for (int i = 0; i < childCount; i++) {
    119             View child = mChildren.get(i);
    120             // We need to layout all children even the GONE ones, such that the heights are
    121             // calculated correctly as they are used to calculate how many we can fit on the screen
    122             child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
    123             mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight);
    124         }
    125         if (mOverflowNumber != null) {
    126             mOverflowNumber.layout(getWidth() - mOverflowNumber.getMeasuredWidth(), 0, getWidth(),
    127                     mOverflowNumber.getMeasuredHeight());
    128         }
    129         if (mNotificationHeader != null) {
    130             mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(),
    131                     mNotificationHeader.getMeasuredHeight());
    132         }
    133     }
    134 
    135     @Override
    136     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    137         int ownMaxHeight = mMaxNotificationHeight;
    138         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    139         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
    140         boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
    141         int size = MeasureSpec.getSize(heightMeasureSpec);
    142         if (hasFixedHeight || isHeightLimited) {
    143             ownMaxHeight = Math.min(ownMaxHeight, size);
    144         }
    145         int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
    146         int width = MeasureSpec.getSize(widthMeasureSpec);
    147         if (mOverflowNumber != null) {
    148             mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
    149                     newHeightSpec);
    150         }
    151         int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY);
    152         int height = mNotificationHeaderMargin + mNotificatonTopPadding;
    153         int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
    154         int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
    155         int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1;
    156         for (int i = 0; i < childCount; i++) {
    157             ExpandableNotificationRow child = mChildren.get(i);
    158             // We need to measure all children even the GONE ones, such that the heights are
    159             // calculated correctly as they are used to calculate how many we can fit on the screen.
    160             boolean isOverflow = i == overflowIndex;
    161             child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null
    162                     ? mOverflowNumber.getMeasuredWidth()
    163                     : 0);
    164             child.measure(widthMeasureSpec, newHeightSpec);
    165             // layout the divider
    166             View divider = mDividers.get(i);
    167             divider.measure(widthMeasureSpec, dividerHeightSpec);
    168             if (child.getVisibility() != GONE) {
    169                 height += child.getMeasuredHeight() + mDividerHeight;
    170             }
    171         }
    172         mRealHeight = height;
    173         if (heightMode != MeasureSpec.UNSPECIFIED) {
    174             height = Math.min(height, size);
    175         }
    176 
    177         if (mNotificationHeader != null) {
    178             int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
    179             mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec);
    180         }
    181 
    182         setMeasuredDimension(width, height);
    183     }
    184 
    185     @Override
    186     public boolean pointInView(float localX, float localY, float slop) {
    187         return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
    188                 localY < (mRealHeight + slop);
    189     }
    190 
    191     /**
    192      * Add a child notification to this view.
    193      *
    194      * @param row the row to add
    195      * @param childIndex the index to add it at, if -1 it will be added at the end
    196      */
    197     public void addNotification(ExpandableNotificationRow row, int childIndex) {
    198         int newIndex = childIndex < 0 ? mChildren.size() : childIndex;
    199         mChildren.add(newIndex, row);
    200         addView(row);
    201         row.setUserLocked(mUserLocked);
    202 
    203         View divider = inflateDivider();
    204         addView(divider);
    205         mDividers.add(newIndex, divider);
    206 
    207         updateGroupOverflow();
    208     }
    209 
    210     public void removeNotification(ExpandableNotificationRow row) {
    211         int childIndex = mChildren.indexOf(row);
    212         mChildren.remove(row);
    213         removeView(row);
    214 
    215         final View divider = mDividers.remove(childIndex);
    216         removeView(divider);
    217         getOverlay().add(divider);
    218         CrossFadeHelper.fadeOut(divider, new Runnable() {
    219             @Override
    220             public void run() {
    221                 getOverlay().remove(divider);
    222             }
    223         });
    224 
    225         row.setSystemChildExpanded(false);
    226         row.setUserLocked(false);
    227         updateGroupOverflow();
    228         if (!row.isRemoved()) {
    229             mHeaderUtil.restoreNotificationHeader(row);
    230         }
    231     }
    232 
    233     /**
    234      * @return The number of notification children in the container.
    235      */
    236     public int getNotificationChildCount() {
    237         return mChildren.size();
    238     }
    239 
    240     public void recreateNotificationHeader(OnClickListener listener, StatusBarNotification notification) {
    241         final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(),
    242                 mNotificationParent.getStatusBarNotification().getNotification());
    243         final RemoteViews header = builder.makeNotificationHeader();
    244         if (mNotificationHeader == null) {
    245             mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this);
    246             final View expandButton = mNotificationHeader.findViewById(
    247                     com.android.internal.R.id.expand_button);
    248             expandButton.setVisibility(VISIBLE);
    249             mNotificationHeader.setOnClickListener(listener);
    250             mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
    251                     mNotificationHeader, mNotificationParent);
    252             addView(mNotificationHeader, 0);
    253             invalidate();
    254         } else {
    255             header.reapply(getContext(), mNotificationHeader);
    256             mNotificationHeaderWrapper.notifyContentUpdated(notification);
    257         }
    258         updateChildrenHeaderAppearance();
    259     }
    260 
    261     public void updateChildrenHeaderAppearance() {
    262         mHeaderUtil.updateChildrenHeaderAppearance();
    263     }
    264 
    265     public void updateGroupOverflow() {
    266         int childCount = mChildren.size();
    267         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
    268         if (childCount > maxAllowedVisibleChildren) {
    269             mOverflowNumber = mHybridGroupManager.bindOverflowNumber(
    270                     mOverflowNumber, childCount - maxAllowedVisibleChildren);
    271             if (mOverflowInvertHelper == null) {
    272                 mOverflowInvertHelper = new ViewInvertHelper(mOverflowNumber,
    273                         NotificationPanelView.DOZE_ANIMATION_DURATION);
    274             }
    275             if (mGroupOverFlowState == null) {
    276                 mGroupOverFlowState = new ViewState();
    277                 mNeverAppliedGroupState = true;
    278             }
    279         } else if (mOverflowNumber != null) {
    280             removeView(mOverflowNumber);
    281             if (isShown()) {
    282                 final View removedOverflowNumber = mOverflowNumber;
    283                 addTransientView(removedOverflowNumber, getTransientViewCount());
    284                 CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() {
    285                     @Override
    286                     public void run() {
    287                         removeTransientView(removedOverflowNumber);
    288                     }
    289                 });
    290             }
    291             mOverflowNumber = null;
    292             mOverflowInvertHelper = null;
    293             mGroupOverFlowState = null;
    294         }
    295     }
    296 
    297     @Override
    298     protected void onConfigurationChanged(Configuration newConfig) {
    299         super.onConfigurationChanged(newConfig);
    300         updateGroupOverflow();
    301     }
    302 
    303     private View inflateDivider() {
    304         return LayoutInflater.from(mContext).inflate(
    305                 R.layout.notification_children_divider, this, false);
    306     }
    307 
    308     public List<ExpandableNotificationRow> getNotificationChildren() {
    309         return mChildren;
    310     }
    311 
    312     /**
    313      * Apply the order given in the list to the children.
    314      *
    315      * @param childOrder the new list order
    316      * @return whether the list order has changed
    317      */
    318     public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
    319         if (childOrder == null) {
    320             return false;
    321         }
    322         boolean result = false;
    323         for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) {
    324             ExpandableNotificationRow child = mChildren.get(i);
    325             ExpandableNotificationRow desiredChild = childOrder.get(i);
    326             if (child != desiredChild) {
    327                 mChildren.remove(desiredChild);
    328                 mChildren.add(i, desiredChild);
    329                 result = true;
    330             }
    331         }
    332         updateExpansionStates();
    333         return result;
    334     }
    335 
    336     private void updateExpansionStates() {
    337         if (mChildrenExpanded || mUserLocked) {
    338             // we don't modify it the group is expanded or if we are expanding it
    339             return;
    340         }
    341         int size = mChildren.size();
    342         for (int i = 0; i < size; i++) {
    343             ExpandableNotificationRow child = mChildren.get(i);
    344             child.setSystemChildExpanded(i == 0 && size == 1);
    345         }
    346     }
    347 
    348     /**
    349      *
    350      * @return the intrinsic size of this children container, i.e the natural fully expanded state
    351      */
    352     public int getIntrinsicHeight() {
    353         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
    354         return getIntrinsicHeight(maxAllowedVisibleChildren);
    355     }
    356 
    357     /**
    358      * @return the intrinsic height with a number of children given
    359      *         in @param maxAllowedVisibleChildren
    360      */
    361     private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
    362         int intrinsicHeight = mNotificationHeaderMargin;
    363         int visibleChildren = 0;
    364         int childCount = mChildren.size();
    365         boolean firstChild = true;
    366         float expandFactor = 0;
    367         if (mUserLocked) {
    368             expandFactor = getGroupExpandFraction();
    369         }
    370         for (int i = 0; i < childCount; i++) {
    371             if (visibleChildren >= maxAllowedVisibleChildren) {
    372                 break;
    373             }
    374             if (!firstChild) {
    375                 if (mUserLocked) {
    376                     intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
    377                             expandFactor);
    378                 } else {
    379                     intrinsicHeight += mChildrenExpanded ? mDividerHeight : mChildPadding;
    380                 }
    381             } else {
    382                 if (mUserLocked) {
    383                     intrinsicHeight += NotificationUtils.interpolate(
    384                             0,
    385                             mNotificatonTopPadding + mDividerHeight,
    386                             expandFactor);
    387                 } else {
    388                     intrinsicHeight += mChildrenExpanded
    389                             ? mNotificatonTopPadding + mDividerHeight
    390                             : 0;
    391                 }
    392                 firstChild = false;
    393             }
    394             ExpandableNotificationRow child = mChildren.get(i);
    395             intrinsicHeight += child.getIntrinsicHeight();
    396             visibleChildren++;
    397         }
    398         if (mUserLocked) {
    399             intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottompadding, 0.0f,
    400                     expandFactor);
    401         } else if (!mChildrenExpanded) {
    402             intrinsicHeight += mCollapsedBottompadding;
    403         }
    404         return intrinsicHeight;
    405     }
    406 
    407     /**
    408      * Update the state of all its children based on a linear layout algorithm.
    409      *
    410      * @param resultState the state to update
    411      * @param parentState the state of the parent
    412      */
    413     public void getState(StackScrollState resultState, StackViewState parentState) {
    414         int childCount = mChildren.size();
    415         int yPosition = mNotificationHeaderMargin;
    416         boolean firstChild = true;
    417         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
    418         int lastVisibleIndex = maxAllowedVisibleChildren - 1;
    419         int firstOverflowIndex = lastVisibleIndex + 1;
    420         float expandFactor = 0;
    421         if (mUserLocked) {
    422             expandFactor = getGroupExpandFraction();
    423             firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
    424         }
    425 
    426         boolean childrenExpanded = !mNotificationParent.isGroupExpansionChanging()
    427                 && mChildrenExpanded;
    428         int parentHeight = parentState.height;
    429         for (int i = 0; i < childCount; i++) {
    430             ExpandableNotificationRow child = mChildren.get(i);
    431             if (!firstChild) {
    432                 if (mUserLocked) {
    433                     yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
    434                             expandFactor);
    435                 } else {
    436                     yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding;
    437                 }
    438             } else {
    439                 if (mUserLocked) {
    440                     yPosition += NotificationUtils.interpolate(
    441                             0,
    442                             mNotificatonTopPadding + mDividerHeight,
    443                             expandFactor);
    444                 } else {
    445                     yPosition += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0;
    446                 }
    447                 firstChild = false;
    448             }
    449 
    450             StackViewState childState = resultState.getViewStateForView(child);
    451             int intrinsicHeight = child.getIntrinsicHeight();
    452             if (childrenExpanded) {
    453                 // When a group is expanded and moving into bottom stack, the bottom visible child
    454                 // adjusts its height to move into it. Children after it are hidden.
    455                 if (updateChildStateForExpandedGroup(child, parentHeight, childState, yPosition)) {
    456                     // Clipping might be deactivated if the view is transforming, however, clipping
    457                     // the child into the bottom stack should take precedent over this.
    458                     childState.isBottomClipped = true;
    459                 }
    460             } else {
    461                 childState.hidden = false;
    462                 childState.height = intrinsicHeight;
    463                 childState.isBottomClipped = false;
    464             }
    465             childState.yTranslation = yPosition;
    466             // When the group is expanded, the children cast the shadows rather than the parent
    467             // so use the parent's elevation here.
    468             childState.zTranslation = childrenExpanded
    469                     ? mNotificationParent.getTranslationZ()
    470                     : 0;
    471             childState.dimmed = parentState.dimmed;
    472             childState.dark = parentState.dark;
    473             childState.hideSensitive = parentState.hideSensitive;
    474             childState.belowSpeedBump = parentState.belowSpeedBump;
    475             childState.clipTopAmount = 0;
    476             childState.alpha = 0;
    477             if (i < firstOverflowIndex) {
    478                 childState.alpha = 1;
    479             } else if (expandFactor == 1.0f && i <= lastVisibleIndex) {
    480                 childState.alpha = (mActualHeight - childState.yTranslation) / childState.height;
    481                 childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha));
    482             }
    483             childState.location = parentState.location;
    484             yPosition += intrinsicHeight;
    485         }
    486         if (mOverflowNumber != null) {
    487             ExpandableNotificationRow overflowView = mChildren.get(Math.min(
    488                     getMaxAllowedVisibleChildren(true /* likeCollpased */), childCount) - 1);
    489             mGroupOverFlowState.copyFrom(resultState.getViewStateForView(overflowView));
    490             if (!mChildrenExpanded) {
    491                 if (mUserLocked) {
    492                     HybridNotificationView singleLineView = overflowView.getSingleLineView();
    493                     View mirrorView = singleLineView.getTextView();
    494                     if (mirrorView.getVisibility() == GONE) {
    495                         mirrorView = singleLineView.getTitleView();
    496                     }
    497                     if (mirrorView.getVisibility() == GONE) {
    498                         mirrorView = singleLineView;
    499                     }
    500                     mGroupOverFlowState.yTranslation += NotificationUtils.getRelativeYOffset(
    501                             mirrorView, overflowView);
    502                     mGroupOverFlowState.alpha = mirrorView.getAlpha();
    503                 }
    504             } else {
    505                 mGroupOverFlowState.yTranslation += mNotificationHeaderMargin;
    506                 mGroupOverFlowState.alpha = 0.0f;
    507             }
    508         }
    509         if (mNotificationHeader != null) {
    510             if (mHeaderViewState == null) {
    511                 mHeaderViewState = new ViewState();
    512             }
    513             mHeaderViewState.initFrom(mNotificationHeader);
    514             mHeaderViewState.zTranslation = childrenExpanded
    515                     ? mNotificationParent.getTranslationZ()
    516                     : 0;
    517         }
    518     }
    519 
    520     /**
    521      * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
    522      * height, children in the group after this are gone.
    523      *
    524      * @param child the child who's height to adjust.
    525      * @param parentHeight the height of the parent.
    526      * @param childState the state to update.
    527      * @param yPosition the yPosition of the view.
    528      * @return true if children after this one should be hidden.
    529      */
    530     private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child,
    531             int parentHeight, StackViewState childState, int yPosition) {
    532         final int top = yPosition + child.getClipTopAmount();
    533         final int intrinsicHeight = child.getIntrinsicHeight();
    534         final int bottom = top + intrinsicHeight;
    535         int newHeight = intrinsicHeight;
    536         if (bottom >= parentHeight) {
    537             // Child is either clipped or gone
    538             newHeight = Math.max((parentHeight - top), 0);
    539         }
    540         childState.hidden = newHeight == 0;
    541         childState.height = newHeight;
    542         return childState.height != intrinsicHeight && !childState.hidden;
    543     }
    544 
    545     private int getMaxAllowedVisibleChildren() {
    546         return getMaxAllowedVisibleChildren(false /* likeCollapsed */);
    547     }
    548 
    549     private int getMaxAllowedVisibleChildren(boolean likeCollapsed) {
    550         if (!likeCollapsed && (mChildrenExpanded || mNotificationParent.isUserLocked())) {
    551             return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
    552         }
    553         if (!mNotificationParent.isOnKeyguard()
    554                 && (mNotificationParent.isExpanded() || mNotificationParent.isHeadsUp())) {
    555             return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
    556         }
    557         return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
    558     }
    559 
    560     public void applyState(StackScrollState state) {
    561         int childCount = mChildren.size();
    562         ViewState tmpState = new ViewState();
    563         float expandFraction = 0.0f;
    564         if (mUserLocked) {
    565             expandFraction = getGroupExpandFraction();
    566         }
    567         final boolean dividersVisible = mUserLocked
    568                 || mNotificationParent.isGroupExpansionChanging();
    569         for (int i = 0; i < childCount; i++) {
    570             ExpandableNotificationRow child = mChildren.get(i);
    571             StackViewState viewState = state.getViewStateForView(child);
    572             state.applyState(child, viewState);
    573 
    574             // layout the divider
    575             View divider = mDividers.get(i);
    576             tmpState.initFrom(divider);
    577             tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
    578             float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
    579             if (mUserLocked && viewState.alpha != 0) {
    580                 alpha = NotificationUtils.interpolate(0, 0.5f,
    581                         Math.min(viewState.alpha, expandFraction));
    582             }
    583             tmpState.hidden = !dividersVisible;
    584             tmpState.alpha = alpha;
    585             state.applyViewState(divider, tmpState);
    586             // There is no fake shadow to be drawn on the children
    587             child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
    588         }
    589         if (mOverflowNumber != null) {
    590             state.applyViewState(mOverflowNumber, mGroupOverFlowState);
    591             mNeverAppliedGroupState = false;
    592         }
    593         if (mNotificationHeader != null) {
    594             state.applyViewState(mNotificationHeader, mHeaderViewState);
    595         }
    596     }
    597 
    598     /**
    599      * This is called when the children expansion has changed and positions the children properly
    600      * for an appear animation.
    601      *
    602      * @param state the new state we animate to
    603      */
    604     public void prepareExpansionChanged(StackScrollState state) {
    605         // TODO: do something that makes sense, like placing the invisible views correctly
    606         return;
    607     }
    608 
    609     public void startAnimationToState(StackScrollState state, StackStateAnimator stateAnimator,
    610             long baseDelay, long duration) {
    611         int childCount = mChildren.size();
    612         ViewState tmpState = new ViewState();
    613         float expandFraction = getGroupExpandFraction();
    614         final boolean dividersVisible = mUserLocked
    615                 || mNotificationParent.isGroupExpansionChanging();
    616         for (int i = childCount - 1; i >= 0; i--) {
    617             ExpandableNotificationRow child = mChildren.get(i);
    618             StackViewState viewState = state.getViewStateForView(child);
    619             stateAnimator.startStackAnimations(child, viewState, state, -1, baseDelay);
    620 
    621             // layout the divider
    622             View divider = mDividers.get(i);
    623             tmpState.initFrom(divider);
    624             tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
    625             float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
    626             if (mUserLocked && viewState.alpha != 0) {
    627                 alpha = NotificationUtils.interpolate(0, 0.5f,
    628                         Math.min(viewState.alpha, expandFraction));
    629             }
    630             tmpState.hidden = !dividersVisible;
    631             tmpState.alpha = alpha;
    632             stateAnimator.startViewAnimations(divider, tmpState, baseDelay, duration);
    633             // There is no fake shadow to be drawn on the children
    634             child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
    635         }
    636         if (mOverflowNumber != null) {
    637             if (mNeverAppliedGroupState) {
    638                 float alpha = mGroupOverFlowState.alpha;
    639                 mGroupOverFlowState.alpha = 0;
    640                 state.applyViewState(mOverflowNumber, mGroupOverFlowState);
    641                 mGroupOverFlowState.alpha = alpha;
    642                 mNeverAppliedGroupState = false;
    643             }
    644             stateAnimator.startViewAnimations(mOverflowNumber, mGroupOverFlowState,
    645                     baseDelay, duration);
    646         }
    647         if (mNotificationHeader != null) {
    648             state.applyViewState(mNotificationHeader, mHeaderViewState);
    649         }
    650     }
    651 
    652     public ExpandableNotificationRow getViewAtPosition(float y) {
    653         // find the view under the pointer, accounting for GONE views
    654         final int count = mChildren.size();
    655         for (int childIdx = 0; childIdx < count; childIdx++) {
    656             ExpandableNotificationRow slidingChild = mChildren.get(childIdx);
    657             float childTop = slidingChild.getTranslationY();
    658             float top = childTop + slidingChild.getClipTopAmount();
    659             float bottom = childTop + slidingChild.getActualHeight();
    660             if (y >= top && y <= bottom) {
    661                 return slidingChild;
    662             }
    663         }
    664         return null;
    665     }
    666 
    667     public void setChildrenExpanded(boolean childrenExpanded) {
    668         mChildrenExpanded = childrenExpanded;
    669         updateExpansionStates();
    670         if (mNotificationHeader != null) {
    671             mNotificationHeader.setExpanded(childrenExpanded);
    672         }
    673         final int count = mChildren.size();
    674         for (int childIdx = 0; childIdx < count; childIdx++) {
    675             ExpandableNotificationRow child = mChildren.get(childIdx);
    676             child.setChildrenExpanded(childrenExpanded, false);
    677         }
    678     }
    679 
    680     public void setNotificationParent(ExpandableNotificationRow parent) {
    681         mNotificationParent = parent;
    682         mHeaderUtil = new NotificationHeaderUtil(mNotificationParent);
    683     }
    684 
    685     public ExpandableNotificationRow getNotificationParent() {
    686         return mNotificationParent;
    687     }
    688 
    689     public NotificationHeaderView getHeaderView() {
    690         return mNotificationHeader;
    691     }
    692 
    693     public void updateHeaderVisibility(int visiblity) {
    694         if (mNotificationHeader != null) {
    695             mNotificationHeader.setVisibility(visiblity);
    696         }
    697     }
    698 
    699     /**
    700      * Called when a groups expansion changes to adjust the background of the header view.
    701      *
    702      * @param expanded whether the group is expanded.
    703      */
    704     public void updateHeaderForExpansion(boolean expanded) {
    705         if (mNotificationHeader != null) {
    706             if (expanded) {
    707                 ColorDrawable cd = new ColorDrawable();
    708                 cd.setColor(mNotificationParent.calculateBgColor());
    709                 mNotificationHeader.setHeaderBackgroundDrawable(cd);
    710             } else {
    711                 mNotificationHeader.setHeaderBackgroundDrawable(null);
    712             }
    713         }
    714     }
    715 
    716     public int getMaxContentHeight() {
    717         int maxContentHeight = mNotificationHeaderMargin + mNotificatonTopPadding;
    718         int visibleChildren = 0;
    719         int childCount = mChildren.size();
    720         for (int i = 0; i < childCount; i++) {
    721             if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) {
    722                 break;
    723             }
    724             ExpandableNotificationRow child = mChildren.get(i);
    725             float childHeight = child.isExpanded(true /* allowOnKeyguard */)
    726                     ? child.getMaxExpandHeight()
    727                     : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
    728             maxContentHeight += childHeight;
    729             visibleChildren++;
    730         }
    731         if (visibleChildren > 0) {
    732             maxContentHeight += visibleChildren * mDividerHeight;
    733         }
    734         return maxContentHeight;
    735     }
    736 
    737     public void setActualHeight(int actualHeight) {
    738         if (!mUserLocked) {
    739             return;
    740         }
    741         mActualHeight = actualHeight;
    742         float fraction = getGroupExpandFraction();
    743         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
    744         int childCount = mChildren.size();
    745         for (int i = 0; i < childCount; i++) {
    746             ExpandableNotificationRow child = mChildren.get(i);
    747             float childHeight = child.isExpanded(true /* allowOnKeyguard */)
    748                     ? child.getMaxExpandHeight()
    749                     : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
    750             if (i < maxAllowedVisibleChildren) {
    751                 float singleLineHeight = child.getShowingLayout().getMinHeight(
    752                         false /* likeGroupExpanded */);
    753                 child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight,
    754                         childHeight, fraction), false);
    755             } else {
    756                 child.setActualHeight((int) childHeight, false);
    757             }
    758         }
    759     }
    760 
    761     public float getGroupExpandFraction() {
    762         int visibleChildrenExpandedHeight = getVisibleChildrenExpandHeight();
    763         int minExpandHeight = getCollapsedHeight();
    764         float factor = (mActualHeight - minExpandHeight)
    765                 / (float) (visibleChildrenExpandedHeight - minExpandHeight);
    766         return Math.max(0.0f, Math.min(1.0f, factor));
    767     }
    768 
    769     private int getVisibleChildrenExpandHeight() {
    770         int intrinsicHeight = mNotificationHeaderMargin + mNotificatonTopPadding + mDividerHeight;
    771         int visibleChildren = 0;
    772         int childCount = mChildren.size();
    773         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
    774         for (int i = 0; i < childCount; i++) {
    775             if (visibleChildren >= maxAllowedVisibleChildren) {
    776                 break;
    777             }
    778             ExpandableNotificationRow child = mChildren.get(i);
    779             float childHeight = child.isExpanded(true /* allowOnKeyguard */)
    780                     ? child.getMaxExpandHeight()
    781                     : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
    782             intrinsicHeight += childHeight;
    783             visibleChildren++;
    784         }
    785         return intrinsicHeight;
    786     }
    787 
    788     public int getMinHeight() {
    789         return getMinHeight(NUMBER_OF_CHILDREN_WHEN_COLLAPSED);
    790     }
    791 
    792     public int getCollapsedHeight() {
    793         return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */));
    794     }
    795 
    796     private int getMinHeight(int maxAllowedVisibleChildren) {
    797         int minExpandHeight = mNotificationHeaderMargin;
    798         int visibleChildren = 0;
    799         boolean firstChild = true;
    800         int childCount = mChildren.size();
    801         for (int i = 0; i < childCount; i++) {
    802             if (visibleChildren >= maxAllowedVisibleChildren) {
    803                 break;
    804             }
    805             if (!firstChild) {
    806                 minExpandHeight += mChildPadding;
    807             } else {
    808                 firstChild = false;
    809             }
    810             ExpandableNotificationRow child = mChildren.get(i);
    811             minExpandHeight += child.getSingleLineView().getHeight();
    812             visibleChildren++;
    813         }
    814         minExpandHeight += mCollapsedBottompadding;
    815         return minExpandHeight;
    816     }
    817 
    818     public void setDark(boolean dark, boolean fade, long delay) {
    819         if (mOverflowNumber != null) {
    820             mOverflowInvertHelper.setInverted(dark, fade, delay);
    821         }
    822         mNotificationHeaderWrapper.setDark(dark, fade, delay);
    823     }
    824 
    825     public void reInflateViews(OnClickListener listener, StatusBarNotification notification) {
    826         removeView(mNotificationHeader);
    827         mNotificationHeader = null;
    828         recreateNotificationHeader(listener, notification);
    829         initDimens();
    830         for (int i = 0; i < mDividers.size(); i++) {
    831             View prevDivider = mDividers.get(i);
    832             int index = indexOfChild(prevDivider);
    833             removeView(prevDivider);
    834             View divider = inflateDivider();
    835             addView(divider, index);
    836             mDividers.set(i, divider);
    837         }
    838         removeView(mOverflowNumber);
    839         mOverflowNumber = null;
    840         mOverflowInvertHelper = null;
    841         mGroupOverFlowState = null;
    842         updateGroupOverflow();
    843     }
    844 
    845     public void setUserLocked(boolean userLocked) {
    846         mUserLocked = userLocked;
    847         int childCount = mChildren.size();
    848         for (int i = 0; i < childCount; i++) {
    849             ExpandableNotificationRow child = mChildren.get(i);
    850             child.setUserLocked(userLocked);
    851         }
    852     }
    853 
    854     public void onNotificationUpdated() {
    855         mHybridGroupManager.setOverflowNumberColor(mOverflowNumber,
    856                 mNotificationParent.getNotificationColor());
    857     }
    858 
    859     public int getPositionInLinearLayout(View childInGroup) {
    860         int position = mNotificationHeaderMargin + mNotificatonTopPadding;
    861 
    862         for (int i = 0; i < mChildren.size(); i++) {
    863             ExpandableNotificationRow child = mChildren.get(i);
    864             boolean notGone = child.getVisibility() != View.GONE;
    865             if (notGone) {
    866                 position += mDividerHeight;
    867             }
    868             if (child == childInGroup) {
    869                 return position;
    870             }
    871             if (notGone) {
    872                 position += child.getIntrinsicHeight();
    873             }
    874         }
    875         return 0;
    876     }
    877 }
    878