/* * 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 mMaxNotificationHeight; private OnHeightChangedListener mOnHeightChangedListener; private int mActualHeight; protected int mClipTopAmount; private boolean mActualHeightInitialized; private boolean mDark; private ArrayList<View> mMatchParentViews = new ArrayList<View>(); public ExpandableView(Context context, AttributeSet attrs) { super(context, attrs); mMaxNotificationHeight = getResources().getDimensionPixelSize( R.dimen.notification_max_height); } @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 maxChildHeight = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); 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 : 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); if (!mActualHeightInitialized && mActualHeight == 0) { int initialHeight = getInitialHeight(); if (initialHeight != 0) { setActualHeight(initialHeight); } } } /** * 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; if (notifyListeners) { notifyHeightChanged(); } } public void setActualHeight(int actualHeight) { setActualHeight(actualHeight, true); } /** * 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 getMaxHeight() { return getHeight(); } /** * @return The minimum 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() { if (mOnHeightChangedListener != null) { mOnHeightChangedListener.onHeightChanged(this); } } 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(); } /** * 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 */ void onHeightChanged(ExpandableView view); /** * Called when the view is reset and therefore the height will change abruptly * * @param view The view which was reset. */ void onReset(ExpandableView view); } }