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