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 
17 package com.android.systemui.statusbar.notification.row;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.graphics.Canvas;
22 import android.graphics.PorterDuff;
23 import android.graphics.PorterDuffXfermode;
24 import android.graphics.drawable.Drawable;
25 import android.graphics.drawable.GradientDrawable;
26 import android.graphics.drawable.LayerDrawable;
27 import android.graphics.drawable.RippleDrawable;
28 import android.util.AttributeSet;
29 import android.view.View;
30 
31 import com.android.internal.util.ArrayUtils;
32 import com.android.systemui.Interpolators;
33 import com.android.systemui.R;
34 import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
35 
36 /**
37  * A view that can be used for both the dimmed and normal background of an notification.
38  */
39 public class NotificationBackgroundView extends View {
40 
41     private final boolean mDontModifyCorners;
42     private Drawable mBackground;
43     private int mClipTopAmount;
44     private int mActualHeight;
45     private int mClipBottomAmount;
46     private int mTintColor;
47     private float[] mCornerRadii = new float[8];
48     private boolean mBottomIsRounded;
49     private boolean mLastInSection;
50     private boolean mFirstInSection;
51     private int mBackgroundTop;
52     private boolean mBottomAmountClips = true;
53     private boolean mExpandAnimationRunning;
54     private float mActualWidth;
55     private int mDrawableAlpha = 255;
56     private boolean mIsPressedAllowed;
57 
58     private boolean mTopAmountRounded;
59     private float mDistanceToTopRoundness;
60 
NotificationBackgroundView(Context context, AttributeSet attrs)61     public NotificationBackgroundView(Context context, AttributeSet attrs) {
62         super(context, attrs);
63         mDontModifyCorners = getResources().getBoolean(
64                 R.bool.config_clipNotificationsToOutline);
65     }
66 
67     @Override
onDraw(Canvas canvas)68     protected void onDraw(Canvas canvas) {
69         if (mClipTopAmount + mClipBottomAmount < mActualHeight - mBackgroundTop
70                 || mExpandAnimationRunning) {
71             canvas.save();
72             if (!mExpandAnimationRunning) {
73                 canvas.clipRect(0, mClipTopAmount, getWidth(), mActualHeight - mClipBottomAmount);
74             }
75             draw(canvas, mBackground);
76             canvas.restore();
77         }
78     }
79 
draw(Canvas canvas, Drawable drawable)80     private void draw(Canvas canvas, Drawable drawable) {
81         if (drawable != null) {
82             int top = mBackgroundTop;
83             int bottom = mActualHeight;
84             if (mBottomIsRounded
85                     && mBottomAmountClips
86                     && !mExpandAnimationRunning
87                     && !mLastInSection) {
88                 bottom -= mClipBottomAmount;
89             }
90             int left = 0;
91             int right = getWidth();
92             if (mExpandAnimationRunning) {
93                 left = (int) ((getWidth() - mActualWidth) / 2.0f);
94                 right = (int) (left + mActualWidth);
95             }
96             if (mTopAmountRounded) {
97                 int clipTop = (int) (mClipTopAmount - mDistanceToTopRoundness);
98                 if (clipTop >= 0 || !mFirstInSection) {
99                     top += clipTop;
100                 }
101                 if (clipTop >= 0 && !mLastInSection) {
102                     bottom += clipTop;
103                 }
104             }
105             drawable.setBounds(left, top, right, bottom);
106             drawable.draw(canvas);
107         }
108     }
109 
110     @Override
verifyDrawable(Drawable who)111     protected boolean verifyDrawable(Drawable who) {
112         return super.verifyDrawable(who) || who == mBackground;
113     }
114 
115     @Override
drawableStateChanged()116     protected void drawableStateChanged() {
117         setState(getDrawableState());
118     }
119 
120     @Override
drawableHotspotChanged(float x, float y)121     public void drawableHotspotChanged(float x, float y) {
122         if (mBackground != null) {
123             mBackground.setHotspot(x, y);
124         }
125     }
126 
127     /**
128      * Sets a background drawable. As we need to change our bounds independently of layout, we need
129      * the notion of a background independently of the regular View background..
130      */
setCustomBackground(Drawable background)131     public void setCustomBackground(Drawable background) {
132         if (mBackground != null) {
133             mBackground.setCallback(null);
134             unscheduleDrawable(mBackground);
135         }
136         mBackground = background;
137         mBackground.mutate();
138         if (mBackground != null) {
139             mBackground.setCallback(this);
140             setTint(mTintColor);
141         }
142         if (mBackground instanceof RippleDrawable) {
143             ((RippleDrawable) mBackground).setForceSoftware(true);
144         }
145         updateBackgroundRadii();
146         invalidate();
147     }
148 
setCustomBackground(int drawableResId)149     public void setCustomBackground(int drawableResId) {
150         final Drawable d = mContext.getDrawable(drawableResId);
151         setCustomBackground(d);
152     }
153 
setTint(int tintColor)154     public void setTint(int tintColor) {
155         if (tintColor != 0) {
156             mBackground.setColorFilter(tintColor, PorterDuff.Mode.SRC_ATOP);
157         } else {
158             mBackground.clearColorFilter();
159         }
160         mTintColor = tintColor;
161         invalidate();
162     }
163 
setActualHeight(int actualHeight)164     public void setActualHeight(int actualHeight) {
165         if (mExpandAnimationRunning) {
166             return;
167         }
168         mActualHeight = actualHeight;
169         invalidate();
170     }
171 
getActualHeight()172     public int getActualHeight() {
173         return mActualHeight;
174     }
175 
setClipTopAmount(int clipTopAmount)176     public void setClipTopAmount(int clipTopAmount) {
177         mClipTopAmount = clipTopAmount;
178         invalidate();
179     }
180 
setClipBottomAmount(int clipBottomAmount)181     public void setClipBottomAmount(int clipBottomAmount) {
182         mClipBottomAmount = clipBottomAmount;
183         invalidate();
184     }
185 
setDistanceToTopRoundness(float distanceToTopRoundness)186     public void setDistanceToTopRoundness(float distanceToTopRoundness) {
187         if (distanceToTopRoundness != mDistanceToTopRoundness) {
188             mTopAmountRounded = distanceToTopRoundness >= 0;
189             mDistanceToTopRoundness = distanceToTopRoundness;
190             invalidate();
191         }
192     }
193 
194     @Override
hasOverlappingRendering()195     public boolean hasOverlappingRendering() {
196 
197         // Prevents this view from creating a layer when alpha is animating.
198         return false;
199     }
200 
setState(int[] drawableState)201     public void setState(int[] drawableState) {
202         if (mBackground != null && mBackground.isStateful()) {
203             if (!mIsPressedAllowed) {
204                 drawableState = ArrayUtils.removeInt(drawableState,
205                         com.android.internal.R.attr.state_pressed);
206             }
207             mBackground.setState(drawableState);
208         }
209     }
210 
setRippleColor(int color)211     public void setRippleColor(int color) {
212         if (mBackground instanceof RippleDrawable) {
213             RippleDrawable ripple = (RippleDrawable) mBackground;
214             ripple.setColor(ColorStateList.valueOf(color));
215         }
216     }
217 
setDrawableAlpha(int drawableAlpha)218     public void setDrawableAlpha(int drawableAlpha) {
219         mDrawableAlpha = drawableAlpha;
220         if (mExpandAnimationRunning) {
221             return;
222         }
223         mBackground.setAlpha(drawableAlpha);
224     }
225 
226     /**
227      * Sets the current top and bottom roundness amounts for this background, between 0.0 (not
228      * rounded) and 1.0 (maximally rounded).
229      */
setRoundness(float topRoundness, float bottomRoundness)230     public void setRoundness(float topRoundness, float bottomRoundness) {
231         if (topRoundness == mCornerRadii[0] && bottomRoundness == mCornerRadii[4]) {
232             return;
233         }
234         mBottomIsRounded = bottomRoundness != 0.0f;
235         mCornerRadii[0] = topRoundness;
236         mCornerRadii[1] = topRoundness;
237         mCornerRadii[2] = topRoundness;
238         mCornerRadii[3] = topRoundness;
239         mCornerRadii[4] = bottomRoundness;
240         mCornerRadii[5] = bottomRoundness;
241         mCornerRadii[6] = bottomRoundness;
242         mCornerRadii[7] = bottomRoundness;
243         updateBackgroundRadii();
244     }
245 
setBottomAmountClips(boolean clips)246     public void setBottomAmountClips(boolean clips) {
247         if (clips != mBottomAmountClips) {
248             mBottomAmountClips = clips;
249             invalidate();
250         }
251     }
252 
253     /** Sets whether this background belongs to the last notification in a section. */
setLastInSection(boolean lastInSection)254     public void setLastInSection(boolean lastInSection) {
255         mLastInSection = lastInSection;
256         invalidate();
257     }
258 
259     /** Sets whether this background belongs to the first notification in a section. */
setFirstInSection(boolean firstInSection)260     public void setFirstInSection(boolean firstInSection) {
261         mFirstInSection = firstInSection;
262         invalidate();
263     }
264 
updateBackgroundRadii()265     private void updateBackgroundRadii() {
266         if (mDontModifyCorners) {
267             return;
268         }
269         if (mBackground instanceof LayerDrawable) {
270             GradientDrawable gradientDrawable =
271                     (GradientDrawable) ((LayerDrawable) mBackground).getDrawable(0);
272             gradientDrawable.setCornerRadii(mCornerRadii);
273         }
274     }
275 
setBackgroundTop(int backgroundTop)276     public void setBackgroundTop(int backgroundTop) {
277         mBackgroundTop = backgroundTop;
278         invalidate();
279     }
280 
setExpandAnimationParams(ActivityLaunchAnimator.ExpandAnimationParameters params)281     public void setExpandAnimationParams(ActivityLaunchAnimator.ExpandAnimationParameters params) {
282         mActualHeight = params.getHeight();
283         mActualWidth = params.getWidth();
284         float alphaProgress = Interpolators.ALPHA_IN.getInterpolation(
285                 params.getProgress(
286                         ActivityLaunchAnimator.ANIMATION_DURATION_FADE_CONTENT /* delay */,
287                         ActivityLaunchAnimator.ANIMATION_DURATION_FADE_APP /* duration */));
288         mBackground.setAlpha((int) (mDrawableAlpha * (1.0f - alphaProgress)));
289         invalidate();
290     }
291 
setExpandAnimationRunning(boolean running)292     public void setExpandAnimationRunning(boolean running) {
293         mExpandAnimationRunning = running;
294         if (mBackground instanceof LayerDrawable) {
295             GradientDrawable gradientDrawable =
296                     (GradientDrawable) ((LayerDrawable) mBackground).getDrawable(0);
297             gradientDrawable.setXfermode(
298                     running ? new PorterDuffXfermode(PorterDuff.Mode.SRC) : null);
299             // Speed optimization: disable AA if transfer mode is not SRC_OVER. AA is not easy to
300             // spot during animation anyways.
301             gradientDrawable.setAntiAlias(!running);
302         }
303         if (!mExpandAnimationRunning) {
304             setDrawableAlpha(mDrawableAlpha);
305         }
306         invalidate();
307     }
308 
setPressedAllowed(boolean allowed)309     public void setPressedAllowed(boolean allowed) {
310         mIsPressedAllowed = allowed;
311     }
312 }
313