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.Resources;
21 import android.graphics.Canvas;
22 import android.graphics.Outline;
23 import android.graphics.Path;
24 import android.graphics.Rect;
25 import android.graphics.RectF;
26 import android.util.AttributeSet;
27 import android.util.IndentingPrintWriter;
28 import android.view.View;
29 import android.view.ViewOutlineProvider;
30 
31 import com.android.systemui.R;
32 import com.android.systemui.flags.Flags;
33 import com.android.systemui.flags.ViewRefactorFlag;
34 import com.android.systemui.statusbar.notification.RoundableState;
35 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
36 import com.android.systemui.util.DumpUtilsKt;
37 
38 import java.io.PrintWriter;
39 
40 /**
41  * Like {@link ExpandableView}, but setting an outline for the height and clipping.
42  */
43 public abstract class ExpandableOutlineView extends ExpandableView {
44 
45     private RoundableState mRoundableState;
46     private static final Path EMPTY_PATH = new Path();
47     private final Rect mOutlineRect = new Rect();
48     private boolean mCustomOutline;
49     private float mOutlineAlpha = -1f;
50     private boolean mAlwaysRoundBothCorners;
51     private Path mTmpPath = new Path();
52     protected final ViewRefactorFlag mImprovedHunAnimation =
53             new ViewRefactorFlag(Flags.IMPROVED_HUN_ANIMATIONS);
54 
55     /**
56      * {@code false} if the children views of the {@link ExpandableOutlineView} are translated when
57      * it is moved. Otherwise, the translation is set on the {@code ExpandableOutlineView} itself.
58      */
59     protected boolean mDismissUsingRowTranslationX = true;
60 
61     private float[] mTmpCornerRadii = new float[8];
62 
63     private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
64         @Override
65         public void getOutline(View view, Outline outline) {
66             if (!mCustomOutline && !hasRoundedCorner() && !mAlwaysRoundBothCorners) {
67                 // Only when translating just the contents, does the outline need to be shifted.
68                 int translation = !mDismissUsingRowTranslationX ? (int) getTranslation() : 0;
69                 int left = Math.max(translation, 0);
70                 int top = mClipTopAmount;
71                 int right = getWidth() + Math.min(translation, 0);
72                 int bottom = Math.max(getActualHeight() - mClipBottomAmount, top);
73                 outline.setRect(left, top, right, bottom);
74             } else {
75                 Path clipPath = getClipPath(false /* ignoreTranslation */);
76                 if (clipPath != null) {
77                     outline.setPath(clipPath);
78                 }
79             }
80             outline.setAlpha(mOutlineAlpha);
81         }
82     };
83 
84     @Override
getRoundableState()85     public RoundableState getRoundableState() {
86         return mRoundableState;
87     }
88 
89     @Override
getClipHeight()90     public int getClipHeight() {
91         if (mCustomOutline) {
92             return mOutlineRect.height();
93         }
94 
95         return super.getClipHeight();
96     }
97 
getClipPath(boolean ignoreTranslation)98     protected Path getClipPath(boolean ignoreTranslation) {
99         int left;
100         int top;
101         int right;
102         int bottom;
103         int height;
104         float topRadius = mAlwaysRoundBothCorners ? getMaxRadius() : getTopCornerRadius();
105         if (!mCustomOutline) {
106             // The outline just needs to be shifted if we're translating the contents. Otherwise
107             // it's already in the right place.
108             int translation = !mDismissUsingRowTranslationX && !ignoreTranslation
109                     ? (int) getTranslation() : 0;
110             int halfExtraWidth = (int) (mExtraWidthForClipping / 2.0f);
111             left = Math.max(translation, 0) - halfExtraWidth;
112             top = mClipTopAmount;
113             right = getWidth() + halfExtraWidth + Math.min(translation, 0);
114             // If the top is rounded we want the bottom to be at most at the top roundness, in order
115             // to avoid the shadow changing when scrolling up.
116             bottom = Math.max(mMinimumHeightForClipping,
117                     Math.max(getActualHeight() - mClipBottomAmount, (int) (top + topRadius)));
118         } else {
119             left = mOutlineRect.left;
120             top = mOutlineRect.top;
121             right = mOutlineRect.right;
122             bottom = mOutlineRect.bottom;
123         }
124         height = bottom - top;
125         if (height == 0) {
126             return EMPTY_PATH;
127         }
128         float bottomRadius = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
129         if (!mImprovedHunAnimation.isEnabled() && (topRadius + bottomRadius > height)) {
130             float overShoot = topRadius + bottomRadius - height;
131             float currentTopRoundness = getTopRoundness();
132             float currentBottomRoundness = getBottomRoundness();
133             topRadius -= overShoot * currentTopRoundness
134                     / (currentTopRoundness + currentBottomRoundness);
135             bottomRadius -= overShoot * currentBottomRoundness
136                     / (currentTopRoundness + currentBottomRoundness);
137         }
138         getRoundedRectPath(left, top, right, bottom, topRadius, bottomRadius, mTmpPath);
139         return mTmpPath;
140     }
141 
142     /**
143      * Add a round rect in {@code outPath}
144      * @param outPath destination path
145      */
getRoundedRectPath( int left, int top, int right, int bottom, float topRoundness, float bottomRoundness, Path outPath)146     public void getRoundedRectPath(
147             int left,
148             int top,
149             int right,
150             int bottom,
151             float topRoundness,
152             float bottomRoundness,
153             Path outPath) {
154         outPath.reset();
155         mTmpCornerRadii[0] = topRoundness;
156         mTmpCornerRadii[1] = topRoundness;
157         mTmpCornerRadii[2] = topRoundness;
158         mTmpCornerRadii[3] = topRoundness;
159         mTmpCornerRadii[4] = bottomRoundness;
160         mTmpCornerRadii[5] = bottomRoundness;
161         mTmpCornerRadii[6] = bottomRoundness;
162         mTmpCornerRadii[7] = bottomRoundness;
163         outPath.addRoundRect(left, top, right, bottom, mTmpCornerRadii, Path.Direction.CW);
164     }
165 
ExpandableOutlineView(Context context, AttributeSet attrs)166     public ExpandableOutlineView(Context context, AttributeSet attrs) {
167         super(context, attrs);
168         setOutlineProvider(mProvider);
169         initDimens();
170     }
171 
172     @Override
drawChild(Canvas canvas, View child, long drawingTime)173     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
174         canvas.save();
175         Path clipPath = null;
176         Path childClipPath = null;
177         if (childNeedsClipping(child)) {
178             clipPath = getCustomClipPath(child);
179             if (clipPath == null) {
180                 clipPath = getClipPath(false /* ignoreTranslation */);
181             }
182             // If the notification uses "RowTranslationX" as dismiss behavior, we should clip the
183             // children instead.
184             if (mDismissUsingRowTranslationX && child instanceof NotificationChildrenContainer) {
185                 childClipPath = clipPath;
186                 clipPath = null;
187             }
188         }
189 
190         if (child instanceof NotificationChildrenContainer) {
191             ((NotificationChildrenContainer) child).setChildClipPath(childClipPath);
192         }
193         if (clipPath != null) {
194             canvas.clipPath(clipPath);
195         }
196 
197         boolean result = super.drawChild(canvas, child, drawingTime);
198         canvas.restore();
199         return result;
200     }
201 
202     @Override
setExtraWidthForClipping(float extraWidthForClipping)203     public void setExtraWidthForClipping(float extraWidthForClipping) {
204         super.setExtraWidthForClipping(extraWidthForClipping);
205         invalidate();
206     }
207 
208     @Override
setMinimumHeightForClipping(int minimumHeightForClipping)209     public void setMinimumHeightForClipping(int minimumHeightForClipping) {
210         super.setMinimumHeightForClipping(minimumHeightForClipping);
211         invalidate();
212     }
213 
childNeedsClipping(View child)214     protected boolean childNeedsClipping(View child) {
215         return false;
216     }
217 
isClippingNeeded()218     protected boolean isClippingNeeded() {
219         // When translating the contents instead of the overall view, we need to make sure we clip
220         // rounded to the contents.
221         boolean forTranslation = getTranslation() != 0 && !mDismissUsingRowTranslationX;
222         return mAlwaysRoundBothCorners || mCustomOutline || forTranslation;
223     }
224 
initDimens()225     private void initDimens() {
226         Resources res = getResources();
227         mAlwaysRoundBothCorners = res.getBoolean(R.bool.config_clipNotificationsToOutline);
228         float maxRadius;
229         if (mAlwaysRoundBothCorners) {
230             maxRadius = res.getDimension(R.dimen.notification_shadow_radius);
231         } else {
232             maxRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius);
233         }
234         if (mRoundableState == null) {
235             mRoundableState = new RoundableState(this, this, maxRadius);
236         } else {
237             mRoundableState.setMaxRadius(maxRadius);
238         }
239         setClipToOutline(mAlwaysRoundBothCorners);
240     }
241 
242     @Override
applyRoundnessAndInvalidate()243     public void applyRoundnessAndInvalidate() {
244         invalidateOutline();
245         super.applyRoundnessAndInvalidate();
246     }
247 
onDensityOrFontScaleChanged()248     public void onDensityOrFontScaleChanged() {
249         initDimens();
250         applyRoundnessAndInvalidate();
251     }
252 
253     @Override
setActualHeight(int actualHeight, boolean notifyListeners)254     public void setActualHeight(int actualHeight, boolean notifyListeners) {
255         int previousHeight = getActualHeight();
256         super.setActualHeight(actualHeight, notifyListeners);
257         if (previousHeight != actualHeight) {
258             applyRoundnessAndInvalidate();
259         }
260     }
261 
262     @Override
setClipTopAmount(int clipTopAmount)263     public void setClipTopAmount(int clipTopAmount) {
264         int previousAmount = getClipTopAmount();
265         super.setClipTopAmount(clipTopAmount);
266         if (previousAmount != clipTopAmount) {
267             applyRoundnessAndInvalidate();
268         }
269     }
270 
271     @Override
setClipBottomAmount(int clipBottomAmount)272     public void setClipBottomAmount(int clipBottomAmount) {
273         int previousAmount = getClipBottomAmount();
274         super.setClipBottomAmount(clipBottomAmount);
275         if (previousAmount != clipBottomAmount) {
276             applyRoundnessAndInvalidate();
277         }
278     }
279 
setOutlineAlpha(float alpha)280     protected void setOutlineAlpha(float alpha) {
281         if (alpha != mOutlineAlpha) {
282             mOutlineAlpha = alpha;
283             applyRoundnessAndInvalidate();
284         }
285     }
286 
287     @Override
getOutlineAlpha()288     public float getOutlineAlpha() {
289         return mOutlineAlpha;
290     }
291 
setOutlineRect(RectF rect)292     protected void setOutlineRect(RectF rect) {
293         if (rect != null) {
294             setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
295         } else {
296             mCustomOutline = false;
297             applyRoundnessAndInvalidate();
298         }
299     }
300 
301     /**
302      * Set the dismiss behavior of the view.
303      *
304      * @param usingRowTranslationX {@code true} if the view should translate using regular
305      *                             translationX, otherwise the contents will be
306      *                             translated.
307      */
setDismissUsingRowTranslationX(boolean usingRowTranslationX)308     public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
309         mDismissUsingRowTranslationX = usingRowTranslationX;
310     }
311 
312     @Override
getOutlineTranslation()313     public int getOutlineTranslation() {
314         if (mCustomOutline) {
315             return mOutlineRect.left;
316         }
317         if (mDismissUsingRowTranslationX) {
318             return 0;
319         }
320         return (int) getTranslation();
321     }
322 
updateOutline()323     public void updateOutline() {
324         if (mCustomOutline) {
325             return;
326         }
327         boolean hasOutline = needsOutline();
328         setOutlineProvider(hasOutline ? mProvider : null);
329     }
330 
331     /**
332      * @return Whether the view currently needs an outline. This is usually {@code false} in case
333      * it doesn't have a background.
334      */
needsOutline()335     protected boolean needsOutline() {
336         if (isChildInGroup()) {
337             return isGroupExpanded() && !isGroupExpansionChanging();
338         } else if (isSummaryWithChildren()) {
339             return !isGroupExpanded() || isGroupExpansionChanging();
340         }
341         return true;
342     }
343 
isOutlineShowing()344     public boolean isOutlineShowing() {
345         ViewOutlineProvider op = getOutlineProvider();
346         return op != null;
347     }
348 
setOutlineRect(float left, float top, float right, float bottom)349     protected void setOutlineRect(float left, float top, float right, float bottom) {
350         mCustomOutline = true;
351 
352         mOutlineRect.set((int) left, (int) top, (int) right, (int) bottom);
353 
354         // Outlines need to be at least 1 dp
355         mOutlineRect.bottom = (int) Math.max(top, mOutlineRect.bottom);
356         mOutlineRect.right = (int) Math.max(left, mOutlineRect.right);
357         applyRoundnessAndInvalidate();
358     }
359 
getCustomClipPath(View child)360     public Path getCustomClipPath(View child) {
361         return null;
362     }
363 
364     @Override
dump(PrintWriter pwOriginal, String[] args)365     public void dump(PrintWriter pwOriginal, String[] args) {
366         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
367         super.dump(pw, args);
368         DumpUtilsKt.withIncreasedIndent(pw, () -> {
369             pw.println(getRoundableState().debugString());
370             if (DUMP_VERBOSE) {
371                 pw.println("mCustomOutline: " + mCustomOutline + " mOutlineRect: " + mOutlineRect);
372                 pw.println("mOutlineAlpha: " + mOutlineAlpha);
373                 pw.println("mAlwaysRoundBothCorners: " + mAlwaysRoundBothCorners);
374             }
375         });
376     }
377 
378 }
379