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