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.content.res.Configuration;
22 import android.graphics.Paint;
23 import android.graphics.Rect;
24 import android.util.AttributeSet;
25 import android.util.IndentingPrintWriter;
26 import android.util.Log;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.view.ViewParent;
30 import android.widget.FrameLayout;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 
35 import com.android.app.animation.Interpolators;
36 import com.android.systemui.Dumpable;
37 import com.android.systemui.R;
38 import com.android.systemui.statusbar.StatusBarIconView;
39 import com.android.systemui.statusbar.notification.Roundable;
40 import com.android.systemui.statusbar.notification.RoundableState;
41 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
42 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
43 import com.android.systemui.util.Compile;
44 import com.android.systemui.util.DumpUtilsKt;
45 
46 import java.io.PrintWriter;
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * An abstract view for expandable views.
52  */
53 public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable {
54     private static final String TAG = "ExpandableView";
55     /** whether the dump() for this class should include verbose details */
56     protected static final boolean DUMP_VERBOSE =
57             Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
58 
59     private RoundableState mRoundableState = null;
60     protected OnHeightChangedListener mOnHeightChangedListener;
61     private int mActualHeight;
62     protected int mClipTopAmount;
63     protected int mClipBottomAmount;
64     protected int mMinimumHeightForClipping = 0;
65     protected float mExtraWidthForClipping = 0;
66     private ArrayList<View> mMatchParentViews = new ArrayList<View>();
67     private static Rect mClipRect = new Rect();
68     private boolean mWillBeGone;
69     private boolean mClipToActualHeight = true;
70     private boolean mChangingPosition = false;
71     private ViewGroup mTransientContainer;
72     private boolean mInShelf;
73     private boolean mTransformingInShelf;
74     protected float mContentTransformationAmount;
75     protected boolean mIsLastChild;
76     protected int mContentShift;
77     @NonNull private final ExpandableViewState mViewState;
78     private float mContentTranslation;
79     protected boolean mLastInSection;
80     protected boolean mFirstInSection;
81 
ExpandableView(Context context, AttributeSet attrs)82     public ExpandableView(Context context, AttributeSet attrs) {
83         super(context, attrs);
84         mViewState = createExpandableViewState();
85         initDimens();
86     }
87 
88     @Override
getRoundableState()89     public RoundableState getRoundableState() {
90         if (mRoundableState == null) {
91             mRoundableState = new RoundableState(this, this, 0f);
92         }
93         return mRoundableState;
94     }
95 
96     @Override
getClipHeight()97     public int getClipHeight() {
98         int clipHeight = Math.max(mActualHeight - mClipTopAmount - mClipBottomAmount, 0);
99         return Math.max(clipHeight, mMinimumHeightForClipping);
100     }
101 
initDimens()102     private void initDimens() {
103         mContentShift = getResources().getDimensionPixelSize(
104                 R.dimen.shelf_transform_content_shift);
105     }
106 
107     @Override
onConfigurationChanged(Configuration newConfig)108     protected void onConfigurationChanged(Configuration newConfig) {
109         super.onConfigurationChanged(newConfig);
110         initDimens();
111     }
112 
113     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)114     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
115         final int givenHeight = MeasureSpec.getSize(heightMeasureSpec);
116         final int viewHorizontalPadding = getPaddingStart() + getPaddingEnd();
117 
118         // Max height is as large as possible, unless otherwise requested
119         int ownMaxHeight = Integer.MAX_VALUE;
120         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
121         if (heightMode != MeasureSpec.UNSPECIFIED && givenHeight != 0) {
122             // Set our max height to what was requested from the parent
123             ownMaxHeight = Math.min(givenHeight, ownMaxHeight);
124         }
125 
126         // height of the largest child
127         int maxChildHeight = 0;
128         int atMostOwnMaxHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
129         int childCount = getChildCount();
130         for (int i = 0; i < childCount; i++) {
131             View child = getChildAt(i);
132             if (child.getVisibility() == GONE) {
133                 continue;
134             }
135             int childHeightSpec = atMostOwnMaxHeightSpec;
136             ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
137             if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
138                 if (layoutParams.height >= 0) {
139                     // If an actual height is set, cap it to the max height
140                     childHeightSpec = MeasureSpec.makeMeasureSpec(
141                             Math.min(layoutParams.height, ownMaxHeight),
142                             MeasureSpec.EXACTLY);
143                 }
144                 child.measure(getChildMeasureSpec(
145                         widthMeasureSpec, viewHorizontalPadding, layoutParams.width),
146                         childHeightSpec);
147                 int childHeight = child.getMeasuredHeight();
148                 maxChildHeight = Math.max(maxChildHeight, childHeight);
149             } else {
150                 mMatchParentViews.add(child);
151             }
152         }
153 
154         // Set our own height to the given height, or the height of the largest child
155         int ownHeight = heightMode == MeasureSpec.EXACTLY
156                 ? givenHeight
157                 : Math.min(ownMaxHeight, maxChildHeight);
158         int exactlyOwnHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
159 
160         // Now that we know our own height, measure the children that are MATCH_PARENT
161         for (View child : mMatchParentViews) {
162             child.measure(getChildMeasureSpec(
163                     widthMeasureSpec, viewHorizontalPadding, child.getLayoutParams().width),
164                     exactlyOwnHeightSpec);
165         }
166         mMatchParentViews.clear();
167 
168         // Finish up
169         int width = MeasureSpec.getSize(widthMeasureSpec);
170         setMeasuredDimension(width, ownHeight);
171     }
172 
173     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)174     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
175         super.onLayout(changed, left, top, right, bottom);
176         updateClipping();
177     }
178 
179     @Override
pointInView(float localX, float localY, float slop)180     public boolean pointInView(float localX, float localY, float slop) {
181         float top = Math.max(0, mClipTopAmount);
182         float bottom = mActualHeight;
183         return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) &&
184                 localY < (bottom + slop);
185     }
186 
187     /**
188      * @return if this view needs to be clipped to the shelf
189      */
needsClippingToShelf()190     public boolean needsClippingToShelf() {
191         return true;
192     }
193 
194 
isPinned()195     public boolean isPinned() {
196         return false;
197     }
198 
isHeadsUpAnimatingAway()199     public boolean isHeadsUpAnimatingAway() {
200         return false;
201     }
202 
203     /**
204      * Sets the actual height of this notification. This is different than the laid out
205      * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
206      *
207      * @param actualHeight The height of this notification.
208      * @param notifyListeners Whether the listener should be informed about the change.
209      */
setActualHeight(int actualHeight, boolean notifyListeners)210     public void setActualHeight(int actualHeight, boolean notifyListeners) {
211         if (mActualHeight != actualHeight) {
212             mActualHeight = actualHeight;
213             updateClipping();
214             if (notifyListeners) {
215                 notifyHeightChanged(false  /* needsAnimation */);
216             }
217         }
218     }
219 
setActualHeight(int actualHeight)220     public void setActualHeight(int actualHeight) {
221         setActualHeight(actualHeight, true /* notifyListeners */);
222     }
223 
224     /**
225      * See {@link #setActualHeight}.
226      *
227      * @return The current actual height of this notification.
228      */
getActualHeight()229     public int getActualHeight() {
230         return mActualHeight;
231     }
232 
isExpandAnimationRunning()233     public boolean isExpandAnimationRunning() {
234         return false;
235     }
236 
237     /**
238      * @return The maximum height of this notification.
239      */
getMaxContentHeight()240     public int getMaxContentHeight() {
241         return getHeight();
242     }
243 
244     /**
245      * @return The minimum content height of this notification. This also respects the temporary
246      * states of the view.
247      */
getMinHeight()248     public int getMinHeight() {
249         return getMinHeight(false /* ignoreTemporaryStates */);
250     }
251 
252     /**
253      * Get the minimum height of this view.
254      *
255      * @param ignoreTemporaryStates should temporary states be ignored like the guts or heads-up.
256      *
257      * @return The minimum height that this view needs.
258      */
getMinHeight(boolean ignoreTemporaryStates)259     public int getMinHeight(boolean ignoreTemporaryStates) {
260         return getHeight();
261     }
262 
263     /**
264      * @return The collapsed height of this view. Note that this might be different
265      * than {@link #getMinHeight()} because some elements like groups may have different sizes when
266      * they are system expanded.
267      */
getCollapsedHeight()268     public int getCollapsedHeight() {
269         return getHeight();
270     }
271 
272     /**
273      * Sets the notification as dimmed. The default implementation does nothing.
274      *
275      * @param dimmed Whether the notification should be dimmed.
276      * @param fade Whether an animation should be played to change the state.
277      */
setDimmed(boolean dimmed, boolean fade)278     public void setDimmed(boolean dimmed, boolean fade) {
279     }
280 
isRemoved()281     public boolean isRemoved() {
282         return false;
283     }
284 
285     /**
286      * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
287      * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
288      * of a stack scroller update such that the updated intrinsic height (which is dependent on
289      * whether private or public layout is showing) gets taken into account into all layout
290      * calculations.
291      */
setHideSensitiveForIntrinsicHeight(boolean hideSensitive)292     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
293     }
294 
295     /**
296      * Sets whether the notification should hide its private contents if it is sensitive.
297      */
setHideSensitive(boolean hideSensitive, boolean animated, long delay, long duration)298     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
299             long duration) {
300     }
301 
getHeightWithoutLockscreenConstraints()302     public int getHeightWithoutLockscreenConstraints() {
303         // ExpandableNotificationRow overrides this.
304         return getHeight();
305     }
306 
307     /**
308      * @return The desired notification height.
309      */
getIntrinsicHeight()310     public int getIntrinsicHeight() {
311         return getHeight();
312     }
313 
314     /**
315      * Sets the amount this view should be clipped from the top. This is used when an expanded
316      * notification is scrolling in the top or bottom stack.
317      *
318      * @param clipTopAmount The amount of pixels this view should be clipped from top.
319      */
setClipTopAmount(int clipTopAmount)320     public void setClipTopAmount(int clipTopAmount) {
321         mClipTopAmount = clipTopAmount;
322         updateClipping();
323     }
324 
325     /**
326      * Set the amount the the notification is clipped on the bottom in addition to the regular
327      * clipping. This is mainly used to clip something in a non-animated way without changing the
328      * actual height of the notification and is purely visual.
329      *
330      * @param clipBottomAmount the amount to clip.
331      */
setClipBottomAmount(int clipBottomAmount)332     public void setClipBottomAmount(int clipBottomAmount) {
333         mClipBottomAmount = clipBottomAmount;
334         updateClipping();
335     }
336 
getClipTopAmount()337     public int getClipTopAmount() {
338         return mClipTopAmount;
339     }
340 
getClipBottomAmount()341     public int getClipBottomAmount() {
342         return mClipBottomAmount;
343     }
344 
setOnHeightChangedListener(OnHeightChangedListener listener)345     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
346         mOnHeightChangedListener = listener;
347     }
348 
349     /**
350      * @return Whether we can expand this views content.
351      */
isContentExpandable()352     public boolean isContentExpandable() {
353         return false;
354     }
355 
notifyHeightChanged(boolean needsAnimation)356     public void notifyHeightChanged(boolean needsAnimation) {
357         if (mOnHeightChangedListener != null) {
358             mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
359         }
360     }
361 
isTransparent()362     public boolean isTransparent() {
363         return false;
364     }
365 
366     /**
367      * Perform a remove animation on this view.
368      * @param duration The duration of the remove animation.
369      * @param delay The delay of the animation
370      * @param translationDirection The direction value from [-1 ... 1] indicating in which the
371      *                             animation should be performed. A value of -1 means that The
372      *                             remove animation should be performed upwards,
373      *                             such that the  child appears to be going away to the top. 1
374      *                             Should mean the opposite.
375      * @param isHeadsUpAnimation Is this a headsUp animation.
376      * @param onFinishedRunnable A runnable which should be run when the animation is finished.
377      * @param animationListener An animation listener to add to the animation.
378      *
379      * @return The additional delay, in milliseconds, that this view needs to add before the
380      * animation starts.
381      */
performRemoveAnimation(long duration, long delay, float translationDirection, boolean isHeadsUpAnimation, Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener)382     public abstract long performRemoveAnimation(long duration,
383             long delay, float translationDirection, boolean isHeadsUpAnimation,
384             Runnable onFinishedRunnable,
385             AnimatorListenerAdapter animationListener);
386 
performAddAnimation(long delay, long duration, boolean isHeadsUpAppear)387     public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) {
388         performAddAnimation(delay, duration, isHeadsUpAppear, null);
389     }
390 
performAddAnimation(long delay, long duration, boolean isHeadsUpAppear, Runnable onEndRunnable)391     public abstract void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear,
392             Runnable onEndRunnable);
393 
394     /**
395      * Set the notification appearance to be below the speed bump.
396      * @param below true if it is below.
397      */
setBelowSpeedBump(boolean below)398     public void setBelowSpeedBump(boolean below) {
399     }
400 
getPinnedHeadsUpHeight()401     public int getPinnedHeadsUpHeight() {
402         return getIntrinsicHeight();
403     }
404 
405 
406     /**
407      * Sets the translation of the view.
408      */
setTranslation(float translation)409     public void setTranslation(float translation) {
410         setTranslationX(translation);
411     }
412 
413     /**
414      * Gets the translation of the view.
415      */
getTranslation()416     public float getTranslation() {
417         return getTranslationX();
418     }
419 
onHeightReset()420     public void onHeightReset() {
421         if (mOnHeightChangedListener != null) {
422             mOnHeightChangedListener.onReset(this);
423         }
424     }
425 
426     /**
427      * This method returns the drawing rect for the view which is different from the regular
428      * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
429      * position 0 and usually the translation is neglected. Since we are manually clipping this
430      * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
431      * ensure that accessibility and focusing work correctly.
432      *
433      * @param outRect The (scrolled) drawing bounds of the view.
434      */
435     @Override
getDrawingRect(Rect outRect)436     public void getDrawingRect(Rect outRect) {
437         super.getDrawingRect(outRect);
438         outRect.left += getTranslationX();
439         outRect.right += getTranslationX();
440         outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
441         outRect.top += getTranslationY() + getClipTopAmount();
442     }
443 
444     @Override
getBoundsOnScreen(Rect outRect, boolean clipToParent)445     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
446         super.getBoundsOnScreen(outRect, clipToParent);
447         if (getTop() + getTranslationY() < 0) {
448             // We got clipped to the parent here - make sure we undo that.
449             outRect.top += getTop() + getTranslationY();
450         }
451         outRect.bottom = outRect.top + getActualHeight();
452         outRect.top += Math.max(0, getClipTopAmount());
453     }
454 
isSummaryWithChildren()455     public boolean isSummaryWithChildren() {
456         return false;
457     }
458 
areChildrenExpanded()459     public boolean areChildrenExpanded() {
460         return false;
461     }
462 
updateClipping()463     protected void updateClipping() {
464         if (mClipToActualHeight && shouldClipToActualHeight()) {
465             int top = getClipTopAmount();
466             int bottom = Math.max(Math.max(getActualHeight()
467                     - mClipBottomAmount, top), mMinimumHeightForClipping);
468             mClipRect.set(Integer.MIN_VALUE, top, Integer.MAX_VALUE, bottom);
469             setClipBounds(mClipRect);
470         } else {
471             setClipBounds(null);
472         }
473     }
474 
setMinimumHeightForClipping(int minimumHeightForClipping)475     public void setMinimumHeightForClipping(int minimumHeightForClipping) {
476         mMinimumHeightForClipping = minimumHeightForClipping;
477         updateClipping();
478     }
479 
setExtraWidthForClipping(float extraWidthForClipping)480     public void setExtraWidthForClipping(float extraWidthForClipping) {
481         mExtraWidthForClipping = extraWidthForClipping;
482     }
483 
getHeaderVisibleAmount()484     public float getHeaderVisibleAmount() {
485         return 1.0f;
486     }
487 
shouldClipToActualHeight()488     protected boolean shouldClipToActualHeight() {
489         return true;
490     }
491 
setClipToActualHeight(boolean clipToActualHeight)492     public void setClipToActualHeight(boolean clipToActualHeight) {
493         mClipToActualHeight = clipToActualHeight;
494         updateClipping();
495     }
496 
willBeGone()497     public boolean willBeGone() {
498         return mWillBeGone;
499     }
500 
setWillBeGone(boolean willBeGone)501     public void setWillBeGone(boolean willBeGone) {
502         mWillBeGone = willBeGone;
503     }
504 
505     @Override
setLayerType(int layerType, Paint paint)506     public void setLayerType(int layerType, Paint paint) {
507         // Allow resetting the layerType to NONE regardless of overlappingRendering
508         if (layerType == LAYER_TYPE_NONE || hasOverlappingRendering()) {
509             super.setLayerType(layerType, paint);
510         }
511     }
512 
513     @Override
hasOverlappingRendering()514     public boolean hasOverlappingRendering() {
515         // Otherwise it will be clipped
516         return super.hasOverlappingRendering() && getActualHeight() <= getHeight();
517     }
518 
mustStayOnScreen()519     public boolean mustStayOnScreen() {
520         return false;
521     }
522 
setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, int outlineTranslation)523     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
524             int outlineTranslation) {
525     }
526 
getOutlineAlpha()527     public float getOutlineAlpha() {
528         return 0.0f;
529     }
530 
getOutlineTranslation()531     public int getOutlineTranslation() {
532         return 0;
533     }
534 
setChangingPosition(boolean changingPosition)535     public void setChangingPosition(boolean changingPosition) {
536         mChangingPosition = changingPosition;
537     }
538 
isChangingPosition()539     public boolean isChangingPosition() {
540         return mChangingPosition;
541     }
542 
543     /**
544      * Called when removing a view from its transient container, such as at the end of an animation.
545      * Generally, when operating on ExpandableView instances, this should be used rather than
546      * {@link ExpandableView#removeTransientView(View)} to ensure that the
547      * {@link #getTransientContainer() transient container} is correctly reset.
548      */
removeFromTransientContainer()549     public void removeFromTransientContainer() {
550         final ViewGroup transientContainer = getTransientContainer();
551         if (transientContainer == null) {
552             return;
553         }
554         final ViewParent parent = getParent();
555         if (parent != transientContainer) {
556             Log.w(TAG, "Expandable view " + this
557                     + " has transient container " + transientContainer
558                     + " but different parent " + parent);
559             setTransientContainer(null);
560             return;
561         }
562         transientContainer.removeTransientView(this);
563         setTransientContainer(null);
564     }
565 
566     /**
567      * Called before adding this view to a group, which would always throw an exception if this view
568      * has a different parent, so clean up the transient container and throw an exception if the
569      * parent isn't a transient container.  Provide as much detail as possible in the crash.
570      */
removeFromTransientContainerForAdditionTo(ViewGroup newParent)571     public void removeFromTransientContainerForAdditionTo(ViewGroup newParent) {
572         final ViewParent parent = getParent();
573         final ViewGroup transientContainer = getTransientContainer();
574         if (parent == null || parent == newParent) {
575             // If this view's current parent is null or the same as the new parent, the add will
576             // succeed as long as it's a true child, so just make sure the view isn't transient.
577             removeFromTransientContainer();
578             return;
579         }
580         if (transientContainer == null) {
581             throw new IllegalStateException("Can't add view " + this + " to container " + newParent
582                     + "; current parent " + parent + " is not a transient container");
583         }
584         if (transientContainer != parent) {
585             // Crash with details before addView() crashes without any; the view is being added
586             // to a different parent, and the transient container isn't the parent, so we can't
587             // even (safely) clean that up.
588             throw new IllegalStateException("Expandable view " + this
589                     + " has transient container " + transientContainer
590                     + " but different parent " + parent);
591         }
592         Log.w(TAG, "Removing view " + this + " from transient container "
593                 + transientContainer + " in preparation for moving to parent " + newParent);
594         transientContainer.removeTransientView(this);
595         setTransientContainer(null);
596     }
597 
setTransientContainer(ViewGroup transientContainer)598     public void setTransientContainer(ViewGroup transientContainer) {
599         mTransientContainer = transientContainer;
600     }
601 
getTransientContainer()602     public ViewGroup getTransientContainer() {
603         return mTransientContainer;
604     }
605 
606     /**
607      * @return true if the group's expansion state is changing, false otherwise.
608      */
isGroupExpansionChanging()609     public boolean isGroupExpansionChanging() {
610         return false;
611     }
612 
isGroupExpanded()613     public boolean isGroupExpanded() {
614         return false;
615     }
616 
setHeadsUpIsVisible()617     public void setHeadsUpIsVisible() {
618     }
619 
showingPulsing()620     public boolean showingPulsing() {
621         return false;
622     }
623 
isChildInGroup()624     public boolean isChildInGroup() {
625         return false;
626     }
627 
setActualHeightAnimating(boolean animating)628     public void setActualHeightAnimating(boolean animating) {}
629 
630     @NonNull
createExpandableViewState()631     protected ExpandableViewState createExpandableViewState() {
632         return new ExpandableViewState();
633     }
634 
635     /** Sets {@link ExpandableViewState} to default state. */
resetViewState()636     public ExpandableViewState resetViewState() {
637         // initialize with the default values of the view
638         mViewState.height = getIntrinsicHeight();
639         mViewState.gone = getVisibility() == View.GONE;
640         mViewState.setAlpha(1f);
641         mViewState.notGoneIndex = -1;
642         mViewState.setXTranslation(getTranslationX());
643         mViewState.hidden = false;
644         mViewState.setScaleX(getScaleX());
645         mViewState.setScaleY(getScaleY());
646         mViewState.inShelf = false;
647         mViewState.headsUpIsVisible = false;
648 
649         // handling reset for child notifications
650         if (this instanceof ExpandableNotificationRow) {
651             ExpandableNotificationRow row = (ExpandableNotificationRow) this;
652             List<ExpandableNotificationRow> children = row.getAttachedChildren();
653             if (row.isSummaryWithChildren() && children != null) {
654                 for (ExpandableNotificationRow childRow : children) {
655                     childRow.resetViewState();
656                 }
657             }
658         }
659 
660         return mViewState;
661     }
662 
663     /**
664      * Get the {@link ExpandableViewState} associated with the view.
665      *
666      * @return the ExpandableView's view state.
667      */
getViewState()668     @NonNull public ExpandableViewState getViewState() {
669         return mViewState;
670     }
671 
672     /** Applies internal {@link ExpandableViewState} to this view. */
applyViewState()673     public void applyViewState() {
674         if (!mViewState.gone) {
675             mViewState.applyToView(this);
676         }
677     }
678 
679     /**
680      * @return whether the current view doesn't add height to the overall content. This means that
681      * if it is added to a list of items, its content will still have the same height.
682      * An example is the notification shelf, that is always placed on top of another view.
683      */
hasNoContentHeight()684     public boolean hasNoContentHeight() {
685         return false;
686     }
687 
688     /**
689      * @param inShelf whether the view is currently fully in the notification shelf.
690      */
setInShelf(boolean inShelf)691     public void setInShelf(boolean inShelf) {
692         mInShelf = inShelf;
693     }
694 
isInShelf()695     public boolean isInShelf() {
696         return mInShelf;
697     }
698 
getShelfIcon()699     public @Nullable StatusBarIconView getShelfIcon() {
700         return null;
701     }
702 
703     /**
704      * @return get the transformation target of the shelf, which usually is the icon
705      */
getShelfTransformationTarget()706     public View getShelfTransformationTarget() {
707         return null;
708     }
709 
710     /**
711      * Get the relative top padding of a view relative to this view. This recursively walks up the
712      * hierarchy and does the corresponding measuring.
713      *
714      * @param view the view to get the padding for. The requested view has to be a child of this
715      *             notification.
716      * @return the toppadding
717      */
getRelativeTopPadding(View view)718     public int getRelativeTopPadding(View view) {
719         int topPadding = 0;
720         while (view.getParent() instanceof ViewGroup) {
721             topPadding += view.getTop();
722             view = (View) view.getParent();
723             if (view == this) {
724                 return topPadding;
725             }
726         }
727         return topPadding;
728     }
729 
730 
731     /**
732      * Get the relative start padding of a view relative to this view. This recursively walks up the
733      * hierarchy and does the corresponding measuring.
734      *
735      * @param view the view to get the padding for. The requested view has to be a child of this
736      *             notification.
737      * @return the start padding
738      */
getRelativeStartPadding(View view)739     public int getRelativeStartPadding(View view) {
740         boolean isRtl = isLayoutRtl();
741         int startPadding = 0;
742         while (view.getParent() instanceof ViewGroup) {
743             View parent = (View) view.getParent();
744             startPadding += isRtl ? parent.getWidth() - view.getRight() : view.getLeft();
745             view = parent;
746             if (view == this) {
747                 return startPadding;
748             }
749         }
750         return startPadding;
751     }
752 
753     /**
754      * Set how much this notification is transformed into the shelf.
755      *
756      * @param contentTransformationAmount A value from 0 to 1 indicating how much we are transformed
757      *                                 to the content away
758      * @param isLastChild is this the last child in the list. If true, then the transformation is
759      *                    different since its content fades out.
760      */
setContentTransformationAmount(float contentTransformationAmount, boolean isLastChild)761     public void setContentTransformationAmount(float contentTransformationAmount,
762             boolean isLastChild) {
763         boolean changeTransformation = isLastChild != mIsLastChild;
764         changeTransformation |= mContentTransformationAmount != contentTransformationAmount;
765         mIsLastChild = isLastChild;
766         mContentTransformationAmount = contentTransformationAmount;
767         if (changeTransformation) {
768             updateContentTransformation();
769         }
770     }
771 
772     /**
773      * Update the content representation based on the amount we are transformed into the shelf.
774      */
updateContentTransformation()775     protected void updateContentTransformation() {
776         float translationY = -mContentTransformationAmount * getContentTransformationShift();
777         float contentAlpha = 1.0f - mContentTransformationAmount;
778         contentAlpha = Math.min(contentAlpha / 0.5f, 1.0f);
779         contentAlpha = Interpolators.ALPHA_OUT.getInterpolation(contentAlpha);
780         if (mIsLastChild) {
781             translationY *= 0.4f;
782         }
783         mContentTranslation = translationY;
784         applyContentTransformation(contentAlpha, translationY);
785     }
786 
787     /**
788      * @return how much the content shifts up when going into the shelf
789      */
getContentTransformationShift()790     protected float getContentTransformationShift() {
791         return mContentShift;
792     }
793 
794     /**
795      * Apply the contentTransformation when going into the shelf.
796      *
797      * @param contentAlpha The alpha that should be applied
798      * @param translationY the translationY that should be applied
799      */
applyContentTransformation(float contentAlpha, float translationY)800     protected void applyContentTransformation(float contentAlpha, float translationY) {
801     }
802 
803     /**
804      * @param transformingInShelf whether the view is currently transforming into the shelf in an
805      *                            animated way
806      */
setTransformingInShelf(boolean transformingInShelf)807     public void setTransformingInShelf(boolean transformingInShelf) {
808         mTransformingInShelf = transformingInShelf;
809     }
810 
isTransformingIntoShelf()811     public boolean isTransformingIntoShelf() {
812         return mTransformingInShelf;
813     }
814 
isAboveShelf()815     public boolean isAboveShelf() {
816         return false;
817     }
818 
hasExpandingChild()819     public boolean hasExpandingChild() {
820         return false;
821     }
822 
823     @Override
dump(PrintWriter pwOriginal, String[] args)824     public void dump(PrintWriter pwOriginal, String[] args) {
825         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
826         pw.println(getClass().getSimpleName());
827         DumpUtilsKt.withIncreasedIndent(pw, () -> {
828             ExpandableViewState viewState = getViewState();
829             if (viewState == null) {
830                 pw.println("no viewState!!!");
831             } else {
832                 viewState.dump(pw, args);
833                 pw.println();
834             }
835             if (DUMP_VERBOSE) {
836                 pw.println("mClipTopAmount: " + mClipTopAmount);
837                 pw.println("mClipBottomAmount " + mClipBottomAmount);
838                 pw.println("mClipToActualHeight: " + mClipToActualHeight);
839                 pw.println("mExtraWidthForClipping: " + mExtraWidthForClipping);
840                 pw.println("mMinimumHeightForClipping: " + mMinimumHeightForClipping);
841                 pw.println("getClipBounds(): " + getClipBounds());
842             }
843         });
844     }
845 
846     /**
847      * return the amount that the content is translated
848      */
getContentTranslation()849     public float getContentTranslation() {
850         return mContentTranslation;
851     }
852 
853     /** Sets whether this view is the first notification in a section. */
setFirstInSection(boolean firstInSection)854     public void setFirstInSection(boolean firstInSection) {
855         mFirstInSection = firstInSection;
856     }
857 
858     /** Sets whether this view is the last notification in a section. */
setLastInSection(boolean lastInSection)859     public void setLastInSection(boolean lastInSection) {
860         mLastInSection = lastInSection;
861     }
862 
isLastInSection()863     public boolean isLastInSection() {
864         return mLastInSection;
865     }
866 
isFirstInSection()867     public boolean isFirstInSection() {
868         return mFirstInSection;
869     }
870 
getHeadsUpHeightWithoutHeader()871     public int getHeadsUpHeightWithoutHeader() {
872         return getHeight();
873     }
874 
875     /**
876      * A listener notifying when {@link #getActualHeight} changes.
877      */
878     public interface OnHeightChangedListener {
879 
880         /**
881          * @param view the view for which the height changed, or {@code null} if just the top
882          *             padding or the padding between the elements changed
883          * @param needsAnimation whether the view height needs to be animated
884          */
onHeightChanged(ExpandableView view, boolean needsAnimation)885         void onHeightChanged(ExpandableView view, boolean needsAnimation);
886 
887         /**
888          * Called when the view is reset and therefore the height will change abruptly
889          *
890          * @param view The view which was reset.
891          */
onReset(ExpandableView view)892         void onReset(ExpandableView view);
893     }
894 }
895