| /* |
| * 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.Paint; |
| import android.graphics.Rect; |
| import android.util.AttributeSet; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.FrameLayout; |
| |
| import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * An abstract view for expandable views. |
| */ |
| public abstract class ExpandableView extends FrameLayout { |
| |
| protected OnHeightChangedListener mOnHeightChangedListener; |
| private int mActualHeight; |
| protected int mClipTopAmount; |
| 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; |
| private boolean mClipToActualHeight = true; |
| private boolean mChangingPosition = false; |
| |
| public ExpandableView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| final int givenSize = MeasureSpec.getSize(heightMeasureSpec); |
| int ownMaxHeight = Integer.MAX_VALUE; |
| int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
| if (heightMode != MeasureSpec.UNSPECIFIED && givenSize != 0) { |
| ownMaxHeight = Math.min(givenSize, 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) { |
| 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 = heightMode == MeasureSpec.EXACTLY |
| ? givenSize : 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); |
| setMeasuredDimension(width, ownHeight); |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| super.onLayout(changed, left, top, right, bottom); |
| updateClipping(); |
| } |
| |
| @Override |
| public boolean pointInView(float localX, float localY, float slop) { |
| float top = mClipTopAmount; |
| float bottom = mActualHeight; |
| return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) && |
| localY < (bottom + slop); |
| } |
| |
| /** |
| * 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) { |
| mActualHeight = actualHeight; |
| updateClipping(); |
| if (notifyListeners) { |
| notifyHeightChanged(false /* needsAnimation */); |
| } |
| } |
| |
| public void setActualHeight(int actualHeight) { |
| setActualHeight(actualHeight, true /* notifyListeners */); |
| } |
| |
| /** |
| * See {@link #setActualHeight}. |
| * |
| * @return The current actual height of this notification. |
| */ |
| public int getActualHeight() { |
| return mActualHeight; |
| } |
| |
| /** |
| * @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(); |
| } |
| |
| /** |
| * @return The minimum height this child chan be expanded to. Note that this might be different |
| * than {@link #getMinHeight()} because some elements can't be collapsed by an expand gesture |
| * to it's absolute minimal height |
| */ |
| public int getMinExpandHeight() { |
| 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) { |
| } |
| |
| /** |
| * Sets the translation of the view. |
| */ |
| public void setTranslation(float translation) { |
| setTranslationX(translation); |
| } |
| |
| /** |
| * Gets the translation of the view. |
| */ |
| public float getTranslation() { |
| return getTranslationX(); |
| } |
| |
| 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 boolean isSummaryWithChildren() { |
| return false; |
| } |
| |
| public boolean areChildrenExpanded() { |
| return false; |
| } |
| |
| private void updateClipping() { |
| if (mClipToActualHeight) { |
| int top = mClipTopOptimization; |
| if (top >= getActualHeight()) { |
| top = getActualHeight() - 1; |
| } |
| mClipRect.set(0, top, getWidth(), getActualHeight()); |
| setClipBounds(mClipRect); |
| } else { |
| setClipBounds(null); |
| } |
| } |
| |
| public void setClipToActualHeight(boolean clipToActualHeight) { |
| mClipToActualHeight = clipToActualHeight; |
| updateClipping(); |
| } |
| |
| 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; |
| } |
| |
| @Override |
| public void setLayerType(int layerType, Paint paint) { |
| if (hasOverlappingRendering()) { |
| super.setLayerType(layerType, paint); |
| } |
| } |
| |
| @Override |
| public boolean hasOverlappingRendering() { |
| // Otherwise it will be clipped |
| return super.hasOverlappingRendering() && getActualHeight() <= getHeight(); |
| } |
| |
| public float getShadowAlpha() { |
| return 0.0f; |
| } |
| |
| public void setShadowAlpha(float shadowAlpha) { |
| } |
| |
| /** |
| * @return an amount between 0 and 1 of increased padding that this child needs |
| */ |
| public float getIncreasedPaddingAmount() { |
| return 0.0f; |
| } |
| |
| public boolean mustStayOnScreen() { |
| return false; |
| } |
| |
| public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, |
| int outlineTranslation) { |
| } |
| |
| public float getOutlineAlpha() { |
| return 0.0f; |
| } |
| |
| public int getOutlineTranslation() { |
| return 0; |
| } |
| |
| public void setChangingPosition(boolean changingPosition) { |
| mChangingPosition = changingPosition; |
| } |
| |
| public boolean isChangingPosition() { |
| return mChangingPosition; |
| } |
| |
| /** |
| * 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); |
| } |
| } |