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