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 mMaxNotificationHeight; 37 38 private OnHeightChangedListener mOnHeightChangedListener; 39 protected 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 int getInitialHeight() { 107 return getHeight(); 108 } 109 110 @Override 111 public boolean dispatchTouchEvent(MotionEvent ev) { 112 if (filterMotionEvent(ev)) { 113 return super.dispatchTouchEvent(ev); 114 } 115 return false; 116 } 117 118 private boolean filterMotionEvent(MotionEvent event) { 119 return event.getActionMasked() != MotionEvent.ACTION_DOWN 120 || event.getY() > mClipTopAmount && event.getY() < mActualHeight; 121 } 122 123 /** 124 * Sets the actual height of this notification. This is different than the laid out 125 * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding. 126 * 127 * @param actualHeight The height of this notification. 128 * @param notifyListeners Whether the listener should be informed about the change. 129 */ 130 public void setActualHeight(int actualHeight, boolean notifyListeners) { 131 mActualHeightInitialized = true; 132 mActualHeight = actualHeight; 133 if (notifyListeners) { 134 notifyHeightChanged(); 135 } 136 } 137 138 public void setActualHeight(int actualHeight) { 139 setActualHeight(actualHeight, true); 140 } 141 142 /** 143 * See {@link #setActualHeight}. 144 * 145 * @return The current actual height of this notification. 146 */ 147 public int getActualHeight() { 148 return mActualHeight; 149 } 150 151 /** 152 * @return The maximum height of this notification. 153 */ 154 public int getMaxHeight() { 155 return getHeight(); 156 } 157 158 /** 159 * @return The minimum height of this notification. 160 */ 161 public int getMinHeight() { 162 return getHeight(); 163 } 164 165 /** 166 * Sets the notification as dimmed. The default implementation does nothing. 167 * 168 * @param dimmed Whether the notification should be dimmed. 169 * @param fade Whether an animation should be played to change the state. 170 */ 171 public void setDimmed(boolean dimmed, boolean fade) { 172 } 173 174 /** 175 * Sets the notification as dark. The default implementation does nothing. 176 * 177 * @param dark Whether the notification should be dark. 178 * @param fade Whether an animation should be played to change the state. 179 */ 180 public void setDark(boolean dark, boolean fade) { 181 } 182 183 /** 184 * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about 185 * the upcoming state of hiding sensitive notifications. It gets called at the very beginning 186 * of a stack scroller update such that the updated intrinsic height (which is dependent on 187 * whether private or public layout is showing) gets taken into account into all layout 188 * calculations. 189 */ 190 public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) { 191 } 192 193 /** 194 * Sets whether the notification should hide its private contents if it is sensitive. 195 */ 196 public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, 197 long duration) { 198 } 199 200 /** 201 * @return The desired notification height. 202 */ 203 public int getIntrinsicHeight() { 204 return getHeight(); 205 } 206 207 /** 208 * Sets the amount this view should be clipped from the top. This is used when an expanded 209 * notification is scrolling in the top or bottom stack. 210 * 211 * @param clipTopAmount The amount of pixels this view should be clipped from top. 212 */ 213 public void setClipTopAmount(int clipTopAmount) { 214 mClipTopAmount = clipTopAmount; 215 } 216 217 public int getClipTopAmount() { 218 return mClipTopAmount; 219 } 220 221 public void setOnHeightChangedListener(OnHeightChangedListener listener) { 222 mOnHeightChangedListener = listener; 223 } 224 225 /** 226 * @return Whether we can expand this views content. 227 */ 228 public boolean isContentExpandable() { 229 return false; 230 } 231 232 public void notifyHeightChanged() { 233 if (mOnHeightChangedListener != null) { 234 mOnHeightChangedListener.onHeightChanged(this); 235 } 236 } 237 238 public boolean isTransparent() { 239 return false; 240 } 241 242 /** 243 * Perform a remove animation on this view. 244 * 245 * @param duration The duration of the remove animation. 246 * @param translationDirection The direction value from [-1 ... 1] indicating in which the 247 * animation should be performed. A value of -1 means that The 248 * remove animation should be performed upwards, 249 * such that the child appears to be going away to the top. 1 250 * Should mean the opposite. 251 * @param onFinishedRunnable A runnable which should be run when the animation is finished. 252 */ 253 public abstract void performRemoveAnimation(long duration, float translationDirection, 254 Runnable onFinishedRunnable); 255 256 public abstract void performAddAnimation(long delay, long duration); 257 258 public void setBelowSpeedBump(boolean below) { 259 } 260 261 public void onHeightReset() { 262 if (mOnHeightChangedListener != null) { 263 mOnHeightChangedListener.onReset(this); 264 } 265 } 266 267 /** 268 * This method returns the drawing rect for the view which is different from the regular 269 * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at 270 * position 0 and usually the translation is neglected. Since we are manually clipping this 271 * view,we also need to subtract the clipTopAmount from the top. This is needed in order to 272 * ensure that accessibility and focusing work correctly. 273 * 274 * @param outRect The (scrolled) drawing bounds of the view. 275 */ 276 @Override 277 public void getDrawingRect(Rect outRect) { 278 super.getDrawingRect(outRect); 279 outRect.left += getTranslationX(); 280 outRect.right += getTranslationX(); 281 outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight()); 282 outRect.top += getTranslationY() + getClipTopAmount(); 283 } 284 285 /** 286 * A listener notifying when {@link #getActualHeight} changes. 287 */ 288 public interface OnHeightChangedListener { 289 290 /** 291 * @param view the view for which the height changed, or {@code null} if just the top 292 * padding or the padding between the elements changed 293 */ 294 void onHeightChanged(ExpandableView view); 295 296 /** 297 * Called when the view is reset and therefore the height will change abruptly 298 * 299 * @param view The view which was reset. 300 */ 301 void onReset(ExpandableView view); 302 } 303 } 304