/*
* Copyright (C) 2014 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;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.android.systemui.R;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
import java.util.ArrayList;
/**
* An abstract view for expandable views.
*/
public abstract class ExpandableView extends FrameLayout {
private final int mBottomDecorHeight;
protected OnHeightChangedListener mOnHeightChangedListener;
protected int mMaxViewHeight;
private int mActualHeight;
protected int mClipTopAmount;
private boolean mActualHeightInitialized;
private boolean mDark;
private ArrayList<View> mMatchParentViews = new ArrayList<View>();
private int mClipTopOptimization;
private static Rect mClipRect = new Rect();
private boolean mWillBeGone;
private int mMinClipTopAmount = 0;
public ExpandableView(Context context, AttributeSet attrs) {
super(context, attrs);
mMaxViewHeight = getResources().getDimensionPixelSize(
R.dimen.notification_max_height);
mBottomDecorHeight = resolveBottomDecorHeight();
}
protected int resolveBottomDecorHeight() {
return getResources().getDimensionPixelSize(
R.dimen.notification_bottom_decor_height);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int ownMaxHeight = mMaxViewHeight;
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
if (hasFixedHeight) {
// We have a height set in our layout, so we want to be at most as big as given
ownMaxHeight = Math.min(MeasureSpec.getSize(heightMeasureSpec), ownMaxHeight);
}
int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
int maxChildHeight = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE || isChildInvisible(child)) {
continue;
}
int childHeightSpec = newHeightSpec;
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
if (layoutParams.height >= 0) {
// An actual height is set
childHeightSpec = layoutParams.height > ownMaxHeight
? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
: MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
}
child.measure(
getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
childHeightSpec);
int childHeight = child.getMeasuredHeight();
maxChildHeight = Math.max(maxChildHeight, childHeight);
} else {
mMatchParentViews.add(child);
}
}
int ownHeight = hasFixedHeight ? ownMaxHeight : Math.min(ownMaxHeight, maxChildHeight);
newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
for (View child : mMatchParentViews) {
child.measure(getChildMeasureSpec(
widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
newHeightSpec);
}
mMatchParentViews.clear();
int width = MeasureSpec.getSize(widthMeasureSpec);
if (canHaveBottomDecor()) {
// We always account for the expandAction as well.
ownHeight += mBottomDecorHeight;
}
setMeasuredDimension(width, ownHeight);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!mActualHeightInitialized && mActualHeight == 0) {
int initialHeight = getInitialHeight();
if (initialHeight != 0) {
setContentHeight(initialHeight);
}
}
updateClipping();
}
/**
* Resets the height of the view on the next layout pass
*/
protected void resetActualHeight() {
mActualHeight = 0;
mActualHeightInitialized = false;
requestLayout();
}
protected int getInitialHeight() {
return getHeight();
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
if (filterMotionEvent(ev)) {
return super.dispatchGenericMotionEvent(ev);
}
return false;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (filterMotionEvent(ev)) {
return super.dispatchTouchEvent(ev);
}
return false;
}
protected boolean filterMotionEvent(MotionEvent event) {
return event.getActionMasked() != MotionEvent.ACTION_DOWN
&& event.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER
&& event.getActionMasked() != MotionEvent.ACTION_HOVER_MOVE
|| event.getY() > mClipTopAmount && event.getY() < mActualHeight;
}
/**
* Sets the actual height of this notification. This is different than the laid out
* {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
*
* @param actualHeight The height of this notification.
* @param notifyListeners Whether the listener should be informed about the change.
*/
public void setActualHeight(int actualHeight, boolean notifyListeners) {
mActualHeightInitialized = true;
mActualHeight = actualHeight;
updateClipping();
if (notifyListeners) {
notifyHeightChanged(false /* needsAnimation */);
}
}
public void setContentHeight(int contentHeight) {
setActualHeight(contentHeight + getBottomDecorHeight(), true);
}
/**
* See {@link #setActualHeight}.
*
* @return The current actual height of this notification.
*/
public int getActualHeight() {
return mActualHeight;
}
/**
* This view may have a bottom decor which will be placed below the content. If it has one, this
* view will be layouted higher than just the content by {@link #mBottomDecorHeight}.
* @return the height of the decor if it currently has one
*/
public int getBottomDecorHeight() {
return hasBottomDecor() ? mBottomDecorHeight : 0;
}
/**
* @return whether this view may have a bottom decor at all. This will force the view to layout
* itself higher than just it's content
*/
protected boolean canHaveBottomDecor() {
return false;
}
/**
* @return whether this view has a decor view below it's content. This will make the intrinsic
* height from {@link #getIntrinsicHeight()} higher as well
*/
protected boolean hasBottomDecor() {
return false;
}
/**
* @return The maximum height of this notification.
*/
public int getMaxContentHeight() {
return getHeight();
}
/**
* @return The minimum content height of this notification.
*/
public int getMinHeight() {
return getHeight();
}
/**
* Sets the notification as dimmed. The default implementation does nothing.
*
* @param dimmed Whether the notification should be dimmed.
* @param fade Whether an animation should be played to change the state.
*/
public void setDimmed(boolean dimmed, boolean fade) {
}
/**
* Sets the notification as dark. The default implementation does nothing.
*
* @param dark Whether the notification should be dark.
* @param fade Whether an animation should be played to change the state.
* @param delay If fading, the delay of the animation.
*/
public void setDark(boolean dark, boolean fade, long delay) {
mDark = dark;
}
public boolean isDark() {
return mDark;
}
/**
* See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
* the upcoming state of hiding sensitive notifications. It gets called at the very beginning
* of a stack scroller update such that the updated intrinsic height (which is dependent on
* whether private or public layout is showing) gets taken into account into all layout
* calculations.
*/
public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
}
/**
* Sets whether the notification should hide its private contents if it is sensitive.
*/
public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
long duration) {
}
/**
* @return The desired notification height.
*/
public int getIntrinsicHeight() {
return getHeight();
}
/**
* Sets the amount this view should be clipped from the top. This is used when an expanded
* notification is scrolling in the top or bottom stack.
*
* @param clipTopAmount The amount of pixels this view should be clipped from top.
*/
public void setClipTopAmount(int clipTopAmount) {
mClipTopAmount = clipTopAmount;
}
public int getClipTopAmount() {
return mClipTopAmount;
}
public void setOnHeightChangedListener(OnHeightChangedListener listener) {
mOnHeightChangedListener = listener;
}
/**
* @return Whether we can expand this views content.
*/
public boolean isContentExpandable() {
return false;
}
public void notifyHeightChanged(boolean needsAnimation) {
if (mOnHeightChangedListener != null) {
mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
}
}
public boolean isTransparent() {
return false;
}
/**
* Perform a remove animation on this view.
*
* @param duration The duration of the remove animation.
* @param translationDirection The direction value from [-1 ... 1] indicating in which the
* animation should be performed. A value of -1 means that The
* remove animation should be performed upwards,
* such that the child appears to be going away to the top. 1
* Should mean the opposite.
* @param onFinishedRunnable A runnable which should be run when the animation is finished.
*/
public abstract void performRemoveAnimation(long duration, float translationDirection,
Runnable onFinishedRunnable);
public abstract void performAddAnimation(long delay, long duration);
public void setBelowSpeedBump(boolean below) {
}
public void onHeightReset() {
if (mOnHeightChangedListener != null) {
mOnHeightChangedListener.onReset(this);
}
}
/**
* This method returns the drawing rect for the view which is different from the regular
* drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
* position 0 and usually the translation is neglected. Since we are manually clipping this
* view,we also need to subtract the clipTopAmount from the top. This is needed in order to
* ensure that accessibility and focusing work correctly.
*
* @param outRect The (scrolled) drawing bounds of the view.
*/
@Override
public void getDrawingRect(Rect outRect) {
super.getDrawingRect(outRect);
outRect.left += getTranslationX();
outRect.right += getTranslationX();
outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
outRect.top += getTranslationY() + getClipTopAmount();
}
@Override
public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
super.getBoundsOnScreen(outRect, clipToParent);
outRect.bottom = outRect.top + getActualHeight();
outRect.top += getClipTopOptimization();
}
public int getContentHeight() {
return mActualHeight - getBottomDecorHeight();
}
/**
* @return whether the given child can be ignored for layouting and measuring purposes
*/
protected boolean isChildInvisible(View child) {
return false;
}
public boolean areChildrenExpanded() {
return false;
}
private void updateClipping() {
int top = mClipTopOptimization;
if (top >= getActualHeight()) {
top = getActualHeight() - 1;
}
mClipRect.set(0, top, getWidth(), getActualHeight());
setClipBounds(mClipRect);
}
public int getClipTopOptimization() {
return mClipTopOptimization;
}
/**
* Set that the view will be clipped by a given amount from the top. Contrary to
* {@link #setClipTopAmount} this amount doesn't effect shadows and the background.
*
* @param clipTopOptimization the amount to clip from the top
*/
public void setClipTopOptimization(int clipTopOptimization) {
mClipTopOptimization = clipTopOptimization;
updateClipping();
}
public boolean willBeGone() {
return mWillBeGone;
}
public void setWillBeGone(boolean willBeGone) {
mWillBeGone = willBeGone;
}
public int getMinClipTopAmount() {
return mMinClipTopAmount;
}
public void setMinClipTopAmount(int minClipTopAmount) {
mMinClipTopAmount = minClipTopAmount;
}
/**
* A listener notifying when {@link #getActualHeight} changes.
*/
public interface OnHeightChangedListener {
/**
* @param view the view for which the height changed, or {@code null} if just the top
* padding or the padding between the elements changed
* @param needsAnimation whether the view height needs to be animated
*/
void onHeightChanged(ExpandableView view, boolean needsAnimation);
/**
* Called when the view is reset and therefore the height will change abruptly
*
* @param view The view which was reset.
*/
void onReset(ExpandableView view);
}
}