ExpandableView.java revision 3322357b5045fdcc3e3b04c65219d39ce1ae441f
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; 28 29import java.util.ArrayList; 30 31/** 32 * An abstract view for expandable views. 33 */ 34public abstract class ExpandableView extends FrameLayout { 35 36 protected OnHeightChangedListener mOnHeightChangedListener; 37 private int mActualHeight; 38 protected int mClipTopAmount; 39 private boolean mActualHeightInitialized; 40 private boolean mDark; 41 private ArrayList<View> mMatchParentViews = new ArrayList<View>(); 42 private int mClipTopOptimization; 43 private static Rect mClipRect = new Rect(); 44 private boolean mWillBeGone; 45 private int mMinClipTopAmount = 0; 46 private boolean mClipToActualHeight = true; 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 if (!mActualHeightInitialized && mActualHeight == 0) { 103 int initialHeight = getInitialHeight(); 104 if (initialHeight != 0) { 105 setActualHeight(initialHeight); 106 } 107 } 108 updateClipping(); 109 } 110 111 /** 112 * Resets the height of the view on the next layout pass 113 */ 114 protected void resetActualHeight() { 115 mActualHeight = 0; 116 mActualHeightInitialized = false; 117 requestLayout(); 118 } 119 120 protected int getInitialHeight() { 121 return getHeight(); 122 } 123 124 @Override 125 public boolean pointInView(float localX, float localY, float slop) { 126 float top = mClipTopAmount; 127 float bottom = mActualHeight; 128 return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) && 129 localY < (bottom + slop); 130 } 131 132 /** 133 * Sets the actual height of this notification. This is different than the laid out 134 * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding. 135 * 136 * @param actualHeight The height of this notification. 137 * @param notifyListeners Whether the listener should be informed about the change. 138 */ 139 public void setActualHeight(int actualHeight, boolean notifyListeners) { 140 mActualHeightInitialized = true; 141 mActualHeight = actualHeight; 142 updateClipping(); 143 if (notifyListeners) { 144 notifyHeightChanged(false /* needsAnimation */); 145 } 146 } 147 148 public void setActualHeight(int actualHeight) { 149 setActualHeight(actualHeight, true /* notifyListeners */); 150 } 151 152 /** 153 * See {@link #setActualHeight}. 154 * 155 * @return The current actual height of this notification. 156 */ 157 public int getActualHeight() { 158 return mActualHeight; 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. 170 */ 171 public int getMinHeight() { 172 return getHeight(); 173 } 174 175 /** 176 * @return The minimum height this child chan be expanded to. Note that this might be different 177 * than {@link #getMinHeight()} because some elements can't be collapsed by an expand gesture 178 * to it's absolute minimal height 179 */ 180 public int getMinExpandHeight() { 181 return getHeight(); 182 } 183 184 /** 185 * Sets the notification as dimmed. The default implementation does nothing. 186 * 187 * @param dimmed Whether the notification should be dimmed. 188 * @param fade Whether an animation should be played to change the state. 189 */ 190 public void setDimmed(boolean dimmed, boolean fade) { 191 } 192 193 /** 194 * Sets the notification as dark. The default implementation does nothing. 195 * 196 * @param dark Whether the notification should be dark. 197 * @param fade Whether an animation should be played to change the state. 198 * @param delay If fading, the delay of the animation. 199 */ 200 public void setDark(boolean dark, boolean fade, long delay) { 201 mDark = dark; 202 } 203 204 public boolean isDark() { 205 return mDark; 206 } 207 208 /** 209 * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about 210 * the upcoming state of hiding sensitive notifications. It gets called at the very beginning 211 * of a stack scroller update such that the updated intrinsic height (which is dependent on 212 * whether private or public layout is showing) gets taken into account into all layout 213 * calculations. 214 */ 215 public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) { 216 } 217 218 /** 219 * Sets whether the notification should hide its private contents if it is sensitive. 220 */ 221 public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, 222 long duration) { 223 } 224 225 /** 226 * @return The desired notification height. 227 */ 228 public int getIntrinsicHeight() { 229 return getHeight(); 230 } 231 232 /** 233 * Sets the amount this view should be clipped from the top. This is used when an expanded 234 * notification is scrolling in the top or bottom stack. 235 * 236 * @param clipTopAmount The amount of pixels this view should be clipped from top. 237 */ 238 public void setClipTopAmount(int clipTopAmount) { 239 mClipTopAmount = clipTopAmount; 240 } 241 242 public int getClipTopAmount() { 243 return mClipTopAmount; 244 } 245 246 public void setOnHeightChangedListener(OnHeightChangedListener listener) { 247 mOnHeightChangedListener = listener; 248 } 249 250 /** 251 * @return Whether we can expand this views content. 252 */ 253 public boolean isContentExpandable() { 254 return false; 255 } 256 257 public void notifyHeightChanged(boolean needsAnimation) { 258 if (mOnHeightChangedListener != null) { 259 mOnHeightChangedListener.onHeightChanged(this, needsAnimation); 260 } 261 } 262 263 public boolean isTransparent() { 264 return false; 265 } 266 267 /** 268 * Perform a remove animation on this view. 269 * 270 * @param duration The duration of the remove animation. 271 * @param translationDirection The direction value from [-1 ... 1] indicating in which the 272 * animation should be performed. A value of -1 means that The 273 * remove animation should be performed upwards, 274 * such that the child appears to be going away to the top. 1 275 * Should mean the opposite. 276 * @param onFinishedRunnable A runnable which should be run when the animation is finished. 277 */ 278 public abstract void performRemoveAnimation(long duration, float translationDirection, 279 Runnable onFinishedRunnable); 280 281 public abstract void performAddAnimation(long delay, long duration); 282 283 public void setBelowSpeedBump(boolean below) { 284 } 285 286 public void onHeightReset() { 287 if (mOnHeightChangedListener != null) { 288 mOnHeightChangedListener.onReset(this); 289 } 290 } 291 292 /** 293 * This method returns the drawing rect for the view which is different from the regular 294 * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at 295 * position 0 and usually the translation is neglected. Since we are manually clipping this 296 * view,we also need to subtract the clipTopAmount from the top. This is needed in order to 297 * ensure that accessibility and focusing work correctly. 298 * 299 * @param outRect The (scrolled) drawing bounds of the view. 300 */ 301 @Override 302 public void getDrawingRect(Rect outRect) { 303 super.getDrawingRect(outRect); 304 outRect.left += getTranslationX(); 305 outRect.right += getTranslationX(); 306 outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight()); 307 outRect.top += getTranslationY() + getClipTopAmount(); 308 } 309 310 @Override 311 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) { 312 super.getBoundsOnScreen(outRect, clipToParent); 313 outRect.bottom = outRect.top + getActualHeight(); 314 outRect.top += getClipTopOptimization(); 315 } 316 317 public boolean isSummaryWithChildren() { 318 return false; 319 } 320 321 public boolean areChildrenExpanded() { 322 return false; 323 } 324 325 private void updateClipping() { 326 if (mClipToActualHeight) { 327 int top = mClipTopOptimization; 328 if (top >= getActualHeight()) { 329 top = getActualHeight() - 1; 330 } 331 mClipRect.set(0, top, getWidth(), getActualHeight()); 332 setClipBounds(mClipRect); 333 } else { 334 setClipBounds(null); 335 } 336 } 337 338 public void setClipToActualHeight(boolean clipToActualHeight) { 339 mClipToActualHeight = clipToActualHeight; 340 updateClipping(); 341 } 342 343 public int getClipTopOptimization() { 344 return mClipTopOptimization; 345 } 346 347 /** 348 * Set that the view will be clipped by a given amount from the top. Contrary to 349 * {@link #setClipTopAmount} this amount doesn't effect shadows and the background. 350 * 351 * @param clipTopOptimization the amount to clip from the top 352 */ 353 public void setClipTopOptimization(int clipTopOptimization) { 354 mClipTopOptimization = clipTopOptimization; 355 updateClipping(); 356 } 357 358 public boolean willBeGone() { 359 return mWillBeGone; 360 } 361 362 public void setWillBeGone(boolean willBeGone) { 363 mWillBeGone = willBeGone; 364 } 365 366 public int getMinClipTopAmount() { 367 return mMinClipTopAmount; 368 } 369 370 public void setMinClipTopAmount(int minClipTopAmount) { 371 mMinClipTopAmount = minClipTopAmount; 372 } 373 374 @Override 375 public void setLayerType(int layerType, Paint paint) { 376 if (hasOverlappingRendering()) { 377 super.setLayerType(layerType, paint); 378 } 379 } 380 381 @Override 382 public boolean hasOverlappingRendering() { 383 // Otherwise it will be clipped 384 return super.hasOverlappingRendering() && getActualHeight() <= getHeight(); 385 } 386 387 public float getShadowAlpha() { 388 return 0.0f; 389 } 390 391 public void setShadowAlpha(float shadowAlpha) { 392 } 393 394 public boolean needsIncreasedPadding() { 395 return false; 396 } 397 398 public boolean mustStayOnScreen() { 399 return false; 400 } 401 402 public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, 403 int outlineTranslation) { 404 } 405 406 public float getOutlineAlpha() { 407 return 0.0f; 408 } 409 410 public int getOutlineTranslation() { 411 return 0; 412 } 413 414 /** 415 * A listener notifying when {@link #getActualHeight} changes. 416 */ 417 public interface OnHeightChangedListener { 418 419 /** 420 * @param view the view for which the height changed, or {@code null} if just the top 421 * padding or the padding between the elements changed 422 * @param needsAnimation whether the view height needs to be animated 423 */ 424 void onHeightChanged(ExpandableView view, boolean needsAnimation); 425 426 /** 427 * Called when the view is reset and therefore the height will change abruptly 428 * 429 * @param view The view which was reset. 430 */ 431 void onReset(ExpandableView view); 432 } 433} 434