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