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