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