ExpandableView.java revision 310df3127aace5a82cdc107fdb1e2d6957f38bcc
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 mMaxNotificationHeight; 37 38 private OnHeightChangedListener mOnHeightChangedListener; 39 private int mActualHeight; 40 protected int mClipTopAmount; 41 private boolean mActualHeightInitialized; 42 private ArrayList<View> mMatchParentViews = new ArrayList<View>(); 43 44 public ExpandableView(Context context, AttributeSet attrs) { 45 super(context, attrs); 46 mMaxNotificationHeight = getResources().getDimensionPixelSize( 47 R.dimen.notification_max_height); 48 } 49 50 @Override 51 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 52 int ownMaxHeight = mMaxNotificationHeight; 53 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 54 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; 55 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; 56 if (hasFixedHeight || isHeightLimited) { 57 int size = MeasureSpec.getSize(heightMeasureSpec); 58 ownMaxHeight = Math.min(ownMaxHeight, size); 59 } 60 int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST); 61 int maxChildHeight = 0; 62 int childCount = getChildCount(); 63 for (int i = 0; i < childCount; i++) { 64 View child = getChildAt(i); 65 int childHeightSpec = newHeightSpec; 66 ViewGroup.LayoutParams layoutParams = child.getLayoutParams(); 67 if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) { 68 if (layoutParams.height >= 0) { 69 // An actual height is set 70 childHeightSpec = layoutParams.height > ownMaxHeight 71 ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY) 72 : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY); 73 } 74 child.measure( 75 getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width), 76 childHeightSpec); 77 int childHeight = child.getMeasuredHeight(); 78 maxChildHeight = Math.max(maxChildHeight, childHeight); 79 } else { 80 mMatchParentViews.add(child); 81 } 82 } 83 int ownHeight = hasFixedHeight ? ownMaxHeight : maxChildHeight; 84 newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY); 85 for (View child : mMatchParentViews) { 86 child.measure(getChildMeasureSpec( 87 widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width), 88 newHeightSpec); 89 } 90 mMatchParentViews.clear(); 91 int width = MeasureSpec.getSize(widthMeasureSpec); 92 setMeasuredDimension(width, ownHeight); 93 } 94 95 @Override 96 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 97 super.onLayout(changed, left, top, right, bottom); 98 if (!mActualHeightInitialized && mActualHeight == 0) { 99 int initialHeight = getInitialHeight(); 100 if (initialHeight != 0) { 101 setActualHeight(initialHeight); 102 } 103 } 104 } 105 106 protected void resetHeight() { 107 mActualHeight = 0; 108 mActualHeightInitialized = false; 109 } 110 111 protected int getInitialHeight() { 112 return getHeight(); 113 } 114 115 @Override 116 public boolean dispatchTouchEvent(MotionEvent ev) { 117 if (filterMotionEvent(ev)) { 118 return super.dispatchTouchEvent(ev); 119 } 120 return false; 121 } 122 123 private boolean filterMotionEvent(MotionEvent event) { 124 return event.getActionMasked() != MotionEvent.ACTION_DOWN 125 || event.getY() > mClipTopAmount && event.getY() < mActualHeight; 126 } 127 128 /** 129 * Sets the actual height of this notification. This is different than the laid out 130 * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding. 131 * 132 * @param actualHeight The height of this notification. 133 * @param notifyListeners Whether the listener should be informed about the change. 134 */ 135 public void setActualHeight(int actualHeight, boolean notifyListeners) { 136 mActualHeightInitialized = true; 137 mActualHeight = actualHeight; 138 if (notifyListeners) { 139 notifyHeightChanged(); 140 } 141 } 142 143 public void setActualHeight(int actualHeight) { 144 setActualHeight(actualHeight, true); 145 } 146 147 /** 148 * See {@link #setActualHeight}. 149 * 150 * @return The current actual height of this notification. 151 */ 152 public int getActualHeight() { 153 return mActualHeight; 154 } 155 156 /** 157 * @return The maximum height of this notification. 158 */ 159 public int getMaxHeight() { 160 return getHeight(); 161 } 162 163 /** 164 * @return The minimum height of this notification. 165 */ 166 public int getMinHeight() { 167 return getHeight(); 168 } 169 170 /** 171 * Sets the notification as dimmed. The default implementation does nothing. 172 * 173 * @param dimmed Whether the notification should be dimmed. 174 * @param fade Whether an animation should be played to change the state. 175 */ 176 public void setDimmed(boolean dimmed, boolean fade) { 177 } 178 179 /** 180 * Sets the notification as dark. The default implementation does nothing. 181 * 182 * @param dark Whether the notification should be dark. 183 * @param fade Whether an animation should be played to change the state. 184 */ 185 public void setDark(boolean dark, boolean fade) { 186 } 187 188 /** 189 * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about 190 * the upcoming state of hiding sensitive notifications. It gets called at the very beginning 191 * of a stack scroller update such that the updated intrinsic height (which is dependent on 192 * whether private or public layout is showing) gets taken into account into all layout 193 * calculations. 194 */ 195 public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) { 196 } 197 198 /** 199 * Sets whether the notification should hide its private contents if it is sensitive. 200 */ 201 public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, 202 long duration) { 203 } 204 205 /** 206 * @return The desired notification height. 207 */ 208 public int getIntrinsicHeight() { 209 return getHeight(); 210 } 211 212 /** 213 * Sets the amount this view should be clipped from the top. This is used when an expanded 214 * notification is scrolling in the top or bottom stack. 215 * 216 * @param clipTopAmount The amount of pixels this view should be clipped from top. 217 */ 218 public void setClipTopAmount(int clipTopAmount) { 219 mClipTopAmount = clipTopAmount; 220 } 221 222 public int getClipTopAmount() { 223 return mClipTopAmount; 224 } 225 226 public void setOnHeightChangedListener(OnHeightChangedListener listener) { 227 mOnHeightChangedListener = listener; 228 } 229 230 /** 231 * @return Whether we can expand this views content. 232 */ 233 public boolean isContentExpandable() { 234 return false; 235 } 236 237 public void notifyHeightChanged() { 238 if (mOnHeightChangedListener != null) { 239 mOnHeightChangedListener.onHeightChanged(this); 240 } 241 } 242 243 public boolean isTransparent() { 244 return false; 245 } 246 247 /** 248 * Perform a remove animation on this view. 249 * 250 * @param duration The duration of the remove animation. 251 * @param translationDirection The direction value from [-1 ... 1] indicating in which the 252 * animation should be performed. A value of -1 means that The 253 * remove animation should be performed upwards, 254 * such that the child appears to be going away to the top. 1 255 * Should mean the opposite. 256 * @param onFinishedRunnable A runnable which should be run when the animation is finished. 257 */ 258 public abstract void performRemoveAnimation(long duration, float translationDirection, 259 Runnable onFinishedRunnable); 260 261 public abstract void performAddAnimation(long delay, long duration); 262 263 public void setBelowSpeedBump(boolean below) { 264 } 265 266 public void onHeightReset() { 267 if (mOnHeightChangedListener != null) { 268 mOnHeightChangedListener.onReset(this); 269 } 270 } 271 272 /** 273 * This method returns the drawing rect for the view which is different from the regular 274 * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at 275 * position 0 and usually the translation is neglected. Since we are manually clipping this 276 * view,we also need to subtract the clipTopAmount from the top. This is needed in order to 277 * ensure that accessibility and focusing work correctly. 278 * 279 * @param outRect The (scrolled) drawing bounds of the view. 280 */ 281 @Override 282 public void getDrawingRect(Rect outRect) { 283 super.getDrawingRect(outRect); 284 outRect.left += getTranslationX(); 285 outRect.right += getTranslationX(); 286 outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight()); 287 outRect.top += getTranslationY() + getClipTopAmount(); 288 } 289 290 /** 291 * A listener notifying when {@link #getActualHeight} changes. 292 */ 293 public interface OnHeightChangedListener { 294 295 /** 296 * @param view the view for which the height changed, or {@code null} if just the top 297 * padding or the padding between the elements changed 298 */ 299 void onHeightChanged(ExpandableView view); 300 301 /** 302 * Called when the view is reset and therefore the height will change abruptly 303 * 304 * @param view The view which was reset. 305 */ 306 void onReset(ExpandableView view); 307 } 308} 309