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