NotificationContentView.java revision 7b8157ef238d4eec7698338e768a602d4182cbb7
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.ColorFilter;
21import android.graphics.ColorMatrix;
22import android.graphics.ColorMatrixColorFilter;
23import android.graphics.Paint;
24import android.graphics.PorterDuff;
25import android.graphics.PorterDuffXfermode;
26import android.graphics.Rect;
27import android.graphics.drawable.Drawable;
28import android.util.AttributeSet;
29import android.view.View;
30import android.view.ViewTreeObserver;
31import android.view.animation.Interpolator;
32import android.view.animation.LinearInterpolator;
33import android.widget.FrameLayout;
34import android.widget.ImageView;
35import com.android.systemui.R;
36
37/**
38 * A frame layout containing the actual payload of the notification, including the contracted and
39 * expanded layout. This class is responsible for clipping the content and and switching between the
40 * expanded and contracted view depending on its clipped size.
41 */
42public class NotificationContentView extends FrameLayout {
43
44    private static final long ANIMATION_DURATION_LENGTH = 170;
45    private static final Paint INVERT_PAINT = createInvertPaint();
46    private static final ColorFilter NO_COLOR_FILTER = new ColorFilter();
47
48    private final Rect mClipBounds = new Rect();
49
50    private View mContractedChild;
51    private View mExpandedChild;
52
53    private int mSmallHeight;
54    private int mClipTopAmount;
55    private int mActualHeight;
56
57    private final Interpolator mLinearInterpolator = new LinearInterpolator();
58
59    private boolean mContractedVisible = true;
60    private boolean mDark;
61
62    private final Paint mFadePaint = new Paint();
63    private boolean mAnimate;
64    private ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener
65            = new ViewTreeObserver.OnPreDrawListener() {
66        @Override
67        public boolean onPreDraw() {
68            mAnimate = true;
69            getViewTreeObserver().removeOnPreDrawListener(this);
70            return true;
71        }
72    };
73
74    public NotificationContentView(Context context, AttributeSet attrs) {
75        super(context, attrs);
76        mFadePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
77        reset(true);
78    }
79
80    @Override
81    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
82        super.onLayout(changed, left, top, right, bottom);
83        updateClipping();
84    }
85
86    @Override
87    protected void onAttachedToWindow() {
88        super.onAttachedToWindow();
89        updateVisibility();
90    }
91
92    public void reset(boolean resetActualHeight) {
93        if (mContractedChild != null) {
94            mContractedChild.animate().cancel();
95        }
96        if (mExpandedChild != null) {
97            mExpandedChild.animate().cancel();
98        }
99        removeAllViews();
100        mContractedChild = null;
101        mExpandedChild = null;
102        mSmallHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
103        mContractedVisible = true;
104        if (resetActualHeight) {
105            mActualHeight = mSmallHeight;
106        }
107    }
108
109    public View getContractedChild() {
110        return mContractedChild;
111    }
112
113    public View getExpandedChild() {
114        return mExpandedChild;
115    }
116
117    public void setContractedChild(View child) {
118        if (mContractedChild != null) {
119            mContractedChild.animate().cancel();
120            removeView(mContractedChild);
121        }
122        sanitizeContractedLayoutParams(child);
123        addView(child);
124        mContractedChild = child;
125        selectLayout(false /* animate */, true /* force */);
126    }
127
128    public void setExpandedChild(View child) {
129        if (mExpandedChild != null) {
130            mExpandedChild.animate().cancel();
131            removeView(mExpandedChild);
132        }
133        addView(child);
134        mExpandedChild = child;
135        selectLayout(false /* animate */, true /* force */);
136    }
137
138    @Override
139    protected void onVisibilityChanged(View changedView, int visibility) {
140        super.onVisibilityChanged(changedView, visibility);
141        updateVisibility();
142    }
143
144    private void updateVisibility() {
145        setVisible(isShown());
146    }
147
148    private void setVisible(final boolean isVisible) {
149        if (isVisible) {
150
151            // We only animate if we are drawn at least once, otherwise the view might animate when
152            // it's shown the first time
153            getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener);
154        } else {
155            getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
156            mAnimate = false;
157        }
158    }
159
160    public void setActualHeight(int actualHeight) {
161        mActualHeight = actualHeight;
162        selectLayout(mAnimate /* animate */, false /* force */);
163        updateClipping();
164    }
165
166    public int getMaxHeight() {
167
168        // The maximum height is just the laid out height.
169        return getHeight();
170    }
171
172    public int getMinHeight() {
173        return mSmallHeight;
174    }
175
176    public void setClipTopAmount(int clipTopAmount) {
177        mClipTopAmount = clipTopAmount;
178        updateClipping();
179    }
180
181    private void updateClipping() {
182        mClipBounds.set(0, mClipTopAmount, getWidth(), mActualHeight);
183        setClipBounds(mClipBounds);
184    }
185
186    private void sanitizeContractedLayoutParams(View contractedChild) {
187        LayoutParams lp = (LayoutParams) contractedChild.getLayoutParams();
188        lp.height = mSmallHeight;
189        contractedChild.setLayoutParams(lp);
190    }
191
192    private void selectLayout(boolean animate, boolean force) {
193        if (mContractedChild == null) {
194            return;
195        }
196        boolean showContractedChild = showContractedChild();
197        if (showContractedChild != mContractedVisible || force) {
198            if (animate && mExpandedChild != null) {
199                runSwitchAnimation(showContractedChild);
200            } else if (mExpandedChild != null) {
201                mContractedChild.setVisibility(showContractedChild ? View.VISIBLE : View.INVISIBLE);
202                mContractedChild.setAlpha(showContractedChild ? 1f : 0f);
203                mExpandedChild.setVisibility(showContractedChild ? View.INVISIBLE : View.VISIBLE);
204                mExpandedChild.setAlpha(showContractedChild ? 0f : 1f);
205            }
206        }
207        mContractedVisible = showContractedChild;
208    }
209
210    private void runSwitchAnimation(final boolean showContractedChild) {
211        mContractedChild.setVisibility(View.VISIBLE);
212        mExpandedChild.setVisibility(View.VISIBLE);
213        mContractedChild.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint);
214        mExpandedChild.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint);
215        setLayerType(LAYER_TYPE_HARDWARE, null);
216        mContractedChild.animate()
217                .alpha(showContractedChild ? 1f : 0f)
218                .setDuration(ANIMATION_DURATION_LENGTH)
219                .setInterpolator(mLinearInterpolator);
220        mExpandedChild.animate()
221                .alpha(showContractedChild ? 0f : 1f)
222                .setDuration(ANIMATION_DURATION_LENGTH)
223                .setInterpolator(mLinearInterpolator)
224                .withEndAction(new Runnable() {
225                    @Override
226                    public void run() {
227                        mContractedChild.setLayerType(LAYER_TYPE_NONE, null);
228                        mExpandedChild.setLayerType(LAYER_TYPE_NONE, null);
229                        setLayerType(LAYER_TYPE_NONE, null);
230                        mContractedChild.setVisibility(showContractedChild
231                                ? View.VISIBLE
232                                : View.INVISIBLE);
233                        mExpandedChild.setVisibility(showContractedChild
234                                ? View.INVISIBLE
235                                : View.VISIBLE);
236                    }
237                });
238    }
239
240    private boolean showContractedChild() {
241        return mActualHeight <= mSmallHeight || mExpandedChild == null;
242    }
243
244    public void notifyContentUpdated() {
245        selectLayout(false /* animate */, true /* force */);
246    }
247
248    public boolean isContentExpandable() {
249        return mExpandedChild != null;
250    }
251
252    public void setDark(boolean dark, boolean fade) {
253        if (mDark == dark || mContractedChild == null) return;
254        mDark = dark;
255        setImageViewDark(dark, fade, com.android.internal.R.id.right_icon);
256        setImageViewDark(dark, fade, com.android.internal.R.id.icon);
257    }
258
259    private void setImageViewDark(boolean dark, boolean fade, int imageViewId) {
260        // TODO: implement fade
261        final ImageView v = (ImageView) mContractedChild.findViewById(imageViewId);
262        if (v == null) return;
263        final Drawable d = v.getBackground();
264        if (dark) {
265            v.setLayerType(LAYER_TYPE_HARDWARE, INVERT_PAINT);
266            if (d != null) {
267                v.setTag(R.id.doze_saved_filter_tag, d.getColorFilter() != null ? d.getColorFilter()
268                        : NO_COLOR_FILTER);
269                d.setColorFilter(getResources().getColor(R.color.doze_small_icon_background_color),
270                        PorterDuff.Mode.SRC_ATOP);
271                v.setImageAlpha(getResources().getInteger(R.integer.doze_small_icon_alpha));
272            }
273        } else {
274            v.setLayerType(LAYER_TYPE_NONE, null);
275            if (d != null)  {
276                final ColorFilter filter = (ColorFilter) v.getTag(R.id.doze_saved_filter_tag);
277                if (filter != null) {
278                    d.setColorFilter(filter == NO_COLOR_FILTER ? null : filter);
279                    v.setTag(R.id.doze_saved_filter_tag, null);
280                }
281                v.setImageAlpha(0xff);
282            }
283        }
284    }
285
286    @Override
287    public boolean hasOverlappingRendering() {
288
289        // This is not really true, but good enough when fading from the contracted to the expanded
290        // layout, and saves us some layers.
291        return false;
292    }
293
294    private static Paint createInvertPaint() {
295        final Paint p = new Paint();
296        final float[] invert = {
297            -1f,  0f,  0f, 1f, 1f,
298             0f, -1f,  0f, 1f, 1f,
299             0f,  0f, -1f, 1f, 1f,
300             0f,  0f,  0f, 1f, 0f
301        };
302        p.setColorFilter(new ColorMatrixColorFilter(new ColorMatrix(invert)));
303        return p;
304    }
305}
306