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.ArrayUtils;
36 import com.android.systemui.Dumpable;
37 import com.android.systemui.R;
38 
39 import java.io.PrintWriter;
40 import java.util.Arrays;
41 
42 /**
43  * A view that can be used for both the dimmed and normal background of an notification.
44  */
45 public class NotificationBackgroundView extends View implements Dumpable {
46 
47     private final boolean mDontModifyCorners;
48     private Drawable mBackground;
49     private int mClipTopAmount;
50     private int mClipBottomAmount;
51     private int mTintColor;
52     @Nullable private Integer mRippleColor;
53     private final float[] mCornerRadii = new float[8];
54     private boolean mBottomIsRounded;
55     private boolean mBottomAmountClips = true;
56     private int mActualHeight = -1;
57     private int mActualWidth = -1;
58     private boolean mExpandAnimationRunning;
59     private int mExpandAnimationWidth = -1;
60     private int mExpandAnimationHeight = -1;
61     private int mDrawableAlpha = 255;
62     private boolean mIsPressedAllowed;
63 
NotificationBackgroundView(Context context, AttributeSet attrs)64     public NotificationBackgroundView(Context context, AttributeSet attrs) {
65         super(context, attrs);
66         mDontModifyCorners = getResources().getBoolean(
67                 R.bool.config_clipNotificationsToOutline);
68     }
69 
70     @Override
onDraw(Canvas canvas)71     protected void onDraw(Canvas canvas) {
72         if (mClipTopAmount + mClipBottomAmount < getActualHeight() || mExpandAnimationRunning) {
73             canvas.save();
74             if (!mExpandAnimationRunning) {
75                 canvas.clipRect(0, mClipTopAmount, getWidth(),
76                         getActualHeight() - mClipBottomAmount);
77             }
78             draw(canvas, mBackground);
79             canvas.restore();
80         }
81     }
82 
draw(Canvas canvas, Drawable drawable)83     private void draw(Canvas canvas, Drawable drawable) {
84         if (drawable != null) {
85             int top = 0;
86             int bottom = getActualHeight();
87             if (mBottomIsRounded
88                     && mBottomAmountClips
89                     && !mExpandAnimationRunning) {
90                 bottom -= mClipBottomAmount;
91             }
92             final boolean isRtl = isLayoutRtl();
93             final int width = getWidth();
94             final int actualWidth = getActualWidth();
95 
96             int left = isRtl ? width - actualWidth : 0;
97             int right = isRtl ? width : actualWidth;
98 
99             if (mExpandAnimationRunning) {
100                 // Horizontally center this background view inside of the container
101                 left = (int) ((width - actualWidth) / 2.0f);
102                 right = (int) (left + actualWidth);
103             }
104             drawable.setBounds(left, top, right, bottom);
105             drawable.draw(canvas);
106         }
107     }
108 
109     @Override
verifyDrawable(Drawable who)110     protected boolean verifyDrawable(Drawable who) {
111         return super.verifyDrawable(who) || who == mBackground;
112     }
113 
114     @Override
drawableStateChanged()115     protected void drawableStateChanged() {
116         setState(getDrawableState());
117     }
118 
119     @Override
drawableHotspotChanged(float x, float y)120     public void drawableHotspotChanged(float x, float y) {
121         if (mBackground != null) {
122             mBackground.setHotspot(x, y);
123         }
124     }
125 
126     /**
127      * Sets a background drawable. As we need to change our bounds independently of layout, we need
128      * the notion of a background independently of the regular View background..
129      */
setCustomBackground(Drawable background)130     public void setCustomBackground(Drawable background) {
131         if (mBackground != null) {
132             mBackground.setCallback(null);
133             unscheduleDrawable(mBackground);
134         }
135         mBackground = background;
136         mRippleColor = null;
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     private int getActualHeight() {
173         if (mExpandAnimationRunning && mExpandAnimationHeight > -1) {
174             return mExpandAnimationHeight;
175         } else if (mActualHeight > -1) {
176             return mActualHeight;
177         }
178         return getHeight();
179     }
180 
setActualWidth(int actualWidth)181     public void setActualWidth(int actualWidth) {
182         mActualWidth = actualWidth;
183     }
184 
getActualWidth()185     private int getActualWidth() {
186         if (mExpandAnimationRunning && mExpandAnimationWidth > -1) {
187             return mExpandAnimationWidth;
188         } else if (mActualWidth > -1) {
189             return mActualWidth;
190         }
191         return getWidth();
192     }
193 
setClipTopAmount(int clipTopAmount)194     public void setClipTopAmount(int clipTopAmount) {
195         mClipTopAmount = clipTopAmount;
196         invalidate();
197     }
198 
setClipBottomAmount(int clipBottomAmount)199     public void setClipBottomAmount(int clipBottomAmount) {
200         mClipBottomAmount = clipBottomAmount;
201         invalidate();
202     }
203 
204     @Override
hasOverlappingRendering()205     public boolean hasOverlappingRendering() {
206 
207         // Prevents this view from creating a layer when alpha is animating.
208         return false;
209     }
210 
setState(int[] drawableState)211     public void setState(int[] drawableState) {
212         if (mBackground != null && mBackground.isStateful()) {
213             if (!mIsPressedAllowed) {
214                 drawableState = ArrayUtils.removeInt(drawableState,
215                         com.android.internal.R.attr.state_pressed);
216             }
217             mBackground.setState(drawableState);
218         }
219     }
220 
setRippleColor(int color)221     public void setRippleColor(int color) {
222         if (mBackground instanceof RippleDrawable) {
223             RippleDrawable ripple = (RippleDrawable) mBackground;
224             ripple.setColor(ColorStateList.valueOf(color));
225             mRippleColor = color;
226         } else {
227             mRippleColor = null;
228         }
229     }
230 
setDrawableAlpha(int drawableAlpha)231     public void setDrawableAlpha(int drawableAlpha) {
232         mDrawableAlpha = drawableAlpha;
233         if (mExpandAnimationRunning) {
234             return;
235         }
236         mBackground.setAlpha(drawableAlpha);
237     }
238 
239     /**
240      * Sets the current top and bottom radius for this background.
241      */
setRadius(float topRoundness, float bottomRoundness)242     public void setRadius(float topRoundness, float bottomRoundness) {
243         if (topRoundness == mCornerRadii[0] && bottomRoundness == mCornerRadii[4]) {
244             return;
245         }
246         mBottomIsRounded = bottomRoundness != 0.0f;
247         mCornerRadii[0] = topRoundness;
248         mCornerRadii[1] = topRoundness;
249         mCornerRadii[2] = topRoundness;
250         mCornerRadii[3] = topRoundness;
251         mCornerRadii[4] = bottomRoundness;
252         mCornerRadii[5] = bottomRoundness;
253         mCornerRadii[6] = bottomRoundness;
254         mCornerRadii[7] = bottomRoundness;
255         updateBackgroundRadii();
256     }
257 
setBottomAmountClips(boolean clips)258     public void setBottomAmountClips(boolean clips) {
259         if (clips != mBottomAmountClips) {
260             mBottomAmountClips = clips;
261             invalidate();
262         }
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 
276     /** Set the current expand animation size. */
setExpandAnimationSize(int width, int height)277     public void setExpandAnimationSize(int width, int height) {
278         mExpandAnimationHeight = height;
279         mExpandAnimationWidth = width;
280         invalidate();
281     }
282 
setExpandAnimationRunning(boolean running)283     public void setExpandAnimationRunning(boolean running) {
284         mExpandAnimationRunning = running;
285         if (mBackground instanceof LayerDrawable) {
286             GradientDrawable gradientDrawable =
287                     (GradientDrawable) ((LayerDrawable) mBackground).getDrawable(0);
288             // Speed optimization: disable AA if transfer mode is not SRC_OVER. AA is not easy to
289             // spot during animation anyways.
290             gradientDrawable.setAntiAlias(!running);
291         }
292         if (!mExpandAnimationRunning) {
293             setDrawableAlpha(mDrawableAlpha);
294         }
295         invalidate();
296     }
297 
setPressedAllowed(boolean allowed)298     public void setPressedAllowed(boolean allowed) {
299         mIsPressedAllowed = allowed;
300     }
301 
302     @Override
dump(PrintWriter pw, @NonNull String[] args)303     public void dump(PrintWriter pw, @NonNull String[] args) {
304         pw.println("mDontModifyCorners: " + mDontModifyCorners);
305         pw.println("mClipTopAmount: " + mClipTopAmount);
306         pw.println("mClipBottomAmount: " + mClipBottomAmount);
307         pw.println("mCornerRadii: " + Arrays.toString(mCornerRadii));
308         pw.println("mBottomIsRounded: " + mBottomIsRounded);
309         pw.println("mBottomAmountClips: " + mBottomAmountClips);
310         pw.println("mActualWidth: " + mActualWidth);
311         pw.println("mActualHeight: " + mActualHeight);
312         pw.println("mTintColor: " + hexColorString(mTintColor));
313         pw.println("mRippleColor: " + hexColorString(mRippleColor));
314         pw.println("mBackground: " + mBackground);
315     }
316 }
317