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