OSDN Git Service

Organize notification classes in row/stack
[android-x86/frameworks-base.git] / packages / SystemUI / src / com / android / systemui / statusbar / notification / row / ExpandableOutlineView.java
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.settingslib.Utils;
31 import com.android.systemui.R;
32 import com.android.systemui.statusbar.notification.AnimatableProperty;
33 import com.android.systemui.statusbar.notification.PropertyAnimator;
34 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
35 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
36
37 /**
38  * Like {@link ExpandableView}, but setting an outline for the height and clipping.
39  */
40 public abstract class ExpandableOutlineView extends ExpandableView {
41
42     private static final AnimatableProperty TOP_ROUNDNESS = AnimatableProperty.from(
43             "topRoundness",
44             ExpandableOutlineView::setTopRoundnessInternal,
45             ExpandableOutlineView::getCurrentTopRoundness,
46             R.id.top_roundess_animator_tag,
47             R.id.top_roundess_animator_end_tag,
48             R.id.top_roundess_animator_start_tag);
49     private static final AnimatableProperty BOTTOM_ROUNDNESS = AnimatableProperty.from(
50             "bottomRoundness",
51             ExpandableOutlineView::setBottomRoundnessInternal,
52             ExpandableOutlineView::getCurrentBottomRoundness,
53             R.id.bottom_roundess_animator_tag,
54             R.id.bottom_roundess_animator_end_tag,
55             R.id.bottom_roundess_animator_start_tag);
56     private static final AnimationProperties ROUNDNESS_PROPERTIES =
57             new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
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 true} 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 mShouldTranslateContents;
78     private boolean mTopAmountRounded;
79     private float mDistanceToTopRoundness = -1;
80     private float mExtraWidthForClipping;
81     private int mMinimumHeightForClipping = 0;
82
83     private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
84         @Override
85         public void getOutline(View view, Outline outline) {
86             if (!mCustomOutline && mCurrentTopRoundness == 0.0f
87                     && mCurrentBottomRoundness == 0.0f && !mAlwaysRoundBothCorners
88                     && !mTopAmountRounded) {
89                 int translation = mShouldTranslateContents ? (int) getTranslation() : 0;
90                 int left = Math.max(translation, 0);
91                 int top = mClipTopAmount + mBackgroundTop;
92                 int right = getWidth() + Math.min(translation, 0);
93                 int bottom = Math.max(getActualHeight() - mClipBottomAmount, top);
94                 outline.setRect(left, top, right, bottom);
95             } else {
96                 Path clipPath = getClipPath(false /* ignoreTranslation */);
97                 if (clipPath != null && clipPath.isConvex()) {
98                     // The path might not be convex in border cases where the view is small and
99                     // clipped
100                     outline.setConvexPath(clipPath);
101                 }
102             }
103             outline.setAlpha(mOutlineAlpha);
104         }
105     };
106
107     protected Path getClipPath(boolean ignoreTranslation) {
108         int left;
109         int top;
110         int right;
111         int bottom;
112         int height;
113         float topRoundness = mAlwaysRoundBothCorners
114                 ? mOutlineRadius : getCurrentBackgroundRadiusTop();
115         if (!mCustomOutline) {
116             int translation = mShouldTranslateContents && !ignoreTranslation
117                     ? (int) getTranslation() : 0;
118             left = Math.max(translation, 0);
119             top = mClipTopAmount + mBackgroundTop;
120             right = getWidth() + Math.min(translation, 0);
121             // If the top is rounded we want the bottom to be at most at the top roundness, in order
122             // to avoid the shadow changing when scrolling up.
123             bottom = Math.max(getActualHeight() - mClipBottomAmount, (int) (top + topRoundness));
124         } else {
125             left = mOutlineRect.left;
126             top = mOutlineRect.top;
127             right = mOutlineRect.right;
128             bottom = mOutlineRect.bottom;
129         }
130         height = bottom - top;
131         if (height == 0) {
132             return EMPTY_PATH;
133         }
134         float bottomRoundness = mAlwaysRoundBothCorners
135                 ? mOutlineRadius : getCurrentBackgroundRadiusBottom();
136         if (topRoundness + bottomRoundness > height) {
137             float overShoot = topRoundness + bottomRoundness - height;
138             topRoundness -= overShoot * mCurrentTopRoundness
139                     / (mCurrentTopRoundness + mCurrentBottomRoundness);
140             bottomRoundness -= overShoot * mCurrentBottomRoundness
141                     / (mCurrentTopRoundness + mCurrentBottomRoundness);
142         }
143         getRoundedRectPath(left, top, right, bottom, topRoundness,
144                 bottomRoundness, mTmpPath);
145         return mTmpPath;
146     }
147
148     public static void getRoundedRectPath(int left, int top, int right, int bottom,
149             float topRoundness, float bottomRoundness, Path outPath) {
150         outPath.reset();
151         int width = right - left;
152         float topRoundnessX = topRoundness;
153         float bottomRoundnessX = bottomRoundness;
154         topRoundnessX = Math.min(width / 2, topRoundnessX);
155         bottomRoundnessX = Math.min(width / 2, bottomRoundnessX);
156         if (topRoundness > 0.0f) {
157             outPath.moveTo(left, top + topRoundness);
158             outPath.quadTo(left, top, left + topRoundnessX, top);
159             outPath.lineTo(right - topRoundnessX, top);
160             outPath.quadTo(right, top, right, top + topRoundness);
161         } else {
162             outPath.moveTo(left, top);
163             outPath.lineTo(right, top);
164         }
165         if (bottomRoundness > 0.0f) {
166             outPath.lineTo(right, bottom - bottomRoundness);
167             outPath.quadTo(right, bottom, right - bottomRoundnessX, bottom);
168             outPath.lineTo(left + bottomRoundnessX, bottom);
169             outPath.quadTo(left, bottom, left, bottom - bottomRoundness);
170         } else {
171             outPath.lineTo(right, bottom);
172             outPath.lineTo(left, bottom);
173         }
174         outPath.close();
175     }
176
177     public ExpandableOutlineView(Context context, AttributeSet attrs) {
178         super(context, attrs);
179         setOutlineProvider(mProvider);
180         initDimens();
181     }
182
183     @Override
184     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
185         canvas.save();
186         Path intersectPath = null;
187         if (mTopAmountRounded && topAmountNeedsClipping()) {
188             int left = (int) (- mExtraWidthForClipping / 2.0f);
189             int top = (int) (mClipTopAmount - mDistanceToTopRoundness);
190             int right = getWidth() + (int) (mExtraWidthForClipping + left);
191             int bottom = (int) Math.max(mMinimumHeightForClipping,
192                     Math.max(getActualHeight() - mClipBottomAmount, top + mOutlineRadius));
193             ExpandableOutlineView.getRoundedRectPath(left, top, right, bottom, mOutlineRadius,
194                     0.0f,
195                     mClipPath);
196             intersectPath = mClipPath;
197         }
198         boolean clipped = false;
199         if (childNeedsClipping(child)) {
200             Path clipPath = getCustomClipPath(child);
201             if (clipPath == null) {
202                 clipPath = getClipPath(false /* ignoreTranslation */);
203             }
204             if (clipPath != null) {
205                 if (intersectPath != null) {
206                     clipPath.op(intersectPath, Path.Op.INTERSECT);
207                 }
208                 canvas.clipPath(clipPath);
209                 clipped = true;
210             }
211         }
212         if (!clipped && intersectPath != null) {
213             canvas.clipPath(intersectPath);
214         }
215         boolean result = super.drawChild(canvas, child, drawingTime);
216         canvas.restore();
217         return result;
218     }
219
220     public void setExtraWidthForClipping(float extraWidthForClipping) {
221         mExtraWidthForClipping = extraWidthForClipping;
222     }
223
224     public void setMinimumHeightForClipping(int minimumHeightForClipping) {
225         mMinimumHeightForClipping = minimumHeightForClipping;
226     }
227
228     @Override
229     public void setDistanceToTopRoundness(float distanceToTopRoundness) {
230         super.setDistanceToTopRoundness(distanceToTopRoundness);
231         if (distanceToTopRoundness != mDistanceToTopRoundness) {
232             mTopAmountRounded = distanceToTopRoundness >= 0;
233             mDistanceToTopRoundness = distanceToTopRoundness;
234             applyRoundness();
235         }
236     }
237
238     protected boolean childNeedsClipping(View child) {
239         return false;
240     }
241
242     public boolean topAmountNeedsClipping() {
243         return true;
244     }
245
246     protected boolean isClippingNeeded() {
247         return mAlwaysRoundBothCorners || mCustomOutline || getTranslation() != 0 ;
248     }
249
250     private void initDimens() {
251         Resources res = getResources();
252         mShouldTranslateContents =
253                 res.getBoolean(R.bool.config_translateNotificationContentsOnSwipe);
254         mOutlineRadius = res.getDimension(R.dimen.notification_shadow_radius);
255         mAlwaysRoundBothCorners = res.getBoolean(R.bool.config_clipNotificationsToOutline);
256         if (!mAlwaysRoundBothCorners) {
257             mOutlineRadius = res.getDimensionPixelSize(
258                     Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius));
259         }
260         setClipToOutline(mAlwaysRoundBothCorners);
261     }
262
263     /**
264      * Set the topRoundness of this view.
265      * @return Whether the roundness was changed.
266      */
267     public boolean setTopRoundness(float topRoundness, boolean animate) {
268         if (mTopRoundness != topRoundness) {
269             mTopRoundness = topRoundness;
270             PropertyAnimator.setProperty(this, TOP_ROUNDNESS, topRoundness,
271                     ROUNDNESS_PROPERTIES, animate);
272             return true;
273         }
274         return false;
275     }
276
277     protected void applyRoundness() {
278         invalidateOutline();
279         invalidate();
280     }
281
282     public float getCurrentBackgroundRadiusTop() {
283         // If this view is top amount notification view, it should always has round corners on top.
284         // It will be applied with applyRoundness()
285         if (mTopAmountRounded) {
286             return mOutlineRadius;
287         }
288         return mCurrentTopRoundness * mOutlineRadius;
289     }
290
291     public float getCurrentTopRoundness() {
292         return mCurrentTopRoundness;
293     }
294
295     public float getCurrentBottomRoundness() {
296         return mCurrentBottomRoundness;
297     }
298
299     protected float getCurrentBackgroundRadiusBottom() {
300         return mCurrentBottomRoundness * mOutlineRadius;
301     }
302
303     /**
304      * Set the bottom roundness of this view.
305      * @return Whether the roundness was changed.
306      */
307     public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
308         if (mBottomRoundness != bottomRoundness) {
309             mBottomRoundness = bottomRoundness;
310             PropertyAnimator.setProperty(this, BOTTOM_ROUNDNESS, bottomRoundness,
311                     ROUNDNESS_PROPERTIES, animate);
312             return true;
313         }
314         return false;
315     }
316
317     protected void setBackgroundTop(int backgroundTop) {
318         if (mBackgroundTop != backgroundTop) {
319             mBackgroundTop = backgroundTop;
320             invalidateOutline();
321         }
322     }
323
324     private void setTopRoundnessInternal(float topRoundness) {
325         mCurrentTopRoundness = topRoundness;
326         applyRoundness();
327     }
328
329     private void setBottomRoundnessInternal(float bottomRoundness) {
330         mCurrentBottomRoundness = bottomRoundness;
331         applyRoundness();
332     }
333
334     public void onDensityOrFontScaleChanged() {
335         initDimens();
336         applyRoundness();
337     }
338
339     @Override
340     public void setActualHeight(int actualHeight, boolean notifyListeners) {
341         int previousHeight = getActualHeight();
342         super.setActualHeight(actualHeight, notifyListeners);
343         if (previousHeight != actualHeight) {
344             applyRoundness();
345         }
346     }
347
348     @Override
349     public void setClipTopAmount(int clipTopAmount) {
350         int previousAmount = getClipTopAmount();
351         super.setClipTopAmount(clipTopAmount);
352         if (previousAmount != clipTopAmount) {
353             applyRoundness();
354         }
355     }
356
357     @Override
358     public void setClipBottomAmount(int clipBottomAmount) {
359         int previousAmount = getClipBottomAmount();
360         super.setClipBottomAmount(clipBottomAmount);
361         if (previousAmount != clipBottomAmount) {
362             applyRoundness();
363         }
364     }
365
366     protected void setOutlineAlpha(float alpha) {
367         if (alpha != mOutlineAlpha) {
368             mOutlineAlpha = alpha;
369             applyRoundness();
370         }
371     }
372
373     @Override
374     public float getOutlineAlpha() {
375         return mOutlineAlpha;
376     }
377
378     protected void setOutlineRect(RectF rect) {
379         if (rect != null) {
380             setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
381         } else {
382             mCustomOutline = false;
383             applyRoundness();
384         }
385     }
386
387     @Override
388     public int getOutlineTranslation() {
389         return mCustomOutline ? mOutlineRect.left : (int) getTranslation();
390     }
391
392     public void updateOutline() {
393         if (mCustomOutline) {
394             return;
395         }
396         boolean hasOutline = needsOutline();
397         setOutlineProvider(hasOutline ? mProvider : null);
398     }
399
400     /**
401      * @return Whether the view currently needs an outline. This is usually {@code false} in case
402      * it doesn't have a background.
403      */
404     protected boolean needsOutline() {
405         if (isChildInGroup()) {
406             return isGroupExpanded() && !isGroupExpansionChanging();
407         } else if (isSummaryWithChildren()) {
408             return !isGroupExpanded() || isGroupExpansionChanging();
409         }
410         return true;
411     }
412
413     public boolean isOutlineShowing() {
414         ViewOutlineProvider op = getOutlineProvider();
415         return op != null;
416     }
417
418     protected void setOutlineRect(float left, float top, float right, float bottom) {
419         mCustomOutline = true;
420
421         mOutlineRect.set((int) left, (int) top, (int) right, (int) bottom);
422
423         // Outlines need to be at least 1 dp
424         mOutlineRect.bottom = (int) Math.max(top, mOutlineRect.bottom);
425         mOutlineRect.right = (int) Math.max(left, mOutlineRect.right);
426         applyRoundness();
427     }
428
429     public Path getCustomClipPath(View child) {
430         return null;
431     }
432 }