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