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