/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar.stack; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import com.android.systemui.R; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; import java.util.ArrayList; import java.util.List; /** * A container containing child notifications */ public class NotificationChildrenContainer extends ViewGroup { private final int mChildPadding; private final int mDividerHeight; private final int mMaxNotificationHeight; private final List<View> mDividers = new ArrayList<>(); private final List<ExpandableNotificationRow> mChildren = new ArrayList<>(); private final View mCollapseButton; private final View mCollapseDivider; private final int mCollapseButtonHeight; private final int mNotificationAppearDistance; public NotificationChildrenContainer(Context context) { this(context, null); } public NotificationChildrenContainer(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mChildPadding = getResources().getDimensionPixelSize( R.dimen.notification_children_padding); mDividerHeight = getResources().getDimensionPixelSize( R.dimen.notification_children_divider_height); mMaxNotificationHeight = getResources().getDimensionPixelSize( R.dimen.notification_max_height); mNotificationAppearDistance = getResources().getDimensionPixelSize( R.dimen.notification_appear_distance); LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class); mCollapseButton = inflater.inflate(R.layout.notification_collapse_button, this, false); mCollapseButtonHeight = getResources().getDimensionPixelSize( R.dimen.notification_bottom_decor_height); addView(mCollapseButton); mCollapseDivider = inflateDivider(); addView(mCollapseDivider); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = mChildren.size(); boolean firstChild = true; for (int i = 0; i < childCount; i++) { View child = mChildren.get(i); boolean viewGone = child.getVisibility() == View.GONE; if (i != 0) { View divider = mDividers.get(i - 1); int dividerVisibility = divider.getVisibility(); int newVisibility = viewGone ? INVISIBLE : VISIBLE; if (dividerVisibility != newVisibility) { divider.setVisibility(newVisibility); } } if (viewGone) { continue; } child.layout(0, 0, getWidth(), child.getMeasuredHeight()); if (!firstChild) { mDividers.get(i - 1).layout(0, 0, getWidth(), mDividerHeight); } else { firstChild = false; } } mCollapseButton.layout(0, 0, getWidth(), mCollapseButtonHeight); mCollapseDivider.layout(0, mCollapseButtonHeight - mDividerHeight, getWidth(), mCollapseButtonHeight); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int ownMaxHeight = mMaxNotificationHeight; int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; if (hasFixedHeight || isHeightLimited) { int size = MeasureSpec.getSize(heightMeasureSpec); ownMaxHeight = Math.min(ownMaxHeight, size); } int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST); int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY); int collapseButtonHeightSpec = MeasureSpec.makeMeasureSpec(mCollapseButtonHeight, MeasureSpec.EXACTLY); mCollapseButton.measure(widthMeasureSpec, collapseButtonHeightSpec); mCollapseDivider.measure(widthMeasureSpec, dividerHeightSpec); int height = mCollapseButtonHeight; int childCount = mChildren.size(); boolean firstChild = true; for (int i = 0; i < childCount; i++) { View child = mChildren.get(i); if (child.getVisibility() == View.GONE) { continue; } child.measure(widthMeasureSpec, newHeightSpec); height += child.getMeasuredHeight(); if (!firstChild) { // layout the divider View divider = mDividers.get(i - 1); divider.measure(widthMeasureSpec, dividerHeightSpec); height += mChildPadding; } else { firstChild = false; } } int width = MeasureSpec.getSize(widthMeasureSpec); height = hasFixedHeight ? ownMaxHeight : isHeightLimited ? Math.min(ownMaxHeight, height) : height; setMeasuredDimension(width, height); } /** * Add a child notification to this view. * * @param row the row to add * @param childIndex the index to add it at, if -1 it will be added at the end */ public void addNotification(ExpandableNotificationRow row, int childIndex) { int newIndex = childIndex < 0 ? mChildren.size() : childIndex; mChildren.add(newIndex, row); addView(row); if (mChildren.size() != 1) { View divider = inflateDivider(); addView(divider); mDividers.add(Math.max(newIndex - 1, 0), divider); } // TODO: adapt background corners // TODO: fix overdraw } public void removeNotification(ExpandableNotificationRow row) { int childIndex = mChildren.indexOf(row); mChildren.remove(row); removeView(row); if (!mDividers.isEmpty()) { View divider = mDividers.remove(Math.max(childIndex - 1, 0)); removeView(divider); } row.setSystemChildExpanded(false); // TODO: adapt background corners } private View inflateDivider() { return LayoutInflater.from(mContext).inflate( R.layout.notification_children_divider, this, false); } public List<ExpandableNotificationRow> getNotificationChildren() { return mChildren; } /** * Apply the order given in the list to the children. * * @param childOrder the new list order * @return whether the list order has changed */ public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) { if (childOrder == null) { return false; } boolean result = false; for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) { ExpandableNotificationRow child = mChildren.get(i); ExpandableNotificationRow desiredChild = childOrder.get(i); if (child != desiredChild) { mChildren.remove(desiredChild); mChildren.add(i, desiredChild); result = true; } } // Let's make the first child expanded! boolean first = true; for (int i = 0; i < childOrder.size(); i++) { ExpandableNotificationRow child = childOrder.get(i); child.setSystemChildExpanded(first); first = false; } return result; } public int getIntrinsicHeight() { int childCount = mChildren.size(); int intrinsicHeight = 0; int visibleChildren = 0; for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); if (child.getVisibility() == View.GONE) { continue; } intrinsicHeight += child.getIntrinsicHeight(); visibleChildren++; } if (visibleChildren > 0) { intrinsicHeight += (visibleChildren - 1) * mDividerHeight; } return intrinsicHeight; } /** * Update the state of all its children based on a linear layout algorithm. * * @param resultState the state to update * @param parentState the state of the parent */ public void getState(StackScrollState resultState, StackViewState parentState) { int childCount = mChildren.size(); int yPosition = mCollapseButtonHeight; boolean firstChild = true; for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); if (child.getVisibility() == View.GONE) { continue; } if (!firstChild) { // There's a divider yPosition += mChildPadding; } else { firstChild = false; } StackViewState childState = resultState.getViewStateForView(child); int intrinsicHeight = child.getIntrinsicHeight(); childState.yTranslation = yPosition; childState.zTranslation = 0; childState.height = intrinsicHeight; childState.dimmed = parentState.dimmed; childState.dark = parentState.dark; childState.hideSensitive = parentState.hideSensitive; childState.belowSpeedBump = parentState.belowSpeedBump; childState.scale = parentState.scale; childState.clipTopAmount = 0; childState.topOverLap = 0; childState.location = parentState.location; yPosition += intrinsicHeight; } } public void applyState(StackScrollState state) { int childCount = mChildren.size(); boolean firstChild = true; ViewState dividerState = new ViewState(); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); StackViewState viewState = state.getViewStateForView(child); if (child.getVisibility() == View.GONE) { continue; } if (!firstChild) { // layout the divider View divider = mDividers.get(i - 1); dividerState.initFrom(divider); dividerState.yTranslation = (int) (viewState.yTranslation - (mChildPadding + mDividerHeight) / 2.0f); dividerState.alpha = 1; state.applyViewState(divider, dividerState); } else { firstChild = false; } state.applyState(child, viewState); } } public void setCollapseClickListener(OnClickListener collapseClickListener) { mCollapseButton.setOnClickListener(collapseClickListener); } /** * This is called when the children expansion has changed and positions the children properly * for an appear animation. * * @param state the new state we animate to */ public void prepareExpansionChanged(StackScrollState state) { int childCount = mChildren.size(); boolean firstChild = true; StackViewState sourceState = new StackViewState(); ViewState dividerState = new ViewState(); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); StackViewState viewState = state.getViewStateForView(child); if (child.getVisibility() == View.GONE) { continue; } if (!firstChild) { // layout the divider View divider = mDividers.get(i - 1); dividerState.initFrom(divider); dividerState.yTranslation = viewState.yTranslation - (mChildPadding + mDividerHeight) / 2.0f + mNotificationAppearDistance; dividerState.alpha = 0; state.applyViewState(divider, dividerState); } else { firstChild = false; } sourceState.copyFrom(viewState); sourceState.alpha = 0; sourceState.yTranslation += mNotificationAppearDistance; state.applyState(child, sourceState); } mCollapseButton.setAlpha(0); mCollapseDivider.setAlpha(0); mCollapseDivider.setTranslationY(mNotificationAppearDistance / 4); } public void startAnimationToState(StackScrollState state, StackStateAnimator stateAnimator, boolean withDelays, long baseDelay, long duration) { int childCount = mChildren.size(); boolean firstChild = true; ViewState dividerState = new ViewState(); int notGoneIndex = 0; for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); StackViewState viewState = state.getViewStateForView(child); if (child.getVisibility() == View.GONE) { continue; } int difference = Math.min(StackStateAnimator.DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN, notGoneIndex + 1); long delay = withDelays ? difference * StackStateAnimator.ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN : 0; delay += baseDelay; if (!firstChild) { // layout the divider View divider = mDividers.get(i - 1); dividerState.initFrom(divider); dividerState.yTranslation = viewState.yTranslation - (mChildPadding + mDividerHeight) / 2.0f; dividerState.alpha = 1; stateAnimator.startViewAnimations(divider, dividerState, delay, duration); } else { firstChild = false; } stateAnimator.startStackAnimations(child, viewState, state, -1, delay); notGoneIndex++; } dividerState.initFrom(mCollapseButton); dividerState.alpha = 1.0f; stateAnimator.startViewAnimations(mCollapseButton, dividerState, baseDelay, duration); dividerState.initFrom(mCollapseDivider); dividerState.alpha = 1.0f; dividerState.yTranslation = 0.0f; stateAnimator.startViewAnimations(mCollapseDivider, dividerState, baseDelay, duration); } public ExpandableNotificationRow getViewAtPosition(float y) { // find the view under the pointer, accounting for GONE views final int count = mChildren.size(); for (int childIdx = 0; childIdx < count; childIdx++) { ExpandableNotificationRow slidingChild = mChildren.get(childIdx); float childTop = slidingChild.getTranslationY(); float top = childTop + slidingChild.getClipTopAmount(); float bottom = childTop + slidingChild.getActualHeight(); if (y >= top && y <= bottom) { return slidingChild; } } return null; } public void setTintColor(int color) { ExpandableNotificationRow.applyTint(mCollapseDivider, color); } }