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.Rect;
21import android.util.AttributeSet;
22import android.view.MotionEvent;
23import android.view.View;
24import android.view.ViewGroup;
25import android.widget.FrameLayout;
26import com.android.systemui.R;
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    private final int mMaxNotificationHeight;
37
38    private OnHeightChangedListener mOnHeightChangedListener;
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
45    public ExpandableView(Context context, AttributeSet attrs) {
46        super(context, attrs);
47        mMaxNotificationHeight = getResources().getDimensionPixelSize(
48                R.dimen.notification_max_height);
49    }
50
51    @Override
52    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
53        int ownMaxHeight = mMaxNotificationHeight;
54        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
55        boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
56        boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
57        if (hasFixedHeight || isHeightLimited) {
58            int size = MeasureSpec.getSize(heightMeasureSpec);
59            ownMaxHeight = Math.min(ownMaxHeight, size);
60        }
61        int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
62        int maxChildHeight = 0;
63        int childCount = getChildCount();
64        for (int i = 0; i < childCount; i++) {
65            View child = getChildAt(i);
66            int childHeightSpec = newHeightSpec;
67            ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
68            if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
69                if (layoutParams.height >= 0) {
70                    // An actual height is set
71                    childHeightSpec = layoutParams.height > ownMaxHeight
72                        ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
73                        : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
74                }
75                child.measure(
76                        getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
77                        childHeightSpec);
78                int childHeight = child.getMeasuredHeight();
79                maxChildHeight = Math.max(maxChildHeight, childHeight);
80            } else {
81                mMatchParentViews.add(child);
82            }
83        }
84        int ownHeight = hasFixedHeight ? ownMaxHeight : maxChildHeight;
85        newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
86        for (View child : mMatchParentViews) {
87            child.measure(getChildMeasureSpec(
88                    widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
89                    newHeightSpec);
90        }
91        mMatchParentViews.clear();
92        int width = MeasureSpec.getSize(widthMeasureSpec);
93        setMeasuredDimension(width, ownHeight);
94    }
95
96    @Override
97    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
98        super.onLayout(changed, left, top, right, bottom);
99        if (!mActualHeightInitialized && mActualHeight == 0) {
100            int initialHeight = getInitialHeight();
101            if (initialHeight != 0) {
102                setActualHeight(initialHeight);
103            }
104        }
105    }
106
107    /**
108     * Resets the height of the view on the next layout pass
109     */
110    protected void resetActualHeight() {
111        mActualHeight = 0;
112        mActualHeightInitialized = false;
113        requestLayout();
114    }
115
116    protected int getInitialHeight() {
117        return getHeight();
118    }
119
120    @Override
121    public boolean dispatchTouchEvent(MotionEvent ev) {
122        if (filterMotionEvent(ev)) {
123            return super.dispatchTouchEvent(ev);
124        }
125        return false;
126    }
127
128    protected boolean filterMotionEvent(MotionEvent event) {
129        return event.getActionMasked() != MotionEvent.ACTION_DOWN
130                || event.getY() > mClipTopAmount && event.getY() < mActualHeight;
131    }
132
133    /**
134     * Sets the actual height of this notification. This is different than the laid out
135     * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
136     *
137     * @param actualHeight The height of this notification.
138     * @param notifyListeners Whether the listener should be informed about the change.
139     */
140    public void setActualHeight(int actualHeight, boolean notifyListeners) {
141        mActualHeightInitialized = true;
142        mActualHeight = actualHeight;
143        if (notifyListeners) {
144            notifyHeightChanged();
145        }
146    }
147
148    public void setActualHeight(int actualHeight) {
149        setActualHeight(actualHeight, true);
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 getMaxHeight() {
165        return getHeight();
166    }
167
168    /**
169     * @return The minimum height of this notification.
170     */
171    public int getMinHeight() {
172        return getHeight();
173    }
174
175    /**
176     * Sets the notification as dimmed. The default implementation does nothing.
177     *
178     * @param dimmed Whether the notification should be dimmed.
179     * @param fade Whether an animation should be played to change the state.
180     */
181    public void setDimmed(boolean dimmed, boolean fade) {
182    }
183
184    /**
185     * Sets the notification as dark. The default implementation does nothing.
186     *
187     * @param dark Whether the notification should be dark.
188     * @param fade Whether an animation should be played to change the state.
189     * @param delay If fading, the delay of the animation.
190     */
191    public void setDark(boolean dark, boolean fade, long delay) {
192        mDark = dark;
193    }
194
195    public boolean isDark() {
196        return mDark;
197    }
198
199    /**
200     * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
201     * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
202     * of a stack scroller update such that the updated intrinsic height (which is dependent on
203     * whether private or public layout is showing) gets taken into account into all layout
204     * calculations.
205     */
206    public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
207    }
208
209    /**
210     * Sets whether the notification should hide its private contents if it is sensitive.
211     */
212    public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
213            long duration) {
214    }
215
216    /**
217     * @return The desired notification height.
218     */
219    public int getIntrinsicHeight() {
220        return getHeight();
221    }
222
223    /**
224     * Sets the amount this view should be clipped from the top. This is used when an expanded
225     * notification is scrolling in the top or bottom stack.
226     *
227     * @param clipTopAmount The amount of pixels this view should be clipped from top.
228     */
229    public void setClipTopAmount(int clipTopAmount) {
230        mClipTopAmount = clipTopAmount;
231    }
232
233    public int getClipTopAmount() {
234        return mClipTopAmount;
235    }
236
237    public void setOnHeightChangedListener(OnHeightChangedListener listener) {
238        mOnHeightChangedListener = listener;
239    }
240
241    /**
242     * @return Whether we can expand this views content.
243     */
244    public boolean isContentExpandable() {
245        return false;
246    }
247
248    public void notifyHeightChanged() {
249        if (mOnHeightChangedListener != null) {
250            mOnHeightChangedListener.onHeightChanged(this);
251        }
252    }
253
254    public boolean isTransparent() {
255        return false;
256    }
257
258    /**
259     * Perform a remove animation on this view.
260     *
261     * @param duration The duration of the remove animation.
262     * @param translationDirection The direction value from [-1 ... 1] indicating in which the
263     *                             animation should be performed. A value of -1 means that The
264     *                             remove animation should be performed upwards,
265     *                             such that the  child appears to be going away to the top. 1
266     *                             Should mean the opposite.
267     * @param onFinishedRunnable A runnable which should be run when the animation is finished.
268     */
269    public abstract void performRemoveAnimation(long duration, float translationDirection,
270            Runnable onFinishedRunnable);
271
272    public abstract void performAddAnimation(long delay, long duration);
273
274    public void setBelowSpeedBump(boolean below) {
275    }
276
277    public void onHeightReset() {
278        if (mOnHeightChangedListener != null) {
279            mOnHeightChangedListener.onReset(this);
280        }
281    }
282
283    /**
284     * This method returns the drawing rect for the view which is different from the regular
285     * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
286     * position 0 and usually the translation is neglected. Since we are manually clipping this
287     * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
288     * ensure that accessibility and focusing work correctly.
289     *
290     * @param outRect The (scrolled) drawing bounds of the view.
291     */
292    @Override
293    public void getDrawingRect(Rect outRect) {
294        super.getDrawingRect(outRect);
295        outRect.left += getTranslationX();
296        outRect.right += getTranslationX();
297        outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
298        outRect.top += getTranslationY() + getClipTopAmount();
299    }
300
301    /**
302     * A listener notifying when {@link #getActualHeight} changes.
303     */
304    public interface OnHeightChangedListener {
305
306        /**
307         * @param view the view for which the height changed, or {@code null} if just the top
308         *             padding or the padding between the elements changed
309         */
310        void onHeightChanged(ExpandableView view);
311
312        /**
313         * Called when the view is reset and therefore the height will change abruptly
314         *
315         * @param view The view which was reset.
316         */
317        void onReset(ExpandableView view);
318    }
319}
320