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