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