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