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