ExpandableView.java revision 2580a976ec93a01ed00fae51364ad872bc591d95
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 * @return The desired notification height. 180 */ 181 public int getIntrinsicHeight() { 182 return getHeight(); 183 } 184 185 /** 186 * Sets the amount this view should be clipped from the top. This is used when an expanded 187 * notification is scrolling in the top or bottom stack. 188 * 189 * @param clipTopAmount The amount of pixels this view should be clipped from top. 190 */ 191 public void setClipTopAmount(int clipTopAmount) { 192 mClipTopAmount = clipTopAmount; 193 } 194 195 public int getClipTopAmount() { 196 return mClipTopAmount; 197 } 198 199 public void setOnHeightChangedListener(OnHeightChangedListener listener) { 200 mOnHeightChangedListener = listener; 201 } 202 203 /** 204 * @return Whether we can expand this views content. 205 */ 206 public boolean isContentExpandable() { 207 return false; 208 } 209 210 public void notifyHeightChanged() { 211 if (mOnHeightChangedListener != null) { 212 mOnHeightChangedListener.onHeightChanged(this); 213 } 214 } 215 216 public boolean isTransparent() { 217 return false; 218 } 219 220 /** 221 * Perform a remove animation on this view. 222 * 223 * @param translationDirection The direction value from [-1 ... 1] indicating in which the 224 * animation should be performed. A value of -1 means that The 225 * remove animation should be performed upwards, 226 * such that the child appears to be going away to the top. 1 227 * Should mean the opposite. 228 * @param onFinishedRunnable A runnable which should be run when the animation is finished. 229 */ 230 public abstract void performRemoveAnimation(float translationDirection, 231 Runnable onFinishedRunnable); 232 233 public abstract void performAddAnimation(long delay); 234 235 public abstract void setScrimAmount(float scrimAmount); 236 237 /** 238 * A listener notifying when {@link #getActualHeight} changes. 239 */ 240 public interface OnHeightChangedListener { 241 void onHeightChanged(ExpandableView view); 242 } 243} 244