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