2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.systemui.statusbar.notification.row;
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;
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;
38 * Like {@link ExpandableView}, but setting an outline for the height and clipping.
40 public abstract class ExpandableOutlineView extends ExpandableView {
42 private static final AnimatableProperty TOP_ROUNDNESS = AnimatableProperty.from(
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(
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();
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;
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.
77 protected boolean mShouldTranslateContents;
78 private boolean mTopAmountRounded;
79 private float mDistanceToTopRoundness = -1;
80 private float mExtraWidthForClipping;
81 private int mMinimumHeightForClipping = 0;
83 private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
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);
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
100 outline.setConvexPath(clipPath);
103 outline.setAlpha(mOutlineAlpha);
107 protected Path getClipPath(boolean ignoreTranslation) {
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));
125 left = mOutlineRect.left;
126 top = mOutlineRect.top;
127 right = mOutlineRect.right;
128 bottom = mOutlineRect.bottom;
130 height = bottom - top;
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);
143 getRoundedRectPath(left, top, right, bottom, topRoundness,
144 bottomRoundness, mTmpPath);
148 public static void getRoundedRectPath(int left, int top, int right, int bottom,
149 float topRoundness, float bottomRoundness, Path outPath) {
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);
162 outPath.moveTo(left, top);
163 outPath.lineTo(right, top);
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);
171 outPath.lineTo(right, bottom);
172 outPath.lineTo(left, bottom);
177 public ExpandableOutlineView(Context context, AttributeSet attrs) {
178 super(context, attrs);
179 setOutlineProvider(mProvider);
184 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
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,
196 intersectPath = mClipPath;
198 boolean clipped = false;
199 if (childNeedsClipping(child)) {
200 Path clipPath = getCustomClipPath(child);
201 if (clipPath == null) {
202 clipPath = getClipPath(false /* ignoreTranslation */);
204 if (clipPath != null) {
205 if (intersectPath != null) {
206 clipPath.op(intersectPath, Path.Op.INTERSECT);
208 canvas.clipPath(clipPath);
212 if (!clipped && intersectPath != null) {
213 canvas.clipPath(intersectPath);
215 boolean result = super.drawChild(canvas, child, drawingTime);
220 public void setExtraWidthForClipping(float extraWidthForClipping) {
221 mExtraWidthForClipping = extraWidthForClipping;
224 public void setMinimumHeightForClipping(int minimumHeightForClipping) {
225 mMinimumHeightForClipping = minimumHeightForClipping;
229 public void setDistanceToTopRoundness(float distanceToTopRoundness) {
230 super.setDistanceToTopRoundness(distanceToTopRoundness);
231 if (distanceToTopRoundness != mDistanceToTopRoundness) {
232 mTopAmountRounded = distanceToTopRoundness >= 0;
233 mDistanceToTopRoundness = distanceToTopRoundness;
238 protected boolean childNeedsClipping(View child) {
242 public boolean topAmountNeedsClipping() {
246 protected boolean isClippingNeeded() {
247 return mAlwaysRoundBothCorners || mCustomOutline || getTranslation() != 0 ;
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));
260 setClipToOutline(mAlwaysRoundBothCorners);
264 * Set the topRoundness of this view.
265 * @return Whether the roundness was changed.
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);
277 protected void applyRoundness() {
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;
288 return mCurrentTopRoundness * mOutlineRadius;
291 public float getCurrentTopRoundness() {
292 return mCurrentTopRoundness;
295 public float getCurrentBottomRoundness() {
296 return mCurrentBottomRoundness;
299 protected float getCurrentBackgroundRadiusBottom() {
300 return mCurrentBottomRoundness * mOutlineRadius;
304 * Set the bottom roundness of this view.
305 * @return Whether the roundness was changed.
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);
317 protected void setBackgroundTop(int backgroundTop) {
318 if (mBackgroundTop != backgroundTop) {
319 mBackgroundTop = backgroundTop;
324 private void setTopRoundnessInternal(float topRoundness) {
325 mCurrentTopRoundness = topRoundness;
329 private void setBottomRoundnessInternal(float bottomRoundness) {
330 mCurrentBottomRoundness = bottomRoundness;
334 public void onDensityOrFontScaleChanged() {
340 public void setActualHeight(int actualHeight, boolean notifyListeners) {
341 int previousHeight = getActualHeight();
342 super.setActualHeight(actualHeight, notifyListeners);
343 if (previousHeight != actualHeight) {
349 public void setClipTopAmount(int clipTopAmount) {
350 int previousAmount = getClipTopAmount();
351 super.setClipTopAmount(clipTopAmount);
352 if (previousAmount != clipTopAmount) {
358 public void setClipBottomAmount(int clipBottomAmount) {
359 int previousAmount = getClipBottomAmount();
360 super.setClipBottomAmount(clipBottomAmount);
361 if (previousAmount != clipBottomAmount) {
366 protected void setOutlineAlpha(float alpha) {
367 if (alpha != mOutlineAlpha) {
368 mOutlineAlpha = alpha;
374 public float getOutlineAlpha() {
375 return mOutlineAlpha;
378 protected void setOutlineRect(RectF rect) {
380 setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
382 mCustomOutline = false;
388 public int getOutlineTranslation() {
389 return mCustomOutline ? mOutlineRect.left : (int) getTranslation();
392 public void updateOutline() {
393 if (mCustomOutline) {
396 boolean hasOutline = needsOutline();
397 setOutlineProvider(hasOutline ? mProvider : null);
401 * @return Whether the view currently needs an outline. This is usually {@code false} in case
402 * it doesn't have a background.
404 protected boolean needsOutline() {
405 if (isChildInGroup()) {
406 return isGroupExpanded() && !isGroupExpansionChanging();
407 } else if (isSummaryWithChildren()) {
408 return !isGroupExpanded() || isGroupExpansionChanging();
413 public boolean isOutlineShowing() {
414 ViewOutlineProvider op = getOutlineProvider();
418 protected void setOutlineRect(float left, float top, float right, float bottom) {
419 mCustomOutline = true;
421 mOutlineRect.set((int) left, (int) top, (int) right, (int) bottom);
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);
429 public Path getCustomClipPath(View child) {