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