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.graphics.Paint; 22 import android.graphics.Rect; 23 import android.util.AttributeSet; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.widget.FrameLayout; 27 28 import androidx.annotation.Nullable; 29 30 import com.android.systemui.Dumpable; 31 import com.android.systemui.statusbar.notification.stack.ExpandableViewState; 32 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; 33 34 import java.io.FileDescriptor; 35 import java.io.PrintWriter; 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** 40 * An abstract view for expandable views. 41 */ 42 public abstract class ExpandableView extends FrameLayout implements Dumpable { 43 private static final String TAG = "ExpandableView"; 44 45 public static final float NO_ROUNDNESS = -1; 46 protected OnHeightChangedListener mOnHeightChangedListener; 47 private int mActualHeight; 48 protected int mClipTopAmount; 49 protected int mClipBottomAmount; 50 protected int mMinimumHeightForClipping = 0; 51 protected float mExtraWidthForClipping = 0; 52 private boolean mDark; 53 private ArrayList<View> mMatchParentViews = new ArrayList<View>(); 54 private static Rect mClipRect = new Rect(); 55 private boolean mWillBeGone; 56 private int mMinClipTopAmount = 0; 57 private boolean mClipToActualHeight = true; 58 private boolean mChangingPosition = false; 59 private ViewGroup mTransientContainer; 60 private boolean mInShelf; 61 private boolean mTransformingInShelf; 62 private final ExpandableViewState mViewState; 63 ExpandableView(Context context, AttributeSet attrs)64 public ExpandableView(Context context, AttributeSet attrs) { 65 super(context, attrs); 66 mViewState = createExpandableViewState(); 67 } 68 69 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)70 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 71 final int givenSize = MeasureSpec.getSize(heightMeasureSpec); 72 final int viewHorizontalPadding = getPaddingStart() + getPaddingEnd(); 73 int ownMaxHeight = Integer.MAX_VALUE; 74 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 75 if (heightMode != MeasureSpec.UNSPECIFIED && givenSize != 0) { 76 ownMaxHeight = Math.min(givenSize, ownMaxHeight); 77 } 78 int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST); 79 int maxChildHeight = 0; 80 int childCount = getChildCount(); 81 for (int i = 0; i < childCount; i++) { 82 View child = getChildAt(i); 83 if (child.getVisibility() == GONE) { 84 continue; 85 } 86 int childHeightSpec = newHeightSpec; 87 ViewGroup.LayoutParams layoutParams = child.getLayoutParams(); 88 if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) { 89 if (layoutParams.height >= 0) { 90 // An actual height is set 91 childHeightSpec = layoutParams.height > ownMaxHeight 92 ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY) 93 : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY); 94 } 95 child.measure(getChildMeasureSpec( 96 widthMeasureSpec, viewHorizontalPadding, layoutParams.width), 97 childHeightSpec); 98 int childHeight = child.getMeasuredHeight(); 99 maxChildHeight = Math.max(maxChildHeight, childHeight); 100 } else { 101 mMatchParentViews.add(child); 102 } 103 } 104 int ownHeight = heightMode == MeasureSpec.EXACTLY 105 ? givenSize : Math.min(ownMaxHeight, maxChildHeight); 106 newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY); 107 for (View child : mMatchParentViews) { 108 child.measure(getChildMeasureSpec( 109 widthMeasureSpec, viewHorizontalPadding, child.getLayoutParams().width), 110 newHeightSpec); 111 } 112 mMatchParentViews.clear(); 113 int width = MeasureSpec.getSize(widthMeasureSpec); 114 setMeasuredDimension(width, ownHeight); 115 } 116 117 @Override onLayout(boolean changed, int left, int top, int right, int bottom)118 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 119 super.onLayout(changed, left, top, right, bottom); 120 updateClipping(); 121 } 122 123 @Override pointInView(float localX, float localY, float slop)124 public boolean pointInView(float localX, float localY, float slop) { 125 float top = mClipTopAmount; 126 float bottom = mActualHeight; 127 return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) && 128 localY < (bottom + slop); 129 } 130 131 /** 132 * Sets the actual height of this notification. This is different than the laid out 133 * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding. 134 * 135 * @param actualHeight The height of this notification. 136 * @param notifyListeners Whether the listener should be informed about the change. 137 */ setActualHeight(int actualHeight, boolean notifyListeners)138 public void setActualHeight(int actualHeight, boolean notifyListeners) { 139 mActualHeight = actualHeight; 140 updateClipping(); 141 if (notifyListeners) { 142 notifyHeightChanged(false /* needsAnimation */); 143 } 144 } 145 146 /** 147 * Set the distance to the top roundness, from where we should start clipping a value above 148 * or equal to 0 is the effective distance, and if a value below 0 is received, there should 149 * be no clipping. 150 */ setDistanceToTopRoundness(float distanceToTopRoundness)151 public void setDistanceToTopRoundness(float distanceToTopRoundness) { 152 } 153 setActualHeight(int actualHeight)154 public void setActualHeight(int actualHeight) { 155 setActualHeight(actualHeight, true /* notifyListeners */); 156 } 157 158 /** 159 * See {@link #setActualHeight}. 160 * 161 * @return The current actual height of this notification. 162 */ getActualHeight()163 public int getActualHeight() { 164 return mActualHeight; 165 } 166 isExpandAnimationRunning()167 public boolean isExpandAnimationRunning() { 168 return false; 169 } 170 171 /** 172 * @return The maximum height of this notification. 173 */ getMaxContentHeight()174 public int getMaxContentHeight() { 175 return getHeight(); 176 } 177 178 /** 179 * @return The minimum content height of this notification. This also respects the temporary 180 * states of the view. 181 */ getMinHeight()182 public int getMinHeight() { 183 return getMinHeight(false /* ignoreTemporaryStates */); 184 } 185 186 /** 187 * Get the minimum height of this view. 188 * 189 * @param ignoreTemporaryStates should temporary states be ignored like the guts or heads-up. 190 * 191 * @return The minimum height that this view needs. 192 */ getMinHeight(boolean ignoreTemporaryStates)193 public int getMinHeight(boolean ignoreTemporaryStates) { 194 return getHeight(); 195 } 196 197 /** 198 * @return The collapsed height of this view. Note that this might be different 199 * than {@link #getMinHeight()} because some elements like groups may have different sizes when 200 * they are system expanded. 201 */ getCollapsedHeight()202 public int getCollapsedHeight() { 203 return getHeight(); 204 } 205 206 /** 207 * Sets the notification as dimmed. The default implementation does nothing. 208 * 209 * @param dimmed Whether the notification should be dimmed. 210 * @param fade Whether an animation should be played to change the state. 211 */ setDimmed(boolean dimmed, boolean fade)212 public void setDimmed(boolean dimmed, boolean fade) { 213 } 214 215 /** 216 * Sets the notification as dark. The default implementation does nothing. 217 * 218 * @param dark Whether the notification should be dark. 219 * @param fade Whether an animation should be played to change the state. 220 * @param delay If fading, the delay of the animation. 221 */ setDark(boolean dark, boolean fade, long delay)222 public void setDark(boolean dark, boolean fade, long delay) { 223 mDark = dark; 224 } 225 isDark()226 public boolean isDark() { 227 return mDark; 228 } 229 isRemoved()230 public boolean isRemoved() { 231 return false; 232 } 233 234 /** 235 * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about 236 * the upcoming state of hiding sensitive notifications. It gets called at the very beginning 237 * of a stack scroller update such that the updated intrinsic height (which is dependent on 238 * whether private or public layout is showing) gets taken into account into all layout 239 * calculations. 240 */ setHideSensitiveForIntrinsicHeight(boolean hideSensitive)241 public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) { 242 } 243 244 /** 245 * Sets whether the notification should hide its private contents if it is sensitive. 246 */ setHideSensitive(boolean hideSensitive, boolean animated, long delay, long duration)247 public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, 248 long duration) { 249 } 250 251 /** 252 * @return The desired notification height. 253 */ getIntrinsicHeight()254 public int getIntrinsicHeight() { 255 return getHeight(); 256 } 257 258 /** 259 * Sets the amount this view should be clipped from the top. This is used when an expanded 260 * notification is scrolling in the top or bottom stack. 261 * 262 * @param clipTopAmount The amount of pixels this view should be clipped from top. 263 */ setClipTopAmount(int clipTopAmount)264 public void setClipTopAmount(int clipTopAmount) { 265 mClipTopAmount = clipTopAmount; 266 updateClipping(); 267 } 268 269 /** 270 * Set the amount the the notification is clipped on the bottom in addition to the regular 271 * clipping. This is mainly used to clip something in a non-animated way without changing the 272 * actual height of the notification and is purely visual. 273 * 274 * @param clipBottomAmount the amount to clip. 275 */ setClipBottomAmount(int clipBottomAmount)276 public void setClipBottomAmount(int clipBottomAmount) { 277 mClipBottomAmount = clipBottomAmount; 278 updateClipping(); 279 } 280 getClipTopAmount()281 public int getClipTopAmount() { 282 return mClipTopAmount; 283 } 284 getClipBottomAmount()285 public int getClipBottomAmount() { 286 return mClipBottomAmount; 287 } 288 setOnHeightChangedListener(OnHeightChangedListener listener)289 public void setOnHeightChangedListener(OnHeightChangedListener listener) { 290 mOnHeightChangedListener = listener; 291 } 292 293 /** 294 * @return Whether we can expand this views content. 295 */ isContentExpandable()296 public boolean isContentExpandable() { 297 return false; 298 } 299 notifyHeightChanged(boolean needsAnimation)300 public void notifyHeightChanged(boolean needsAnimation) { 301 if (mOnHeightChangedListener != null) { 302 mOnHeightChangedListener.onHeightChanged(this, needsAnimation); 303 } 304 } 305 isTransparent()306 public boolean isTransparent() { 307 return false; 308 } 309 310 /** 311 * Perform a remove animation on this view. 312 * @param duration The duration of the remove animation. 313 * @param delay The delay of the animation 314 * @param translationDirection The direction value from [-1 ... 1] indicating in which the 315 * animation should be performed. A value of -1 means that The 316 * remove animation should be performed upwards, 317 * such that the child appears to be going away to the top. 1 318 * Should mean the opposite. 319 * @param isHeadsUpAnimation Is this a headsUp animation. 320 * @param endLocation The location where the horizonal heads up disappear animation should end. 321 * @param onFinishedRunnable A runnable which should be run when the animation is finished. 322 * @param animationListener An animation listener to add to the animation. 323 * 324 * @return The additional delay, in milliseconds, that this view needs to add before the 325 * animation starts. 326 */ performRemoveAnimation(long duration, long delay, float translationDirection, boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener)327 public abstract long performRemoveAnimation(long duration, 328 long delay, float translationDirection, boolean isHeadsUpAnimation, float endLocation, 329 Runnable onFinishedRunnable, 330 AnimatorListenerAdapter animationListener); 331 performAddAnimation(long delay, long duration, boolean isHeadsUpAppear)332 public abstract void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear); 333 334 /** 335 * Set the notification appearance to be below the speed bump. 336 * @param below true if it is below. 337 */ setBelowSpeedBump(boolean below)338 public void setBelowSpeedBump(boolean below) { 339 } 340 getPinnedHeadsUpHeight()341 public int getPinnedHeadsUpHeight() { 342 return getIntrinsicHeight(); 343 } 344 345 346 /** 347 * Sets the translation of the view. 348 */ setTranslation(float translation)349 public void setTranslation(float translation) { 350 setTranslationX(translation); 351 } 352 353 /** 354 * Gets the translation of the view. 355 */ getTranslation()356 public float getTranslation() { 357 return getTranslationX(); 358 } 359 onHeightReset()360 public void onHeightReset() { 361 if (mOnHeightChangedListener != null) { 362 mOnHeightChangedListener.onReset(this); 363 } 364 } 365 366 /** 367 * This method returns the drawing rect for the view which is different from the regular 368 * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at 369 * position 0 and usually the translation is neglected. Since we are manually clipping this 370 * view,we also need to subtract the clipTopAmount from the top. This is needed in order to 371 * ensure that accessibility and focusing work correctly. 372 * 373 * @param outRect The (scrolled) drawing bounds of the view. 374 */ 375 @Override getDrawingRect(Rect outRect)376 public void getDrawingRect(Rect outRect) { 377 super.getDrawingRect(outRect); 378 outRect.left += getTranslationX(); 379 outRect.right += getTranslationX(); 380 outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight()); 381 outRect.top += getTranslationY() + getClipTopAmount(); 382 } 383 384 @Override getBoundsOnScreen(Rect outRect, boolean clipToParent)385 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) { 386 super.getBoundsOnScreen(outRect, clipToParent); 387 if (getTop() + getTranslationY() < 0) { 388 // We got clipped to the parent here - make sure we undo that. 389 outRect.top += getTop() + getTranslationY(); 390 } 391 outRect.bottom = outRect.top + getActualHeight(); 392 outRect.top += getClipTopAmount(); 393 } 394 isSummaryWithChildren()395 public boolean isSummaryWithChildren() { 396 return false; 397 } 398 areChildrenExpanded()399 public boolean areChildrenExpanded() { 400 return false; 401 } 402 updateClipping()403 protected void updateClipping() { 404 if (mClipToActualHeight && shouldClipToActualHeight()) { 405 int top = getClipTopAmount(); 406 int bottom = Math.max(Math.max(getActualHeight() + getExtraBottomPadding() 407 - mClipBottomAmount, top), mMinimumHeightForClipping); 408 int halfExtraWidth = (int) (mExtraWidthForClipping / 2.0f); 409 mClipRect.set(-halfExtraWidth, top, getWidth() + halfExtraWidth, bottom); 410 setClipBounds(mClipRect); 411 } else { 412 setClipBounds(null); 413 } 414 } 415 setMinimumHeightForClipping(int minimumHeightForClipping)416 public void setMinimumHeightForClipping(int minimumHeightForClipping) { 417 mMinimumHeightForClipping = minimumHeightForClipping; 418 updateClipping(); 419 } 420 setExtraWidthForClipping(float extraWidthForClipping)421 public void setExtraWidthForClipping(float extraWidthForClipping) { 422 mExtraWidthForClipping = extraWidthForClipping; 423 updateClipping(); 424 } 425 getHeaderVisibleAmount()426 public float getHeaderVisibleAmount() { 427 return 1.0f; 428 } 429 shouldClipToActualHeight()430 protected boolean shouldClipToActualHeight() { 431 return true; 432 } 433 setClipToActualHeight(boolean clipToActualHeight)434 public void setClipToActualHeight(boolean clipToActualHeight) { 435 mClipToActualHeight = clipToActualHeight; 436 updateClipping(); 437 } 438 willBeGone()439 public boolean willBeGone() { 440 return mWillBeGone; 441 } 442 setWillBeGone(boolean willBeGone)443 public void setWillBeGone(boolean willBeGone) { 444 mWillBeGone = willBeGone; 445 } 446 getMinClipTopAmount()447 public int getMinClipTopAmount() { 448 return mMinClipTopAmount; 449 } 450 setMinClipTopAmount(int minClipTopAmount)451 public void setMinClipTopAmount(int minClipTopAmount) { 452 mMinClipTopAmount = minClipTopAmount; 453 } 454 455 @Override setLayerType(int layerType, Paint paint)456 public void setLayerType(int layerType, Paint paint) { 457 if (hasOverlappingRendering()) { 458 super.setLayerType(layerType, paint); 459 } 460 } 461 462 @Override hasOverlappingRendering()463 public boolean hasOverlappingRendering() { 464 // Otherwise it will be clipped 465 return super.hasOverlappingRendering() && getActualHeight() <= getHeight(); 466 } 467 468 /** 469 * @return an amount between -1 and 1 of increased padding that this child needs. 1 means it 470 * needs a full increased padding while -1 means it needs no padding at all. For 0.0f the normal 471 * padding is applied. 472 */ getIncreasedPaddingAmount()473 public float getIncreasedPaddingAmount() { 474 return 0.0f; 475 } 476 mustStayOnScreen()477 public boolean mustStayOnScreen() { 478 return false; 479 } 480 setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, int outlineTranslation)481 public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, 482 int outlineTranslation) { 483 } 484 getOutlineAlpha()485 public float getOutlineAlpha() { 486 return 0.0f; 487 } 488 getOutlineTranslation()489 public int getOutlineTranslation() { 490 return 0; 491 } 492 setChangingPosition(boolean changingPosition)493 public void setChangingPosition(boolean changingPosition) { 494 mChangingPosition = changingPosition; 495 } 496 isChangingPosition()497 public boolean isChangingPosition() { 498 return mChangingPosition; 499 } 500 setTransientContainer(ViewGroup transientContainer)501 public void setTransientContainer(ViewGroup transientContainer) { 502 mTransientContainer = transientContainer; 503 } 504 getTransientContainer()505 public ViewGroup getTransientContainer() { 506 return mTransientContainer; 507 } 508 509 /** 510 * @return padding used to alter how much of the view is clipped. 511 */ getExtraBottomPadding()512 public int getExtraBottomPadding() { 513 return 0; 514 } 515 516 /** 517 * @return true if the group's expansion state is changing, false otherwise. 518 */ isGroupExpansionChanging()519 public boolean isGroupExpansionChanging() { 520 return false; 521 } 522 isGroupExpanded()523 public boolean isGroupExpanded() { 524 return false; 525 } 526 setHeadsUpIsVisible()527 public void setHeadsUpIsVisible() { 528 } 529 showingAmbientPulsing()530 public boolean showingAmbientPulsing() { 531 return false; 532 } 533 isChildInGroup()534 public boolean isChildInGroup() { 535 return false; 536 } 537 setActualHeightAnimating(boolean animating)538 public void setActualHeightAnimating(boolean animating) {} 539 createExpandableViewState()540 protected ExpandableViewState createExpandableViewState() { 541 return new ExpandableViewState(); 542 } 543 544 /** Sets {@link ExpandableViewState} to default state. */ resetViewState()545 public ExpandableViewState resetViewState() { 546 // initialize with the default values of the view 547 mViewState.height = getIntrinsicHeight(); 548 mViewState.gone = getVisibility() == View.GONE; 549 mViewState.alpha = 1f; 550 mViewState.notGoneIndex = -1; 551 mViewState.xTranslation = getTranslationX(); 552 mViewState.hidden = false; 553 mViewState.scaleX = getScaleX(); 554 mViewState.scaleY = getScaleY(); 555 mViewState.inShelf = false; 556 mViewState.headsUpIsVisible = false; 557 558 // handling reset for child notifications 559 if (this instanceof ExpandableNotificationRow) { 560 ExpandableNotificationRow row = (ExpandableNotificationRow) this; 561 List<ExpandableNotificationRow> children = row.getNotificationChildren(); 562 if (row.isSummaryWithChildren() && children != null) { 563 for (ExpandableNotificationRow childRow : children) { 564 childRow.resetViewState(); 565 } 566 } 567 } 568 569 return mViewState; 570 } 571 getViewState()572 @Nullable public ExpandableViewState getViewState() { 573 return mViewState; 574 } 575 576 /** Applies internal {@link ExpandableViewState} to this view. */ applyViewState()577 public void applyViewState() { 578 if (!mViewState.gone) { 579 mViewState.applyToView(this); 580 } 581 } 582 583 /** 584 * @return whether the current view doesn't add height to the overall content. This means that 585 * if it is added to a list of items, it's content will still have the same height. 586 * An example is the notification shelf, that is always placed on top of another view. 587 */ hasNoContentHeight()588 public boolean hasNoContentHeight() { 589 return false; 590 } 591 592 /** 593 * @param inShelf whether the view is currently fully in the notification shelf. 594 */ setInShelf(boolean inShelf)595 public void setInShelf(boolean inShelf) { 596 mInShelf = inShelf; 597 } 598 isInShelf()599 public boolean isInShelf() { 600 return mInShelf; 601 } 602 603 /** 604 * @param transformingInShelf whether the view is currently transforming into the shelf in an 605 * animated way 606 */ setTransformingInShelf(boolean transformingInShelf)607 public void setTransformingInShelf(boolean transformingInShelf) { 608 mTransformingInShelf = transformingInShelf; 609 } 610 isTransformingIntoShelf()611 public boolean isTransformingIntoShelf() { 612 return mTransformingInShelf; 613 } 614 isAboveShelf()615 public boolean isAboveShelf() { 616 return false; 617 } 618 hasExpandingChild()619 public boolean hasExpandingChild() { 620 return false; 621 } 622 623 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)624 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 625 } 626 627 /** 628 * A listener notifying when {@link #getActualHeight} changes. 629 */ 630 public interface OnHeightChangedListener { 631 632 /** 633 * @param view the view for which the height changed, or {@code null} if just the top 634 * padding or the padding between the elements changed 635 * @param needsAnimation whether the view height needs to be animated 636 */ onHeightChanged(ExpandableView view, boolean needsAnimation)637 void onHeightChanged(ExpandableView view, boolean needsAnimation); 638 639 /** 640 * Called when the view is reset and therefore the height will change abruptly 641 * 642 * @param view The view which was reset. 643 */ onReset(ExpandableView view)644 void onReset(ExpandableView view); 645 } 646 } 647