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