• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.android.systemui.util.ColorUtilKt.hexColorString;
20 
21 import android.content.Context;
22 import android.content.res.ColorStateList;
23 import android.graphics.Canvas;
24 import android.graphics.PorterDuff;
25 import android.graphics.drawable.Drawable;
26 import android.graphics.drawable.GradientDrawable;
27 import android.graphics.drawable.LayerDrawable;
28 import android.graphics.drawable.RippleDrawable;
29 import android.util.AttributeSet;
30 import android.view.View;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 
35 import com.android.internal.util.ContrastColorUtil;
36 import com.android.settingslib.Utils;
37 import com.android.systemui.Dumpable;
38 import com.android.systemui.res.R;
39 import com.android.systemui.util.DrawableDumpKt;
40 
41 import java.io.PrintWriter;
42 import java.util.Arrays;
43 
44 /**
45  * A view that can be used for both the dimmed and normal background of an notification.
46  */
47 public class NotificationBackgroundView extends View implements Dumpable {
48 
49     private final boolean mDontModifyCorners;
50     private Drawable mBackground;
51     private int mClipTopAmount;
52     private int mClipBottomAmount;
53     private int mTintColor;
54     @Nullable private Integer mRippleColor;
55     private final float[] mCornerRadii = new float[8];
56     private final float[] mFocusOverlayCornerRadii = new float[8];
57     private float mFocusOverlayStroke = 0;
58     private boolean mBottomIsRounded;
59     private boolean mBottomAmountClips = true;
60     private int mActualHeight = -1;
61     private int mActualWidth = -1;
62     private boolean mExpandAnimationRunning;
63     private int mExpandAnimationWidth = -1;
64     private int mExpandAnimationHeight = -1;
65     private int mDrawableAlpha = 255;
66     private final ColorStateList mLightColoredStatefulColors;
67     private final ColorStateList mDarkColoredStatefulColors;
68     private final int mNormalColor;
69 
NotificationBackgroundView(Context context, AttributeSet attrs)70     public NotificationBackgroundView(Context context, AttributeSet attrs) {
71         super(context, attrs);
72         mDontModifyCorners = getResources().getBoolean(R.bool.config_clipNotificationsToOutline);
73         mLightColoredStatefulColors = getResources().getColorStateList(
74                 R.color.notification_state_color_light);
75         mDarkColoredStatefulColors = getResources().getColorStateList(
76                 R.color.notification_state_color_dark);
77         mNormalColor = Utils.getColorAttrDefaultColor(mContext,
78                 com.android.internal.R.attr.materialColorSurfaceContainerHigh);
79         mFocusOverlayStroke = getResources().getDimension(R.dimen.notification_focus_stroke_width);
80     }
81 
82     @Override
onDraw(Canvas canvas)83     protected void onDraw(Canvas canvas) {
84         if (mClipTopAmount + mClipBottomAmount < getActualHeight() || mExpandAnimationRunning) {
85             canvas.save();
86             if (!mExpandAnimationRunning) {
87                 canvas.clipRect(0, mClipTopAmount, getWidth(),
88                         getActualHeight() - mClipBottomAmount);
89             }
90             draw(canvas, mBackground);
91             canvas.restore();
92         }
93     }
94 
draw(Canvas canvas, Drawable drawable)95     private void draw(Canvas canvas, Drawable drawable) {
96         if (drawable != null) {
97             int top = 0;
98             int bottom = getActualHeight();
99             if (mBottomIsRounded
100                     && mBottomAmountClips
101                     && !mExpandAnimationRunning) {
102                 bottom -= mClipBottomAmount;
103             }
104             final boolean isRtl = isLayoutRtl();
105             final int width = getWidth();
106             final int actualWidth = getActualWidth();
107 
108             int left = isRtl ? width - actualWidth : 0;
109             int right = isRtl ? width : actualWidth;
110 
111             if (mExpandAnimationRunning) {
112                 // Horizontally center this background view inside of the container
113                 left = (int) ((width - actualWidth) / 2.0f);
114                 right = (int) (left + actualWidth);
115             }
116             drawable.setBounds(left, top, right, bottom);
117             drawable.draw(canvas);
118         }
119     }
120 
121     @Override
verifyDrawable(Drawable who)122     protected boolean verifyDrawable(Drawable who) {
123         return super.verifyDrawable(who) || who == mBackground;
124     }
125 
126     @Override
drawableStateChanged()127     protected void drawableStateChanged() {
128         setState(getDrawableState());
129     }
130 
131     @Override
drawableHotspotChanged(float x, float y)132     public void drawableHotspotChanged(float x, float y) {
133         if (mBackground != null) {
134             mBackground.setHotspot(x, y);
135         }
136     }
137 
138     /**
139      * Stateful colors are colors that will overlay on the notification original color when one of
140      * hover states, pressed states or other similar states is activated.
141      */
setStatefulColors()142     private void setStatefulColors() {
143         if (mTintColor != mNormalColor) {
144             ColorStateList newColor = ContrastColorUtil.isColorDark(mTintColor)
145                     ? mDarkColoredStatefulColors : mLightColoredStatefulColors;
146             ((GradientDrawable) getStatefulBackgroundLayer().mutate()).setColor(newColor);
147         }
148     }
149 
150     /**
151      * Sets a background drawable. As we need to change our bounds independently of layout, we need
152      * the notion of a background independently of the regular View background..
153      */
setCustomBackground(Drawable background)154     public void setCustomBackground(Drawable background) {
155         if (mBackground != null) {
156             mBackground.setCallback(null);
157             unscheduleDrawable(mBackground);
158         }
159         mBackground = background;
160         mRippleColor = null;
161         mBackground.mutate();
162         if (mBackground != null) {
163             mBackground.setCallback(this);
164             setTint(mTintColor);
165         }
166         if (mBackground instanceof RippleDrawable) {
167             ((RippleDrawable) mBackground).setForceSoftware(true);
168         }
169         updateBackgroundRadii();
170         invalidate();
171     }
172 
setCustomBackground(int drawableResId)173     public void setCustomBackground(int drawableResId) {
174         final Drawable d = mContext.getDrawable(drawableResId);
175         setCustomBackground(d);
176     }
177 
getBaseBackgroundLayer()178     private Drawable getBaseBackgroundLayer() {
179         return ((LayerDrawable) mBackground).getDrawable(0);
180     }
181 
getStatefulBackgroundLayer()182     private Drawable getStatefulBackgroundLayer() {
183         return ((LayerDrawable) mBackground).getDrawable(1);
184     }
185 
setTint(int tintColor)186     public void setTint(int tintColor) {
187         Drawable baseLayer = getBaseBackgroundLayer();
188         baseLayer.mutate().setTintMode(PorterDuff.Mode.SRC_ATOP);
189         baseLayer.setTint(tintColor);
190         mTintColor = tintColor;
191         setStatefulColors();
192         invalidate();
193     }
194 
setActualHeight(int actualHeight)195     public void setActualHeight(int actualHeight) {
196         if (mExpandAnimationRunning) {
197             return;
198         }
199         mActualHeight = actualHeight;
200         invalidate();
201     }
202 
getActualHeight()203     private int getActualHeight() {
204         if (mExpandAnimationRunning && mExpandAnimationHeight > -1) {
205             return mExpandAnimationHeight;
206         } else if (mActualHeight > -1) {
207             return mActualHeight;
208         }
209         return getHeight();
210     }
211 
setActualWidth(int actualWidth)212     public void setActualWidth(int actualWidth) {
213         mActualWidth = actualWidth;
214     }
215 
getActualWidth()216     private int getActualWidth() {
217         if (mExpandAnimationRunning && mExpandAnimationWidth > -1) {
218             return mExpandAnimationWidth;
219         } else if (mActualWidth > -1) {
220             return mActualWidth;
221         }
222         return getWidth();
223     }
224 
setClipTopAmount(int clipTopAmount)225     public void setClipTopAmount(int clipTopAmount) {
226         mClipTopAmount = clipTopAmount;
227         invalidate();
228     }
229 
setClipBottomAmount(int clipBottomAmount)230     public void setClipBottomAmount(int clipBottomAmount) {
231         mClipBottomAmount = clipBottomAmount;
232         invalidate();
233     }
234 
235     @Override
hasOverlappingRendering()236     public boolean hasOverlappingRendering() {
237 
238         // Prevents this view from creating a layer when alpha is animating.
239         return false;
240     }
241 
setState(int[] drawableState)242     public void setState(int[] drawableState) {
243         if (mBackground != null && mBackground.isStateful()) {
244             mBackground.setState(drawableState);
245         }
246     }
247 
setRippleColor(int color)248     public void setRippleColor(int color) {
249         if (mBackground instanceof RippleDrawable) {
250             RippleDrawable ripple = (RippleDrawable) mBackground;
251             ripple.setColor(ColorStateList.valueOf(color));
252             mRippleColor = color;
253         } else {
254             mRippleColor = null;
255         }
256     }
257 
setDrawableAlpha(int drawableAlpha)258     public void setDrawableAlpha(int drawableAlpha) {
259         mDrawableAlpha = drawableAlpha;
260         if (mExpandAnimationRunning) {
261             return;
262         }
263         mBackground.setAlpha(drawableAlpha);
264     }
265 
266     /**
267      * Sets the current top and bottom radius for this background.
268      */
setRadius(float topRoundness, float bottomRoundness)269     public void setRadius(float topRoundness, float bottomRoundness) {
270         if (topRoundness == mCornerRadii[0] && bottomRoundness == mCornerRadii[4]) {
271             return;
272         }
273         mBottomIsRounded = bottomRoundness != 0.0f;
274         mCornerRadii[0] = topRoundness;
275         mCornerRadii[1] = topRoundness;
276         mCornerRadii[2] = topRoundness;
277         mCornerRadii[3] = topRoundness;
278         mCornerRadii[4] = bottomRoundness;
279         mCornerRadii[5] = bottomRoundness;
280         mCornerRadii[6] = bottomRoundness;
281         mCornerRadii[7] = bottomRoundness;
282         updateBackgroundRadii();
283     }
284 
setBottomAmountClips(boolean clips)285     public void setBottomAmountClips(boolean clips) {
286         if (clips != mBottomAmountClips) {
287             mBottomAmountClips = clips;
288             invalidate();
289         }
290     }
291 
updateBackgroundRadii()292     private void updateBackgroundRadii() {
293         if (mDontModifyCorners) {
294             return;
295         }
296         if (mBackground instanceof LayerDrawable layerDrawable) {
297             int numberOfLayers = layerDrawable.getNumberOfLayers();
298             for (int i = 0; i < numberOfLayers; i++) {
299                 GradientDrawable gradientDrawable = (GradientDrawable) layerDrawable.getDrawable(i);
300                 gradientDrawable.setCornerRadii(mCornerRadii);
301             }
302             updateFocusOverlayRadii(layerDrawable);
303         }
304     }
305 
updateFocusOverlayRadii(LayerDrawable background)306     private void updateFocusOverlayRadii(LayerDrawable background) {
307         GradientDrawable overlay =
308                 (GradientDrawable) background.findDrawableByLayerId(
309                         R.id.notification_focus_overlay);
310         for (int i = 0; i < mCornerRadii.length; i++) {
311             // in theory subtracting mFocusOverlayStroke/2 should be enough but notification
312             // background is still peeking a bit from below - probably due to antialiasing or
313             // overlay uneven scaling. So let's subtract full mFocusOverlayStroke to make sure the
314             // radius is a bit smaller and covers background corners fully
315             mFocusOverlayCornerRadii[i] = Math.max(0, mCornerRadii[i] - mFocusOverlayStroke);
316         }
317         overlay.setCornerRadii(mFocusOverlayCornerRadii);
318     }
319 
320     /** Set the current expand animation size. */
setExpandAnimationSize(int width, int height)321     public void setExpandAnimationSize(int width, int height) {
322         mExpandAnimationHeight = height;
323         mExpandAnimationWidth = width;
324         invalidate();
325     }
326 
setExpandAnimationRunning(boolean running)327     public void setExpandAnimationRunning(boolean running) {
328         mExpandAnimationRunning = running;
329         if (mBackground instanceof LayerDrawable) {
330             GradientDrawable gradientDrawable =
331                     (GradientDrawable) ((LayerDrawable) mBackground).getDrawable(0);
332             // Speed optimization: disable AA if transfer mode is not SRC_OVER. AA is not easy to
333             // spot during animation anyways.
334             gradientDrawable.setAntiAlias(!running);
335         }
336         if (!mExpandAnimationRunning) {
337             setDrawableAlpha(mDrawableAlpha);
338         }
339         invalidate();
340     }
341 
342     @Override
dump(PrintWriter pw, @NonNull String[] args)343     public void dump(PrintWriter pw, @NonNull String[] args) {
344         pw.println("mDontModifyCorners: " + mDontModifyCorners);
345         pw.println("mClipTopAmount: " + mClipTopAmount);
346         pw.println("mClipBottomAmount: " + mClipBottomAmount);
347         pw.println("mCornerRadii: " + Arrays.toString(mCornerRadii));
348         pw.println("mBottomIsRounded: " + mBottomIsRounded);
349         pw.println("mBottomAmountClips: " + mBottomAmountClips);
350         pw.println("mActualWidth: " + mActualWidth);
351         pw.println("mActualHeight: " + mActualHeight);
352         pw.println("mTintColor: " + hexColorString(mTintColor));
353         pw.println("mRippleColor: " + hexColorString(mRippleColor));
354         pw.println("mBackground: " + DrawableDumpKt.dumpToString(mBackground));
355     }
356 
357     /** create a concise dump of this view's colors */
toDumpString()358     public String toDumpString() {
359         return "<NotificationBackgroundView"
360                 + " tintColor=" + hexColorString(mTintColor)
361                 + " rippleColor=" + hexColorString(mRippleColor)
362                 + " bgColor=" + DrawableDumpKt.getSolidColor(mBackground)
363                 + ">";
364 
365     }
366 }
367