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