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