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