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 mBottomDecorHeight;
37     protected OnHeightChangedListener mOnHeightChangedListener;
38     protected int mMaxViewHeight;
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     private int mClipTopOptimization;
45     private static Rect mClipRect = new Rect();
46     private boolean mWillBeGone;
47     private int mMinClipTopAmount = 0;
48 
ExpandableView(Context context, AttributeSet attrs)49     public ExpandableView(Context context, AttributeSet attrs) {
50         super(context, attrs);
51         mMaxViewHeight = getResources().getDimensionPixelSize(
52                 R.dimen.notification_max_height);
53         mBottomDecorHeight = resolveBottomDecorHeight();
54     }
55 
resolveBottomDecorHeight()56     protected int resolveBottomDecorHeight() {
57         return getResources().getDimensionPixelSize(
58                 R.dimen.notification_bottom_decor_height);
59     }
60 
61     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)62     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
63         int ownMaxHeight = mMaxViewHeight;
64         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
65         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
66         if (hasFixedHeight) {
67             // We have a height set in our layout, so we want to be at most as big as given
68             ownMaxHeight = Math.min(MeasureSpec.getSize(heightMeasureSpec), ownMaxHeight);
69         }
70         int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
71         int maxChildHeight = 0;
72         int childCount = getChildCount();
73         for (int i = 0; i < childCount; i++) {
74             View child = getChildAt(i);
75             if (child.getVisibility() == GONE || isChildInvisible(child)) {
76                 continue;
77             }
78             int childHeightSpec = newHeightSpec;
79             ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
80             if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
81                 if (layoutParams.height >= 0) {
82                     // An actual height is set
83                     childHeightSpec = layoutParams.height > ownMaxHeight
84                         ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
85                         : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
86                 }
87                 child.measure(
88                         getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
89                         childHeightSpec);
90                 int childHeight = child.getMeasuredHeight();
91                 maxChildHeight = Math.max(maxChildHeight, childHeight);
92             } else {
93                 mMatchParentViews.add(child);
94             }
95         }
96         int ownHeight = hasFixedHeight ? ownMaxHeight : Math.min(ownMaxHeight, maxChildHeight);
97         newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
98         for (View child : mMatchParentViews) {
99             child.measure(getChildMeasureSpec(
100                     widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
101                     newHeightSpec);
102         }
103         mMatchParentViews.clear();
104         int width = MeasureSpec.getSize(widthMeasureSpec);
105         if (canHaveBottomDecor()) {
106             // We always account for the expandAction as well.
107             ownHeight += mBottomDecorHeight;
108         }
109         setMeasuredDimension(width, ownHeight);
110     }
111 
112     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)113     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
114         super.onLayout(changed, left, top, right, bottom);
115         if (!mActualHeightInitialized && mActualHeight == 0) {
116             int initialHeight = getInitialHeight();
117             if (initialHeight != 0) {
118                 setContentHeight(initialHeight);
119             }
120         }
121         updateClipping();
122     }
123 
124     /**
125      * Resets the height of the view on the next layout pass
126      */
resetActualHeight()127     protected void resetActualHeight() {
128         mActualHeight = 0;
129         mActualHeightInitialized = false;
130         requestLayout();
131     }
132 
getInitialHeight()133     protected int getInitialHeight() {
134         return getHeight();
135     }
136 
137     @Override
dispatchGenericMotionEvent(MotionEvent ev)138     public boolean dispatchGenericMotionEvent(MotionEvent ev) {
139         if (filterMotionEvent(ev)) {
140             return super.dispatchGenericMotionEvent(ev);
141         }
142         return false;
143     }
144 
145     @Override
dispatchTouchEvent(MotionEvent ev)146     public boolean dispatchTouchEvent(MotionEvent ev) {
147         if (filterMotionEvent(ev)) {
148             return super.dispatchTouchEvent(ev);
149         }
150         return false;
151     }
152 
filterMotionEvent(MotionEvent event)153     protected boolean filterMotionEvent(MotionEvent event) {
154         return event.getActionMasked() != MotionEvent.ACTION_DOWN
155                 && event.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER
156                 && event.getActionMasked() != MotionEvent.ACTION_HOVER_MOVE
157                 || event.getY() > mClipTopAmount && event.getY() < mActualHeight;
158     }
159 
160     /**
161      * Sets the actual height of this notification. This is different than the laid out
162      * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
163      *
164      * @param actualHeight The height of this notification.
165      * @param notifyListeners Whether the listener should be informed about the change.
166      */
setActualHeight(int actualHeight, boolean notifyListeners)167     public void setActualHeight(int actualHeight, boolean notifyListeners) {
168         mActualHeightInitialized = true;
169         mActualHeight = actualHeight;
170         updateClipping();
171         if (notifyListeners) {
172             notifyHeightChanged(false  /* needsAnimation */);
173         }
174     }
175 
setContentHeight(int contentHeight)176     public void setContentHeight(int contentHeight) {
177         setActualHeight(contentHeight + getBottomDecorHeight(), true);
178     }
179 
180     /**
181      * See {@link #setActualHeight}.
182      *
183      * @return The current actual height of this notification.
184      */
getActualHeight()185     public int getActualHeight() {
186         return mActualHeight;
187     }
188 
189     /**
190      * This view may have a bottom decor which will be placed below the content. If it has one, this
191      * view will be layouted higher than just the content by {@link #mBottomDecorHeight}.
192      * @return the height of the decor if it currently has one
193      */
getBottomDecorHeight()194     public int getBottomDecorHeight() {
195         return hasBottomDecor() ? mBottomDecorHeight : 0;
196     }
197 
198     /**
199      * @return whether this view may have a bottom decor at all. This will force the view to layout
200      *         itself higher than just it's content
201      */
canHaveBottomDecor()202     protected boolean canHaveBottomDecor() {
203         return false;
204     }
205 
206     /**
207      * @return whether this view has a decor view below it's content. This will make the intrinsic
208      *         height from {@link #getIntrinsicHeight()} higher as well
209      */
hasBottomDecor()210     protected boolean hasBottomDecor() {
211         return false;
212     }
213 
214     /**
215      * @return The maximum height of this notification.
216      */
getMaxContentHeight()217     public int getMaxContentHeight() {
218         return getHeight();
219     }
220 
221     /**
222      * @return The minimum content height of this notification.
223      */
getMinHeight()224     public int getMinHeight() {
225         return getHeight();
226     }
227 
228     /**
229      * Sets the notification as dimmed. The default implementation does nothing.
230      *
231      * @param dimmed Whether the notification should be dimmed.
232      * @param fade Whether an animation should be played to change the state.
233      */
setDimmed(boolean dimmed, boolean fade)234     public void setDimmed(boolean dimmed, boolean fade) {
235     }
236 
237     /**
238      * Sets the notification as dark. The default implementation does nothing.
239      *
240      * @param dark Whether the notification should be dark.
241      * @param fade Whether an animation should be played to change the state.
242      * @param delay If fading, the delay of the animation.
243      */
setDark(boolean dark, boolean fade, long delay)244     public void setDark(boolean dark, boolean fade, long delay) {
245         mDark = dark;
246     }
247 
isDark()248     public boolean isDark() {
249         return mDark;
250     }
251 
252     /**
253      * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
254      * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
255      * of a stack scroller update such that the updated intrinsic height (which is dependent on
256      * whether private or public layout is showing) gets taken into account into all layout
257      * calculations.
258      */
setHideSensitiveForIntrinsicHeight(boolean hideSensitive)259     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
260     }
261 
262     /**
263      * Sets whether the notification should hide its private contents if it is sensitive.
264      */
setHideSensitive(boolean hideSensitive, boolean animated, long delay, long duration)265     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
266             long duration) {
267     }
268 
269     /**
270      * @return The desired notification height.
271      */
getIntrinsicHeight()272     public int getIntrinsicHeight() {
273         return getHeight();
274     }
275 
276     /**
277      * Sets the amount this view should be clipped from the top. This is used when an expanded
278      * notification is scrolling in the top or bottom stack.
279      *
280      * @param clipTopAmount The amount of pixels this view should be clipped from top.
281      */
setClipTopAmount(int clipTopAmount)282     public void setClipTopAmount(int clipTopAmount) {
283         mClipTopAmount = clipTopAmount;
284     }
285 
getClipTopAmount()286     public int getClipTopAmount() {
287         return mClipTopAmount;
288     }
289 
setOnHeightChangedListener(OnHeightChangedListener listener)290     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
291         mOnHeightChangedListener = listener;
292     }
293 
294     /**
295      * @return Whether we can expand this views content.
296      */
isContentExpandable()297     public boolean isContentExpandable() {
298         return false;
299     }
300 
notifyHeightChanged(boolean needsAnimation)301     public void notifyHeightChanged(boolean needsAnimation) {
302         if (mOnHeightChangedListener != null) {
303             mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
304         }
305     }
306 
isTransparent()307     public boolean isTransparent() {
308         return false;
309     }
310 
311     /**
312      * Perform a remove animation on this view.
313      *
314      * @param duration The duration of the remove animation.
315      * @param translationDirection The direction value from [-1 ... 1] indicating in which the
316      *                             animation should be performed. A value of -1 means that The
317      *                             remove animation should be performed upwards,
318      *                             such that the  child appears to be going away to the top. 1
319      *                             Should mean the opposite.
320      * @param onFinishedRunnable A runnable which should be run when the animation is finished.
321      */
performRemoveAnimation(long duration, float translationDirection, Runnable onFinishedRunnable)322     public abstract void performRemoveAnimation(long duration, float translationDirection,
323             Runnable onFinishedRunnable);
324 
performAddAnimation(long delay, long duration)325     public abstract void performAddAnimation(long delay, long duration);
326 
setBelowSpeedBump(boolean below)327     public void setBelowSpeedBump(boolean below) {
328     }
329 
onHeightReset()330     public void onHeightReset() {
331         if (mOnHeightChangedListener != null) {
332             mOnHeightChangedListener.onReset(this);
333         }
334     }
335 
336     /**
337      * This method returns the drawing rect for the view which is different from the regular
338      * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
339      * position 0 and usually the translation is neglected. Since we are manually clipping this
340      * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
341      * ensure that accessibility and focusing work correctly.
342      *
343      * @param outRect The (scrolled) drawing bounds of the view.
344      */
345     @Override
getDrawingRect(Rect outRect)346     public void getDrawingRect(Rect outRect) {
347         super.getDrawingRect(outRect);
348         outRect.left += getTranslationX();
349         outRect.right += getTranslationX();
350         outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
351         outRect.top += getTranslationY() + getClipTopAmount();
352     }
353 
354     @Override
getBoundsOnScreen(Rect outRect, boolean clipToParent)355     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
356         super.getBoundsOnScreen(outRect, clipToParent);
357         outRect.bottom = outRect.top + getActualHeight();
358         outRect.top += getClipTopOptimization();
359     }
360 
getContentHeight()361     public int getContentHeight() {
362         return mActualHeight - getBottomDecorHeight();
363     }
364 
365     /**
366      * @return whether the given child can be ignored for layouting and measuring purposes
367      */
isChildInvisible(View child)368     protected boolean isChildInvisible(View child) {
369         return false;
370     }
371 
areChildrenExpanded()372     public boolean areChildrenExpanded() {
373         return false;
374     }
375 
updateClipping()376     private void updateClipping() {
377         int top = mClipTopOptimization;
378         if (top >= getActualHeight()) {
379             top = getActualHeight() - 1;
380         }
381         mClipRect.set(0, top, getWidth(), getActualHeight());
382         setClipBounds(mClipRect);
383     }
384 
getClipTopOptimization()385     public int getClipTopOptimization() {
386         return mClipTopOptimization;
387     }
388 
389     /**
390      * Set that the view will be clipped by a given amount from the top. Contrary to
391      * {@link #setClipTopAmount} this amount doesn't effect shadows and the background.
392      *
393      * @param clipTopOptimization the amount to clip from the top
394      */
setClipTopOptimization(int clipTopOptimization)395     public void setClipTopOptimization(int clipTopOptimization) {
396         mClipTopOptimization = clipTopOptimization;
397         updateClipping();
398     }
399 
willBeGone()400     public boolean willBeGone() {
401         return mWillBeGone;
402     }
403 
setWillBeGone(boolean willBeGone)404     public void setWillBeGone(boolean willBeGone) {
405         mWillBeGone = willBeGone;
406     }
407 
getMinClipTopAmount()408     public int getMinClipTopAmount() {
409         return mMinClipTopAmount;
410     }
411 
setMinClipTopAmount(int minClipTopAmount)412     public void setMinClipTopAmount(int minClipTopAmount) {
413         mMinClipTopAmount = minClipTopAmount;
414     }
415 
416     /**
417      * A listener notifying when {@link #getActualHeight} changes.
418      */
419     public interface OnHeightChangedListener {
420 
421         /**
422          * @param view the view for which the height changed, or {@code null} if just the top
423          *             padding or the padding between the elements changed
424          * @param needsAnimation whether the view height needs to be animated
425          */
onHeightChanged(ExpandableView view, boolean needsAnimation)426         void onHeightChanged(ExpandableView view, boolean needsAnimation);
427 
428         /**
429          * Called when the view is reset and therefore the height will change abruptly
430          *
431          * @param view The view which was reset.
432          */
onReset(ExpandableView view)433         void onReset(ExpandableView view);
434     }
435 }
436