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