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.Paint;
21 import android.graphics.Rect;
22 import android.util.AttributeSet;
23 import android.view.View;
24 import android.view.ViewGroup;
25 import android.widget.FrameLayout;
26 
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     protected OnHeightChangedListener mOnHeightChangedListener;
37     private int mActualHeight;
38     protected int mClipTopAmount;
39     private boolean mDark;
40     private ArrayList<View> mMatchParentViews = new ArrayList<View>();
41     private static Rect mClipRect = new Rect();
42     private boolean mWillBeGone;
43     private int mMinClipTopAmount = 0;
44     private boolean mClipToActualHeight = true;
45     private boolean mChangingPosition = false;
46     private ViewGroup mTransientContainer;
47 
ExpandableView(Context context, AttributeSet attrs)48     public ExpandableView(Context context, AttributeSet attrs) {
49         super(context, attrs);
50     }
51 
52     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)53     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
54         final int givenSize = MeasureSpec.getSize(heightMeasureSpec);
55         int ownMaxHeight = Integer.MAX_VALUE;
56         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
57         if (heightMode != MeasureSpec.UNSPECIFIED && givenSize != 0) {
58             ownMaxHeight = Math.min(givenSize, ownMaxHeight);
59         }
60         int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
61         int maxChildHeight = 0;
62         int childCount = getChildCount();
63         for (int i = 0; i < childCount; i++) {
64             View child = getChildAt(i);
65             if (child.getVisibility() == GONE) {
66                 continue;
67             }
68             int childHeightSpec = newHeightSpec;
69             ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
70             if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
71                 if (layoutParams.height >= 0) {
72                     // An actual height is set
73                     childHeightSpec = layoutParams.height > ownMaxHeight
74                         ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
75                         : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
76                 }
77                 child.measure(
78                         getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
79                         childHeightSpec);
80                 int childHeight = child.getMeasuredHeight();
81                 maxChildHeight = Math.max(maxChildHeight, childHeight);
82             } else {
83                 mMatchParentViews.add(child);
84             }
85         }
86         int ownHeight = heightMode == MeasureSpec.EXACTLY
87                 ? givenSize : Math.min(ownMaxHeight, maxChildHeight);
88         newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
89         for (View child : mMatchParentViews) {
90             child.measure(getChildMeasureSpec(
91                     widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
92                     newHeightSpec);
93         }
94         mMatchParentViews.clear();
95         int width = MeasureSpec.getSize(widthMeasureSpec);
96         setMeasuredDimension(width, ownHeight);
97     }
98 
99     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)100     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
101         super.onLayout(changed, left, top, right, bottom);
102         updateClipping();
103     }
104 
105     @Override
pointInView(float localX, float localY, float slop)106     public boolean pointInView(float localX, float localY, float slop) {
107         float top = mClipTopAmount;
108         float bottom = mActualHeight;
109         return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) &&
110                 localY < (bottom + slop);
111     }
112 
113     /**
114      * Sets the actual height of this notification. This is different than the laid out
115      * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
116      *
117      * @param actualHeight The height of this notification.
118      * @param notifyListeners Whether the listener should be informed about the change.
119      */
setActualHeight(int actualHeight, boolean notifyListeners)120     public void setActualHeight(int actualHeight, boolean notifyListeners) {
121         mActualHeight = actualHeight;
122         updateClipping();
123         if (notifyListeners) {
124             notifyHeightChanged(false  /* needsAnimation */);
125         }
126     }
127 
setActualHeight(int actualHeight)128     public void setActualHeight(int actualHeight) {
129         setActualHeight(actualHeight, true /* notifyListeners */);
130     }
131 
132     /**
133      * See {@link #setActualHeight}.
134      *
135      * @return The current actual height of this notification.
136      */
getActualHeight()137     public int getActualHeight() {
138         return mActualHeight;
139     }
140 
141     /**
142      * @return The maximum height of this notification.
143      */
getMaxContentHeight()144     public int getMaxContentHeight() {
145         return getHeight();
146     }
147 
148     /**
149      * @return The minimum content height of this notification.
150      */
getMinHeight()151     public int getMinHeight() {
152         return getHeight();
153     }
154 
155     /**
156      * @return The collapsed height of this view. Note that this might be different
157      * than {@link #getMinHeight()} because some elements like groups may have different sizes when
158      * they are system expanded.
159      */
getCollapsedHeight()160     public int getCollapsedHeight() {
161         return getHeight();
162     }
163 
164     /**
165      * Sets the notification as dimmed. The default implementation does nothing.
166      *
167      * @param dimmed Whether the notification should be dimmed.
168      * @param fade Whether an animation should be played to change the state.
169      */
setDimmed(boolean dimmed, boolean fade)170     public void setDimmed(boolean dimmed, boolean fade) {
171     }
172 
173     /**
174      * Sets the notification as dark. The default implementation does nothing.
175      *
176      * @param dark Whether the notification should be dark.
177      * @param fade Whether an animation should be played to change the state.
178      * @param delay If fading, the delay of the animation.
179      */
setDark(boolean dark, boolean fade, long delay)180     public void setDark(boolean dark, boolean fade, long delay) {
181         mDark = dark;
182     }
183 
isDark()184     public boolean isDark() {
185         return mDark;
186     }
187 
188     /**
189      * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
190      * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
191      * of a stack scroller update such that the updated intrinsic height (which is dependent on
192      * whether private or public layout is showing) gets taken into account into all layout
193      * calculations.
194      */
setHideSensitiveForIntrinsicHeight(boolean hideSensitive)195     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
196     }
197 
198     /**
199      * Sets whether the notification should hide its private contents if it is sensitive.
200      */
setHideSensitive(boolean hideSensitive, boolean animated, long delay, long duration)201     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
202             long duration) {
203     }
204 
205     /**
206      * @return The desired notification height.
207      */
getIntrinsicHeight()208     public int getIntrinsicHeight() {
209         return getHeight();
210     }
211 
212     /**
213      * Sets the amount this view should be clipped from the top. This is used when an expanded
214      * notification is scrolling in the top or bottom stack.
215      *
216      * @param clipTopAmount The amount of pixels this view should be clipped from top.
217      */
setClipTopAmount(int clipTopAmount)218     public void setClipTopAmount(int clipTopAmount) {
219         mClipTopAmount = clipTopAmount;
220         updateClipping();
221     }
222 
getClipTopAmount()223     public int getClipTopAmount() {
224         return mClipTopAmount;
225     }
226 
setOnHeightChangedListener(OnHeightChangedListener listener)227     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
228         mOnHeightChangedListener = listener;
229     }
230 
231     /**
232      * @return Whether we can expand this views content.
233      */
isContentExpandable()234     public boolean isContentExpandable() {
235         return false;
236     }
237 
notifyHeightChanged(boolean needsAnimation)238     public void notifyHeightChanged(boolean needsAnimation) {
239         if (mOnHeightChangedListener != null) {
240             mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
241         }
242     }
243 
isTransparent()244     public boolean isTransparent() {
245         return false;
246     }
247 
248     /**
249      * Perform a remove animation on this view.
250      *
251      * @param duration The duration of the remove animation.
252      * @param translationDirection The direction value from [-1 ... 1] indicating in which the
253      *                             animation should be performed. A value of -1 means that The
254      *                             remove animation should be performed upwards,
255      *                             such that the  child appears to be going away to the top. 1
256      *                             Should mean the opposite.
257      * @param onFinishedRunnable A runnable which should be run when the animation is finished.
258      */
performRemoveAnimation(long duration, float translationDirection, Runnable onFinishedRunnable)259     public abstract void performRemoveAnimation(long duration, float translationDirection,
260             Runnable onFinishedRunnable);
261 
performAddAnimation(long delay, long duration)262     public abstract void performAddAnimation(long delay, long duration);
263 
setBelowSpeedBump(boolean below)264     public void setBelowSpeedBump(boolean below) {
265     }
266 
267     /**
268      * Sets the translation of the view.
269      */
setTranslation(float translation)270     public void setTranslation(float translation) {
271         setTranslationX(translation);
272     }
273 
274     /**
275      * Gets the translation of the view.
276      */
getTranslation()277     public float getTranslation() {
278         return getTranslationX();
279     }
280 
onHeightReset()281     public void onHeightReset() {
282         if (mOnHeightChangedListener != null) {
283             mOnHeightChangedListener.onReset(this);
284         }
285     }
286 
287     /**
288      * This method returns the drawing rect for the view which is different from the regular
289      * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
290      * position 0 and usually the translation is neglected. Since we are manually clipping this
291      * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
292      * ensure that accessibility and focusing work correctly.
293      *
294      * @param outRect The (scrolled) drawing bounds of the view.
295      */
296     @Override
getDrawingRect(Rect outRect)297     public void getDrawingRect(Rect outRect) {
298         super.getDrawingRect(outRect);
299         outRect.left += getTranslationX();
300         outRect.right += getTranslationX();
301         outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
302         outRect.top += getTranslationY() + getClipTopAmount();
303     }
304 
305     @Override
getBoundsOnScreen(Rect outRect, boolean clipToParent)306     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
307         super.getBoundsOnScreen(outRect, clipToParent);
308         if (getTop() + getTranslationY() < 0) {
309             // We got clipped to the parent here - make sure we undo that.
310             outRect.top += getTop() + getTranslationY();
311         }
312         outRect.bottom = outRect.top + getActualHeight();
313         outRect.top += getClipTopAmount();
314     }
315 
isSummaryWithChildren()316     public boolean isSummaryWithChildren() {
317         return false;
318     }
319 
areChildrenExpanded()320     public boolean areChildrenExpanded() {
321         return false;
322     }
323 
updateClipping()324     private void updateClipping() {
325         if (mClipToActualHeight) {
326             int top = getClipTopAmount();
327             if (top >= getActualHeight()) {
328                 top = getActualHeight() - 1;
329             }
330             mClipRect.set(0, top, getWidth(), getActualHeight() + getExtraBottomPadding());
331             setClipBounds(mClipRect);
332         } else {
333             setClipBounds(null);
334         }
335     }
336 
setClipToActualHeight(boolean clipToActualHeight)337     public void setClipToActualHeight(boolean clipToActualHeight) {
338         mClipToActualHeight = clipToActualHeight;
339         updateClipping();
340     }
341 
willBeGone()342     public boolean willBeGone() {
343         return mWillBeGone;
344     }
345 
setWillBeGone(boolean willBeGone)346     public void setWillBeGone(boolean willBeGone) {
347         mWillBeGone = willBeGone;
348     }
349 
getMinClipTopAmount()350     public int getMinClipTopAmount() {
351         return mMinClipTopAmount;
352     }
353 
setMinClipTopAmount(int minClipTopAmount)354     public void setMinClipTopAmount(int minClipTopAmount) {
355         mMinClipTopAmount = minClipTopAmount;
356     }
357 
358     @Override
setLayerType(int layerType, Paint paint)359     public void setLayerType(int layerType, Paint paint) {
360         if (hasOverlappingRendering()) {
361             super.setLayerType(layerType, paint);
362         }
363     }
364 
365     @Override
hasOverlappingRendering()366     public boolean hasOverlappingRendering() {
367         // Otherwise it will be clipped
368         return super.hasOverlappingRendering() && getActualHeight() <= getHeight();
369     }
370 
getShadowAlpha()371     public float getShadowAlpha() {
372         return 0.0f;
373     }
374 
setShadowAlpha(float shadowAlpha)375     public void setShadowAlpha(float shadowAlpha) {
376     }
377 
378     /**
379      * @return an amount between 0 and 1 of increased padding that this child needs
380      */
getIncreasedPaddingAmount()381     public float getIncreasedPaddingAmount() {
382         return 0.0f;
383     }
384 
mustStayOnScreen()385     public boolean mustStayOnScreen() {
386         return false;
387     }
388 
setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, int outlineTranslation)389     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
390             int outlineTranslation) {
391     }
392 
getOutlineAlpha()393     public float getOutlineAlpha() {
394         return 0.0f;
395     }
396 
getOutlineTranslation()397     public int getOutlineTranslation() {
398         return 0;
399     }
400 
setChangingPosition(boolean changingPosition)401     public void setChangingPosition(boolean changingPosition) {
402         mChangingPosition = changingPosition;
403     }
404 
isChangingPosition()405     public boolean isChangingPosition() {
406         return mChangingPosition;
407     }
408 
setTransientContainer(ViewGroup transientContainer)409     public void setTransientContainer(ViewGroup transientContainer) {
410         mTransientContainer = transientContainer;
411     }
412 
getTransientContainer()413     public ViewGroup getTransientContainer() {
414         return mTransientContainer;
415     }
416 
417     /**
418      * @return padding used to alter how much of the view is clipped.
419      */
getExtraBottomPadding()420     public int getExtraBottomPadding() {
421         return 0;
422     }
423 
424     /**
425      * @return true if the group's expansion state is changing, false otherwise.
426      */
isGroupExpansionChanging()427     public boolean isGroupExpansionChanging() {
428         return false;
429     }
430 
isGroupExpanded()431     public boolean isGroupExpanded() {
432         return false;
433     }
434 
isChildInGroup()435     public boolean isChildInGroup() {
436         return false;
437     }
438 
setActualHeightAnimating(boolean animating)439     public void setActualHeightAnimating(boolean animating) {}
440 
441     /**
442      * A listener notifying when {@link #getActualHeight} changes.
443      */
444     public interface OnHeightChangedListener {
445 
446         /**
447          * @param view the view for which the height changed, or {@code null} if just the top
448          *             padding or the padding between the elements changed
449          * @param needsAnimation whether the view height needs to be animated
450          */
onHeightChanged(ExpandableView view, boolean needsAnimation)451         void onHeightChanged(ExpandableView view, boolean needsAnimation);
452 
453         /**
454          * Called when the view is reset and therefore the height will change abruptly
455          *
456          * @param view The view which was reset.
457          */
onReset(ExpandableView view)458         void onReset(ExpandableView view);
459     }
460 }
461