ExpandableView.java revision 4ffd63611a0d516c9988b37e9c06e6f8390c2a2f
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.View;
24import android.view.ViewGroup;
25import android.widget.FrameLayout;
26
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    private boolean mClipToActualHeight = true;
49
50    public ExpandableView(Context context, AttributeSet attrs) {
51        super(context, attrs);
52        mMaxViewHeight = getResources().getDimensionPixelSize(
53                R.dimen.notification_max_height);
54    }
55
56    @Override
57    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
58        final int givenSize = MeasureSpec.getSize(heightMeasureSpec);
59        int ownMaxHeight = 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    @Override
104    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
105        super.onLayout(changed, left, top, right, bottom);
106        if (!mActualHeightInitialized && mActualHeight == 0) {
107            int initialHeight = getInitialHeight();
108            if (initialHeight != 0) {
109                setActualHeight(initialHeight);
110            }
111        }
112        updateClipping();
113    }
114
115    /**
116     * Resets the height of the view on the next layout pass
117     */
118    protected void resetActualHeight() {
119        mActualHeight = 0;
120        mActualHeightInitialized = false;
121        requestLayout();
122    }
123
124    protected int getInitialHeight() {
125        return getHeight();
126    }
127
128    @Override
129    public boolean pointInView(float localX, float localY, float slop) {
130        float top = mClipTopAmount;
131        float bottom = mActualHeight;
132        return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) &&
133                localY < (bottom + slop);
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(false  /* needsAnimation */);
149        }
150    }
151
152    public void setActualHeight(int actualHeight) {
153        setActualHeight(actualHeight, true /* notifyListeners */);
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 getMaxContentHeight() {
169        return getHeight();
170    }
171
172    /**
173     * @return The minimum content height of this notification.
174     */
175    public int getMinHeight() {
176        return getHeight();
177    }
178
179    /**
180     * @return The minimum height this child chan be expanded to. Note that this might be different
181     * than {@link #getMinHeight()} because some elements can't be collapsed by an expand gesture
182     * to it's absolute minimal height
183     */
184    public int getMinExpandHeight() {
185        return getHeight();
186    }
187
188    /**
189     * Sets the notification as dimmed. The default implementation does nothing.
190     *
191     * @param dimmed Whether the notification should be dimmed.
192     * @param fade Whether an animation should be played to change the state.
193     */
194    public void setDimmed(boolean dimmed, boolean fade) {
195    }
196
197    /**
198     * Sets the notification as dark. The default implementation does nothing.
199     *
200     * @param dark Whether the notification should be dark.
201     * @param fade Whether an animation should be played to change the state.
202     * @param delay If fading, the delay of the animation.
203     */
204    public void setDark(boolean dark, boolean fade, long delay) {
205        mDark = dark;
206    }
207
208    public boolean isDark() {
209        return mDark;
210    }
211
212    /**
213     * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
214     * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
215     * of a stack scroller update such that the updated intrinsic height (which is dependent on
216     * whether private or public layout is showing) gets taken into account into all layout
217     * calculations.
218     */
219    public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
220    }
221
222    /**
223     * Sets whether the notification should hide its private contents if it is sensitive.
224     */
225    public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
226            long duration) {
227    }
228
229    /**
230     * @return The desired notification height.
231     */
232    public int getIntrinsicHeight() {
233        return getHeight();
234    }
235
236    /**
237     * Sets the amount this view should be clipped from the top. This is used when an expanded
238     * notification is scrolling in the top or bottom stack.
239     *
240     * @param clipTopAmount The amount of pixels this view should be clipped from top.
241     */
242    public void setClipTopAmount(int clipTopAmount) {
243        mClipTopAmount = clipTopAmount;
244    }
245
246    public int getClipTopAmount() {
247        return mClipTopAmount;
248    }
249
250    public void setOnHeightChangedListener(OnHeightChangedListener listener) {
251        mOnHeightChangedListener = listener;
252    }
253
254    /**
255     * @return Whether we can expand this views content.
256     */
257    public boolean isContentExpandable() {
258        return false;
259    }
260
261    public void notifyHeightChanged(boolean needsAnimation) {
262        if (mOnHeightChangedListener != null) {
263            mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
264        }
265    }
266
267    public boolean isTransparent() {
268        return false;
269    }
270
271    /**
272     * Perform a remove animation on this view.
273     *
274     * @param duration The duration of the remove animation.
275     * @param translationDirection The direction value from [-1 ... 1] indicating in which the
276     *                             animation should be performed. A value of -1 means that The
277     *                             remove animation should be performed upwards,
278     *                             such that the  child appears to be going away to the top. 1
279     *                             Should mean the opposite.
280     * @param onFinishedRunnable A runnable which should be run when the animation is finished.
281     */
282    public abstract void performRemoveAnimation(long duration, float translationDirection,
283            Runnable onFinishedRunnable);
284
285    public abstract void performAddAnimation(long delay, long duration);
286
287    public void setBelowSpeedBump(boolean below) {
288    }
289
290    public void onHeightReset() {
291        if (mOnHeightChangedListener != null) {
292            mOnHeightChangedListener.onReset(this);
293        }
294    }
295
296    /**
297     * This method returns the drawing rect for the view which is different from the regular
298     * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
299     * position 0 and usually the translation is neglected. Since we are manually clipping this
300     * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
301     * ensure that accessibility and focusing work correctly.
302     *
303     * @param outRect The (scrolled) drawing bounds of the view.
304     */
305    @Override
306    public void getDrawingRect(Rect outRect) {
307        super.getDrawingRect(outRect);
308        outRect.left += getTranslationX();
309        outRect.right += getTranslationX();
310        outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
311        outRect.top += getTranslationY() + getClipTopAmount();
312    }
313
314    @Override
315    public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
316        super.getBoundsOnScreen(outRect, clipToParent);
317        outRect.bottom = outRect.top + getActualHeight();
318        outRect.top += getClipTopOptimization();
319    }
320
321    public boolean isSummaryWithChildren() {
322        return false;
323    }
324
325    public boolean areChildrenExpanded() {
326        return false;
327    }
328
329    private void updateClipping() {
330        if (mClipToActualHeight) {
331            int top = mClipTopOptimization;
332            if (top >= getActualHeight()) {
333                top = getActualHeight() - 1;
334            }
335            mClipRect.set(0, top, getWidth(), getActualHeight());
336            setClipBounds(mClipRect);
337        } else {
338            setClipBounds(null);
339        }
340    }
341
342    public void setClipToActualHeight(boolean clipToActualHeight) {
343        mClipToActualHeight = clipToActualHeight;
344        updateClipping();
345    }
346
347    public int getClipTopOptimization() {
348        return mClipTopOptimization;
349    }
350
351    /**
352     * Set that the view will be clipped by a given amount from the top. Contrary to
353     * {@link #setClipTopAmount} this amount doesn't effect shadows and the background.
354     *
355     * @param clipTopOptimization the amount to clip from the top
356     */
357    public void setClipTopOptimization(int clipTopOptimization) {
358        mClipTopOptimization = clipTopOptimization;
359        updateClipping();
360    }
361
362    public boolean willBeGone() {
363        return mWillBeGone;
364    }
365
366    public void setWillBeGone(boolean willBeGone) {
367        mWillBeGone = willBeGone;
368    }
369
370    public int getMinClipTopAmount() {
371        return mMinClipTopAmount;
372    }
373
374    public void setMinClipTopAmount(int minClipTopAmount) {
375        mMinClipTopAmount = minClipTopAmount;
376    }
377
378    @Override
379    public void setLayerType(int layerType, Paint paint) {
380        if (hasOverlappingRendering()) {
381            super.setLayerType(layerType, paint);
382        }
383    }
384
385    @Override
386    public boolean hasOverlappingRendering() {
387        // Otherwise it will be clipped
388        return super.hasOverlappingRendering() && getActualHeight() <= getHeight();
389    }
390
391    /**
392     * A listener notifying when {@link #getActualHeight} changes.
393     */
394    public interface OnHeightChangedListener {
395
396        /**
397         * @param view the view for which the height changed, or {@code null} if just the top
398         *             padding or the padding between the elements changed
399         * @param needsAnimation whether the view height needs to be animated
400         */
401        void onHeightChanged(ExpandableView view, boolean needsAnimation);
402
403        /**
404         * Called when the view is reset and therefore the height will change abruptly
405         *
406         * @param view The view which was reset.
407         */
408        void onReset(ExpandableView view);
409    }
410}
411