blob: 03697b86e31e0190fb5a63fff30858103664ad8c [file] [log] [blame]
/*
* Copyright (C) 2016 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.phone;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import com.android.systemui.R;
import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.stack.AnimationFilter;
import com.android.systemui.statusbar.stack.AnimationProperties;
import com.android.systemui.statusbar.stack.ViewState;
import java.util.WeakHashMap;
/**
* A container for notification icons. It handles overflowing icons properly and positions them
* correctly on the screen.
*/
public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
private static final String TAG = "NotificationIconContainer";
private static final boolean DEBUG = false;
private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
@Override
public AnimationFilter getAnimationFilter() {
return mAnimationFilter;
}
}.setDuration(200);
private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
@Override
public AnimationFilter getAnimationFilter() {
return mAnimationFilter;
}
}.setDuration(200).setDelay(50);
private boolean mShowAllIcons = true;
private WeakHashMap<View, IconState> mIconStates = new WeakHashMap<>();
private int mDotPadding;
private int mStaticDotRadius;
private int mActualLayoutWidth = -1;
private float mActualPaddingEnd = -1;
private float mActualPaddingStart = -1;
private boolean mChangingViewPositions;
private int mAnimationStartIndex = -1;
public NotificationIconContainer(Context context, AttributeSet attrs) {
super(context, attrs);
initDimens();
setWillNotDraw(!DEBUG);
}
private void initDimens() {
mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
initDimens();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
float centerY = getHeight() / 2.0f;
// we layout all our children on the left at the top
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// We need to layout all children even the GONE ones, such that the heights are
// calculated correctly as they are used to calculate how many we can fit on the screen
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
int top = (int) (centerY - height / 2.0f);
child.layout(0, top, width, top + height);
}
if (mShowAllIcons) {
resetViewStates();
calculateIconTranslations();
applyIconStates();
}
}
public void applyIconStates() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
ViewState childState = mIconStates.get(child);
if (childState != null) {
childState.applyToView(child);
}
}
mAnimationStartIndex = -1;
}
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
if (!mChangingViewPositions) {
mIconStates.put(child, new IconState());
}
int childIndex = indexOfChild(child);
if (childIndex < getChildCount() - 1
&& mIconStates.get(getChildAt(childIndex + 1)).iconAppearAmount > 0.0f) {
if (mAnimationStartIndex < 0) {
mAnimationStartIndex = childIndex;
} else {
mAnimationStartIndex = Math.min(mAnimationStartIndex, childIndex);
}
}
}
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
if (child instanceof StatusBarIconView) {
final StatusBarIconView icon = (StatusBarIconView) child;
if (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
&& child.getVisibility() == VISIBLE) {
int animationStartIndex = findFirstViewIndexAfter(icon.getTranslationX());
if (mAnimationStartIndex < 0) {
mAnimationStartIndex = animationStartIndex;
} else {
mAnimationStartIndex = Math.min(mAnimationStartIndex, animationStartIndex);
}
}
if (!mChangingViewPositions) {
mIconStates.remove(child);
addTransientView(icon, 0);
icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */,
() -> removeTransientView(icon));
}
}
}
/**
* Finds the first view with a translation bigger then a given value
*/
private int findFirstViewIndexAfter(float translationX) {
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
if (view.getTranslationX() > translationX) {
return i;
}
}
return getChildCount();
}
public WeakHashMap<View, IconState> resetViewStates() {
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
ViewState iconState = mIconStates.get(view);
iconState.initFrom(view);
iconState.alpha = 1.0f;
}
return mIconStates;
}
/**
* Calulate the horizontal translations for each notification based on how much the icons
* are inserted into the notification container.
* If this is not a whole number, the fraction means by how much the icon is appearing.
*/
public void calculateIconTranslations() {
float translationX = getActualPaddingStart();
int overflowingIconIndex = -1;
int lastTwoIconWidth = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
IconState iconState = mIconStates.get(view);
iconState.xTranslation = translationX;
iconState.visibleState = StatusBarIconView.STATE_ICON;
translationX += iconState.iconAppearAmount * view.getWidth();
if (translationX > getLayoutEnd()) {
// we are overflowing it with this icon
overflowingIconIndex = i - 1;
lastTwoIconWidth = view.getWidth();
break;
}
}
if (overflowingIconIndex != -1) {
int numDots = 1;
View overflowIcon = getChildAt(overflowingIconIndex);
IconState overflowState = mIconStates.get(overflowIcon);
lastTwoIconWidth += overflowIcon.getWidth();
int dotWidth = mStaticDotRadius * 2 + mDotPadding;
int totalDotLength = mStaticDotRadius * 6 + 2 * mDotPadding;
translationX = (getLayoutEnd() - lastTwoIconWidth / 2 - totalDotLength / 2)
- overflowIcon.getWidth() * 0.3f + mStaticDotRadius;
float overflowStart = getLayoutEnd() - lastTwoIconWidth;
float overlapAmount = (overflowState.xTranslation - overflowStart)
/ overflowIcon.getWidth();
translationX += overlapAmount * dotWidth;
for (int i = overflowingIconIndex; i < childCount; i++) {
View view = getChildAt(i);
IconState iconState = mIconStates.get(view);
iconState.xTranslation = translationX;
if (numDots <= 3) {
iconState.visibleState = StatusBarIconView.STATE_DOT;
translationX += numDots == 3 ? 3 * dotWidth : dotWidth;
} else {
iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
}
numDots++;
}
}
if (isLayoutRtl()) {
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
IconState iconState = mIconStates.get(view);
iconState.xTranslation = getWidth() - iconState.xTranslation - view.getWidth();
}
}
}
private float getLayoutEnd() {
return getActualWidth() - getActualPaddingEnd();
}
private float getActualPaddingEnd() {
if (mActualPaddingEnd < 0) {
return getPaddingEnd();
}
return mActualPaddingEnd;
}
private float getActualPaddingStart() {
if (mActualPaddingStart < 0) {
return getPaddingStart();
}
return mActualPaddingStart;
}
/**
* Sets whether the layout should always show all icons.
* If this is true, the icon positions will be updated on layout.
* If this if false, the layout is managed from the outside and layouting won't trigger a
* repositioning of the icons.
*/
public void setShowAllIcons(boolean showAllIcons) {
mShowAllIcons = showAllIcons;
}
public void setActualLayoutWidth(int actualLayoutWidth) {
mActualLayoutWidth = actualLayoutWidth;
if (DEBUG) {
invalidate();
}
}
public void setActualPaddingEnd(float paddingEnd) {
mActualPaddingEnd = paddingEnd;
if (DEBUG) {
invalidate();
}
}
public void setActualPaddingStart(float paddingStart) {
mActualPaddingStart = paddingStart;
if (DEBUG) {
invalidate();
}
}
public int getActualWidth() {
if (mActualLayoutWidth < 0) {
return getWidth();
}
return mActualLayoutWidth;
}
public void setChangingViewPositions(boolean changingViewPositions) {
mChangingViewPositions = changingViewPositions;
}
public class IconState extends ViewState {
public float iconAppearAmount = 1.0f;
public int visibleState;
public boolean justAdded = true;
@Override
public void applyToView(View view) {
if (view instanceof StatusBarIconView) {
StatusBarIconView icon = (StatusBarIconView) view;
AnimationProperties animationProperties = DOT_ANIMATION_PROPERTIES;
if (justAdded) {
super.applyToView(icon);
icon.setAlpha(0.0f);
icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, false /* animate */);
animationProperties = ADD_ICON_PROPERTIES;
}
boolean animate = visibleState != icon.getVisibleState() || justAdded;
if (!animate && mAnimationStartIndex >= 0
&& (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
|| visibleState != StatusBarIconView.STATE_HIDDEN)) {
int viewIndex = indexOfChild(view);
animate = viewIndex >= mAnimationStartIndex;
}
icon.setVisibleState(visibleState);
if (animate) {
animateTo(icon, animationProperties);
} else {
super.applyToView(view);
}
}
justAdded = false;
}
}
}