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