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