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