| /* |
| * 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(); |
| |
| 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 dispatchTouchEvent(MotionEvent ev) { |
| if (filterMotionEvent(ev)) { |
| return super.dispatchTouchEvent(ev); |
| } |
| return false; |
| } |
| |
| protected boolean filterMotionEvent(MotionEvent event) { |
| return event.getActionMasked() != MotionEvent.ACTION_DOWN |
| || 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(); |
| } |
| |
| 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() { |
| mClipRect.set(0, mClipTopOptimization, 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(); |
| } |
| |
| /** |
| * 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); |
| } |
| } |