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