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