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 55 public ExpandableView(Context context, AttributeSet attrs) { 56 super(context, attrs); 57 } 58 59 @Override 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 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 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 */ 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 */ 141 public void setDistanceToTopRoundness(float distanceToTopRoundness) { 142 } 143 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 */ 153 public int getActualHeight() { 154 return mActualHeight; 155 } 156 157 public boolean isExpandAnimationRunning() { 158 return false; 159 } 160 161 /** 162 * @return The maximum height of this notification. 163 */ 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 */ 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 */ 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 */ 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 */ 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 */ 212 public void setDark(boolean dark, boolean fade, long delay) { 213 mDark = dark; 214 } 215 216 public boolean isDark() { 217 return mDark; 218 } 219 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 */ 231 public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) { 232 } 233 234 /** 235 * Sets whether the notification should hide its private contents if it is sensitive. 236 */ 237 public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, 238 long duration) { 239 } 240 241 /** 242 * @return The desired notification height. 243 */ 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 */ 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 */ 266 public void setClipBottomAmount(int clipBottomAmount) { 267 mClipBottomAmount = clipBottomAmount; 268 updateClipping(); 269 } 270 271 public int getClipTopAmount() { 272 return mClipTopAmount; 273 } 274 275 public int getClipBottomAmount() { 276 return mClipBottomAmount; 277 } 278 279 public void setOnHeightChangedListener(OnHeightChangedListener listener) { 280 mOnHeightChangedListener = listener; 281 } 282 283 /** 284 * @return Whether we can expand this views content. 285 */ 286 public boolean isContentExpandable() { 287 return false; 288 } 289 290 public void notifyHeightChanged(boolean needsAnimation) { 291 if (mOnHeightChangedListener != null) { 292 mOnHeightChangedListener.onHeightChanged(this, needsAnimation); 293 } 294 } 295 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 */ 314 public abstract void performRemoveAnimation(long duration, 315 long delay, float translationDirection, boolean isHeadsUpAnimation, float endLocation, 316 Runnable onFinishedRunnable, 317 AnimatorListenerAdapter animationListener); 318 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 */ 325 public void setBelowSpeedBump(boolean below) { 326 } 327 328 public int getPinnedHeadsUpHeight() { 329 return getIntrinsicHeight(); 330 } 331 332 333 /** 334 * Sets the translation of the view. 335 */ 336 public void setTranslation(float translation) { 337 setTranslationX(translation); 338 } 339 340 /** 341 * Gets the translation of the view. 342 */ 343 public float getTranslation() { 344 return getTranslationX(); 345 } 346 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 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 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 382 public boolean isSummaryWithChildren() { 383 return false; 384 } 385 386 public boolean areChildrenExpanded() { 387 return false; 388 } 389 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 401 public float getHeaderVisibleAmount() { 402 return 1.0f; 403 } 404 405 protected boolean shouldClipToActualHeight() { 406 return true; 407 } 408 409 public void setClipToActualHeight(boolean clipToActualHeight) { 410 mClipToActualHeight = clipToActualHeight; 411 updateClipping(); 412 } 413 414 public boolean willBeGone() { 415 return mWillBeGone; 416 } 417 418 public void setWillBeGone(boolean willBeGone) { 419 mWillBeGone = willBeGone; 420 } 421 422 public int getMinClipTopAmount() { 423 return mMinClipTopAmount; 424 } 425 426 public void setMinClipTopAmount(int minClipTopAmount) { 427 mMinClipTopAmount = minClipTopAmount; 428 } 429 430 @Override 431 public void setLayerType(int layerType, Paint paint) { 432 if (hasOverlappingRendering()) { 433 super.setLayerType(layerType, paint); 434 } 435 } 436 437 @Override 438 public boolean hasOverlappingRendering() { 439 // Otherwise it will be clipped 440 return super.hasOverlappingRendering() && getActualHeight() <= getHeight(); 441 } 442 443 public float getShadowAlpha() { 444 return 0.0f; 445 } 446 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 */ 455 public float getIncreasedPaddingAmount() { 456 return 0.0f; 457 } 458 459 public boolean mustStayOnScreen() { 460 return false; 461 } 462 463 public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, 464 int outlineTranslation) { 465 } 466 467 public float getOutlineAlpha() { 468 return 0.0f; 469 } 470 471 public int getOutlineTranslation() { 472 return 0; 473 } 474 475 public void setChangingPosition(boolean changingPosition) { 476 mChangingPosition = changingPosition; 477 } 478 479 public boolean isChangingPosition() { 480 return mChangingPosition; 481 } 482 483 public void setTransientContainer(ViewGroup transientContainer) { 484 mTransientContainer = transientContainer; 485 } 486 487 public ViewGroup getTransientContainer() { 488 return mTransientContainer; 489 } 490 491 /** 492 * @return padding used to alter how much of the view is clipped. 493 */ 494 public int getExtraBottomPadding() { 495 return 0; 496 } 497 498 /** 499 * @return true if the group's expansion state is changing, false otherwise. 500 */ 501 public boolean isGroupExpansionChanging() { 502 return false; 503 } 504 505 public boolean isGroupExpanded() { 506 return false; 507 } 508 509 public void setHeadsUpIsVisible() { 510 } 511 512 public boolean isChildInGroup() { 513 return false; 514 } 515 516 public void setActualHeightAnimating(boolean animating) {} 517 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 */ 527 public boolean hasNoContentHeight() { 528 return false; 529 } 530 531 /** 532 * @param inShelf whether the view is currently fully in the notification shelf. 533 */ 534 public void setInShelf(boolean inShelf) { 535 mInShelf = inShelf; 536 } 537 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 */ 546 public void setTransformingInShelf(boolean transformingInShelf) { 547 mTransformingInShelf = transformingInShelf; 548 } 549 550 public boolean isTransformingIntoShelf() { 551 return mTransformingInShelf; 552 } 553 554 public boolean isAboveShelf() { 555 return false; 556 } 557 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 */ 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 */ 579 void onReset(ExpandableView view); 580 } 581 } 582