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