/*
* 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);
}
}