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