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