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