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