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