1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.systemui.statusbar;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.util.AttributeSet;
22 import android.view.MotionEvent;
23 import android.view.View;
24 import android.view.ViewGroup;
25 import android.widget.FrameLayout;
26 import com.android.systemui.R;
27 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
28 
29 import java.util.ArrayList;
30 
31 /**
32  * An abstract view for expandable views.
33  */
34 public abstract class ExpandableView extends FrameLayout {
35 
36     private final int mMaxNotificationHeight;
37 
38     private OnHeightChangedListener mOnHeightChangedListener;
39     private int mActualHeight;
40     protected int mClipTopAmount;
41     private boolean mActualHeightInitialized;
42     private boolean mDark;
43     private ArrayList<View> mMatchParentViews = new ArrayList<View>();
44 
ExpandableView(Context context, AttributeSet attrs)45     public ExpandableView(Context context, AttributeSet attrs) {
46         super(context, attrs);
47         mMaxNotificationHeight = getResources().getDimensionPixelSize(
48                 R.dimen.notification_max_height);
49     }
50 
51     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)52     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
53         int ownMaxHeight = mMaxNotificationHeight;
54         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
55         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
56         boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
57         if (hasFixedHeight || isHeightLimited) {
58             int size = MeasureSpec.getSize(heightMeasureSpec);
59             ownMaxHeight = Math.min(ownMaxHeight, size);
60         }
61         int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
62         int maxChildHeight = 0;
63         int childCount = getChildCount();
64         for (int i = 0; i < childCount; i++) {
65             View child = getChildAt(i);
66             int childHeightSpec = newHeightSpec;
67             ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
68             if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
69                 if (layoutParams.height >= 0) {
70                     // An actual height is set
71                     childHeightSpec = layoutParams.height > ownMaxHeight
72                         ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
73                         : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
74                 }
75                 child.measure(
76                         getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
77                         childHeightSpec);
78                 int childHeight = child.getMeasuredHeight();
79                 maxChildHeight = Math.max(maxChildHeight, childHeight);
80             } else {
81                 mMatchParentViews.add(child);
82             }
83         }
84         int ownHeight = hasFixedHeight ? ownMaxHeight : maxChildHeight;
85         newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
86         for (View child : mMatchParentViews) {
87             child.measure(getChildMeasureSpec(
88                     widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
89                     newHeightSpec);
90         }
91         mMatchParentViews.clear();
92         int width = MeasureSpec.getSize(widthMeasureSpec);
93         setMeasuredDimension(width, ownHeight);
94     }
95 
96     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)97     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
98         super.onLayout(changed, left, top, right, bottom);
99         if (!mActualHeightInitialized && mActualHeight == 0) {
100             int initialHeight = getInitialHeight();
101             if (initialHeight != 0) {
102                 setActualHeight(initialHeight);
103             }
104         }
105     }
106 
107     /**
108      * Resets the height of the view on the next layout pass
109      */
resetActualHeight()110     protected void resetActualHeight() {
111         mActualHeight = 0;
112         mActualHeightInitialized = false;
113         requestLayout();
114     }
115 
getInitialHeight()116     protected int getInitialHeight() {
117         return getHeight();
118     }
119 
120     @Override
dispatchTouchEvent(MotionEvent ev)121     public boolean dispatchTouchEvent(MotionEvent ev) {
122         if (filterMotionEvent(ev)) {
123             return super.dispatchTouchEvent(ev);
124         }
125         return false;
126     }
127 
filterMotionEvent(MotionEvent event)128     protected boolean filterMotionEvent(MotionEvent event) {
129         return event.getActionMasked() != MotionEvent.ACTION_DOWN
130                 || event.getY() > mClipTopAmount && event.getY() < mActualHeight;
131     }
132 
133     /**
134      * Sets the actual height of this notification. This is different than the laid out
135      * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
136      *
137      * @param actualHeight The height of this notification.
138      * @param notifyListeners Whether the listener should be informed about the change.
139      */
setActualHeight(int actualHeight, boolean notifyListeners)140     public void setActualHeight(int actualHeight, boolean notifyListeners) {
141         mActualHeightInitialized = true;
142         mActualHeight = actualHeight;
143         if (notifyListeners) {
144             notifyHeightChanged();
145         }
146     }
147 
setActualHeight(int actualHeight)148     public void setActualHeight(int actualHeight) {
149         setActualHeight(actualHeight, true);
150     }
151 
152     /**
153      * See {@link #setActualHeight}.
154      *
155      * @return The current actual height of this notification.
156      */
getActualHeight()157     public int getActualHeight() {
158         return mActualHeight;
159     }
160 
161     /**
162      * @return The maximum height of this notification.
163      */
getMaxHeight()164     public int getMaxHeight() {
165         return getHeight();
166     }
167 
168     /**
169      * @return The minimum height of this notification.
170      */
getMinHeight()171     public int getMinHeight() {
172         return getHeight();
173     }
174 
175     /**
176      * Sets the notification as dimmed. The default implementation does nothing.
177      *
178      * @param dimmed Whether the notification should be dimmed.
179      * @param fade Whether an animation should be played to change the state.
180      */
setDimmed(boolean dimmed, boolean fade)181     public void setDimmed(boolean dimmed, boolean fade) {
182     }
183 
184     /**
185      * Sets the notification as dark. The default implementation does nothing.
186      *
187      * @param dark Whether the notification should be dark.
188      * @param fade Whether an animation should be played to change the state.
189      * @param delay If fading, the delay of the animation.
190      */
setDark(boolean dark, boolean fade, long delay)191     public void setDark(boolean dark, boolean fade, long delay) {
192         mDark = dark;
193     }
194 
isDark()195     public boolean isDark() {
196         return mDark;
197     }
198 
199     /**
200      * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
201      * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
202      * of a stack scroller update such that the updated intrinsic height (which is dependent on
203      * whether private or public layout is showing) gets taken into account into all layout
204      * calculations.
205      */
setHideSensitiveForIntrinsicHeight(boolean hideSensitive)206     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
207     }
208 
209     /**
210      * Sets whether the notification should hide its private contents if it is sensitive.
211      */
setHideSensitive(boolean hideSensitive, boolean animated, long delay, long duration)212     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
213             long duration) {
214     }
215 
216     /**
217      * @return The desired notification height.
218      */
getIntrinsicHeight()219     public int getIntrinsicHeight() {
220         return getHeight();
221     }
222 
223     /**
224      * Sets the amount this view should be clipped from the top. This is used when an expanded
225      * notification is scrolling in the top or bottom stack.
226      *
227      * @param clipTopAmount The amount of pixels this view should be clipped from top.
228      */
setClipTopAmount(int clipTopAmount)229     public void setClipTopAmount(int clipTopAmount) {
230         mClipTopAmount = clipTopAmount;
231     }
232 
getClipTopAmount()233     public int getClipTopAmount() {
234         return mClipTopAmount;
235     }
236 
setOnHeightChangedListener(OnHeightChangedListener listener)237     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
238         mOnHeightChangedListener = listener;
239     }
240 
241     /**
242      * @return Whether we can expand this views content.
243      */
isContentExpandable()244     public boolean isContentExpandable() {
245         return false;
246     }
247 
notifyHeightChanged()248     public void notifyHeightChanged() {
249         if (mOnHeightChangedListener != null) {
250             mOnHeightChangedListener.onHeightChanged(this);
251         }
252     }
253 
isTransparent()254     public boolean isTransparent() {
255         return false;
256     }
257 
258     /**
259      * Perform a remove animation on this view.
260      *
261      * @param duration The duration of the remove animation.
262      * @param translationDirection The direction value from [-1 ... 1] indicating in which the
263      *                             animation should be performed. A value of -1 means that The
264      *                             remove animation should be performed upwards,
265      *                             such that the  child appears to be going away to the top. 1
266      *                             Should mean the opposite.
267      * @param onFinishedRunnable A runnable which should be run when the animation is finished.
268      */
performRemoveAnimation(long duration, float translationDirection, Runnable onFinishedRunnable)269     public abstract void performRemoveAnimation(long duration, float translationDirection,
270             Runnable onFinishedRunnable);
271 
performAddAnimation(long delay, long duration)272     public abstract void performAddAnimation(long delay, long duration);
273 
setBelowSpeedBump(boolean below)274     public void setBelowSpeedBump(boolean below) {
275     }
276 
onHeightReset()277     public void onHeightReset() {
278         if (mOnHeightChangedListener != null) {
279             mOnHeightChangedListener.onReset(this);
280         }
281     }
282 
283     /**
284      * This method returns the drawing rect for the view which is different from the regular
285      * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
286      * position 0 and usually the translation is neglected. Since we are manually clipping this
287      * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
288      * ensure that accessibility and focusing work correctly.
289      *
290      * @param outRect The (scrolled) drawing bounds of the view.
291      */
292     @Override
getDrawingRect(Rect outRect)293     public void getDrawingRect(Rect outRect) {
294         super.getDrawingRect(outRect);
295         outRect.left += getTranslationX();
296         outRect.right += getTranslationX();
297         outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
298         outRect.top += getTranslationY() + getClipTopAmount();
299     }
300 
301     /**
302      * A listener notifying when {@link #getActualHeight} changes.
303      */
304     public interface OnHeightChangedListener {
305 
306         /**
307          * @param view the view for which the height changed, or {@code null} if just the top
308          *             padding or the padding between the elements changed
309          */
onHeightChanged(ExpandableView view)310         void onHeightChanged(ExpandableView view);
311 
312         /**
313          * Called when the view is reset and therefore the height will change abruptly
314          *
315          * @param view The view which was reset.
316          */
onReset(ExpandableView view)317         void onReset(ExpandableView view);
318     }
319 }
320