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