ExpandableView.java revision a272dfed9a4f31d8245099c0d99a73e79b90670c
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 37 private 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 } 52 53 @Override 54 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 55 int ownMaxHeight = mMaxViewHeight; 56 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 57 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; 58 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; 59 if (hasFixedHeight || isHeightLimited) { 60 int size = MeasureSpec.getSize(heightMeasureSpec); 61 ownMaxHeight = Math.min(ownMaxHeight, size); 62 } 63 int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST); 64 int maxChildHeight = 0; 65 int childCount = getChildCount(); 66 for (int i = 0; i < childCount; i++) { 67 View child = getChildAt(i); 68 int childHeightSpec = newHeightSpec; 69 ViewGroup.LayoutParams layoutParams = child.getLayoutParams(); 70 if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) { 71 if (layoutParams.height >= 0) { 72 // An actual height is set 73 childHeightSpec = layoutParams.height > ownMaxHeight 74 ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY) 75 : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY); 76 } 77 child.measure( 78 getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width), 79 childHeightSpec); 80 int childHeight = child.getMeasuredHeight(); 81 maxChildHeight = Math.max(maxChildHeight, childHeight); 82 } else { 83 mMatchParentViews.add(child); 84 } 85 } 86 int ownHeight = hasFixedHeight ? ownMaxHeight : 87 isHeightLimited ? Math.min(ownMaxHeight, maxChildHeight) : maxChildHeight; 88 newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY); 89 for (View child : mMatchParentViews) { 90 child.measure(getChildMeasureSpec( 91 widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width), 92 newHeightSpec); 93 } 94 mMatchParentViews.clear(); 95 int width = MeasureSpec.getSize(widthMeasureSpec); 96 setMeasuredDimension(width, ownHeight); 97 } 98 99 @Override 100 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 101 super.onLayout(changed, left, top, right, bottom); 102 if (!mActualHeightInitialized && mActualHeight == 0) { 103 int initialHeight = getInitialHeight(); 104 if (initialHeight != 0) { 105 setActualHeight(initialHeight); 106 } 107 } 108 } 109 110 /** 111 * Resets the height of the view on the next layout pass 112 */ 113 protected void resetActualHeight() { 114 mActualHeight = 0; 115 mActualHeightInitialized = false; 116 requestLayout(); 117 } 118 119 protected int getInitialHeight() { 120 return getHeight(); 121 } 122 123 @Override 124 public boolean dispatchTouchEvent(MotionEvent ev) { 125 if (filterMotionEvent(ev)) { 126 return super.dispatchTouchEvent(ev); 127 } 128 return false; 129 } 130 131 protected boolean filterMotionEvent(MotionEvent event) { 132 return event.getActionMasked() != MotionEvent.ACTION_DOWN 133 || event.getY() > mClipTopAmount && event.getY() < mActualHeight; 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(); 149 } 150 } 151 152 public void setActualHeight(int actualHeight) { 153 setActualHeight(actualHeight, true); 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 getMaxHeight() { 169 return getHeight(); 170 } 171 172 /** 173 * @return The minimum height of this notification. 174 */ 175 public int getMinHeight() { 176 return getHeight(); 177 } 178 179 /** 180 * Sets the notification as dimmed. The default implementation does nothing. 181 * 182 * @param dimmed Whether the notification should be dimmed. 183 * @param fade Whether an animation should be played to change the state. 184 */ 185 public void setDimmed(boolean dimmed, boolean fade) { 186 } 187 188 /** 189 * Sets the notification as dark. The default implementation does nothing. 190 * 191 * @param dark Whether the notification should be dark. 192 * @param fade Whether an animation should be played to change the state. 193 * @param delay If fading, the delay of the animation. 194 */ 195 public void setDark(boolean dark, boolean fade, long delay) { 196 mDark = dark; 197 } 198 199 public boolean isDark() { 200 return mDark; 201 } 202 203 /** 204 * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about 205 * the upcoming state of hiding sensitive notifications. It gets called at the very beginning 206 * of a stack scroller update such that the updated intrinsic height (which is dependent on 207 * whether private or public layout is showing) gets taken into account into all layout 208 * calculations. 209 */ 210 public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) { 211 } 212 213 /** 214 * Sets whether the notification should hide its private contents if it is sensitive. 215 */ 216 public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, 217 long duration) { 218 } 219 220 /** 221 * @return The desired notification height. 222 */ 223 public int getIntrinsicHeight() { 224 return getHeight(); 225 } 226 227 /** 228 * Sets the amount this view should be clipped from the top. This is used when an expanded 229 * notification is scrolling in the top or bottom stack. 230 * 231 * @param clipTopAmount The amount of pixels this view should be clipped from top. 232 */ 233 public void setClipTopAmount(int clipTopAmount) { 234 mClipTopAmount = clipTopAmount; 235 } 236 237 public int getClipTopAmount() { 238 return mClipTopAmount; 239 } 240 241 public void setOnHeightChangedListener(OnHeightChangedListener listener) { 242 mOnHeightChangedListener = listener; 243 } 244 245 /** 246 * @return Whether we can expand this views content. 247 */ 248 public boolean isContentExpandable() { 249 return false; 250 } 251 252 public void notifyHeightChanged() { 253 if (mOnHeightChangedListener != null) { 254 mOnHeightChangedListener.onHeightChanged(this); 255 } 256 } 257 258 public boolean isTransparent() { 259 return false; 260 } 261 262 /** 263 * Perform a remove animation on this view. 264 * 265 * @param duration The duration of the remove animation. 266 * @param translationDirection The direction value from [-1 ... 1] indicating in which the 267 * animation should be performed. A value of -1 means that The 268 * remove animation should be performed upwards, 269 * such that the child appears to be going away to the top. 1 270 * Should mean the opposite. 271 * @param onFinishedRunnable A runnable which should be run when the animation is finished. 272 */ 273 public abstract void performRemoveAnimation(long duration, float translationDirection, 274 Runnable onFinishedRunnable); 275 276 public abstract void performAddAnimation(long delay, long duration); 277 278 public void setBelowSpeedBump(boolean below) { 279 } 280 281 public void onHeightReset() { 282 if (mOnHeightChangedListener != null) { 283 mOnHeightChangedListener.onReset(this); 284 } 285 } 286 287 /** 288 * This method returns the drawing rect for the view which is different from the regular 289 * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at 290 * position 0 and usually the translation is neglected. Since we are manually clipping this 291 * view,we also need to subtract the clipTopAmount from the top. This is needed in order to 292 * ensure that accessibility and focusing work correctly. 293 * 294 * @param outRect The (scrolled) drawing bounds of the view. 295 */ 296 @Override 297 public void getDrawingRect(Rect outRect) { 298 super.getDrawingRect(outRect); 299 outRect.left += getTranslationX(); 300 outRect.right += getTranslationX(); 301 outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight()); 302 outRect.top += getTranslationY() + getClipTopAmount(); 303 } 304 305 private void updateClipping() { 306 mClipRect.set(0, mClipTopOptimization, getWidth(), getActualHeight()); 307 setClipBounds(mClipRect); 308 } 309 310 public int getClipTopOptimization() { 311 return mClipTopOptimization; 312 } 313 314 /** 315 * Set that the view will be clipped by a given amount from the top. Contrary to 316 * {@link #setClipTopAmount} this amount doesn't effect shadows and the background. 317 * 318 * @param clipTopOptimization the amount to clip from the top 319 */ 320 public void setClipTopOptimization(int clipTopOptimization) { 321 mClipTopOptimization = clipTopOptimization; 322 updateClipping(); 323 } 324 325 /** 326 * A listener notifying when {@link #getActualHeight} changes. 327 */ 328 public interface OnHeightChangedListener { 329 330 /** 331 * @param view the view for which the height changed, or {@code null} if just the top 332 * padding or the padding between the elements changed 333 */ 334 void onHeightChanged(ExpandableView view); 335 336 /** 337 * Called when the view is reset and therefore the height will change abruptly 338 * 339 * @param view The view which was reset. 340 */ 341 void onReset(ExpandableView view); 342 } 343} 344