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