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