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