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