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