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