1 /* 2 * Copyright (C) 2021 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 android.view; 18 19 import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT; 20 import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT; 21 import static android.view.RoundedCorner.POSITION_TOP_LEFT; 22 import static android.view.RoundedCorner.POSITION_TOP_RIGHT; 23 import static android.view.Surface.ROTATION_0; 24 import static android.view.Surface.ROTATION_270; 25 import static android.view.Surface.ROTATION_90; 26 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.content.res.Resources; 30 import android.content.res.TypedArray; 31 import android.graphics.Point; 32 import android.os.Parcel; 33 import android.os.Parcelable; 34 import android.util.DisplayUtils; 35 import android.util.Pair; 36 import android.view.RoundedCorner.Position; 37 38 import com.android.internal.R; 39 import com.android.internal.annotations.GuardedBy; 40 import com.android.internal.annotations.VisibleForTesting; 41 42 import java.util.Arrays; 43 44 /** 45 * A class to create & manage all the {@link RoundedCorner} on the display. 46 * 47 * @hide 48 */ 49 public class RoundedCorners implements Parcelable { 50 51 public static final RoundedCorners NO_ROUNDED_CORNERS = new RoundedCorners( 52 new RoundedCorner(POSITION_TOP_LEFT), new RoundedCorner(POSITION_TOP_RIGHT), 53 new RoundedCorner(POSITION_BOTTOM_RIGHT), new RoundedCorner(POSITION_BOTTOM_LEFT)); 54 55 /** 56 * The number of possible positions at which rounded corners can be located. 57 */ 58 public static final int ROUNDED_CORNER_POSITION_LENGTH = 4; 59 60 private static final Object CACHE_LOCK = new Object(); 61 62 @GuardedBy("CACHE_LOCK") 63 private static int sCachedDisplayWidth; 64 @GuardedBy("CACHE_LOCK") 65 private static int sCachedDisplayHeight; 66 @GuardedBy("CACHE_LOCK") 67 private static Pair<Integer, Integer> sCachedRadii; 68 @GuardedBy("CACHE_LOCK") 69 private static RoundedCorners sCachedRoundedCorners; 70 71 @VisibleForTesting 72 public final RoundedCorner[] mRoundedCorners; 73 RoundedCorners(RoundedCorner[] roundedCorners)74 public RoundedCorners(RoundedCorner[] roundedCorners) { 75 mRoundedCorners = roundedCorners; 76 } 77 RoundedCorners(RoundedCorner topLeft, RoundedCorner topRight, RoundedCorner bottomRight, RoundedCorner bottomLeft)78 public RoundedCorners(RoundedCorner topLeft, RoundedCorner topRight, RoundedCorner bottomRight, 79 RoundedCorner bottomLeft) { 80 mRoundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH]; 81 mRoundedCorners[POSITION_TOP_LEFT] = topLeft; 82 mRoundedCorners[POSITION_TOP_RIGHT] = topRight; 83 mRoundedCorners[POSITION_BOTTOM_RIGHT] = bottomRight; 84 mRoundedCorners[POSITION_BOTTOM_LEFT] = bottomLeft; 85 } 86 RoundedCorners(RoundedCorners roundedCorners)87 public RoundedCorners(RoundedCorners roundedCorners) { 88 mRoundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH]; 89 for (int i = 0; i < ROUNDED_CORNER_POSITION_LENGTH; ++i) { 90 mRoundedCorners[i] = new RoundedCorner(roundedCorners.mRoundedCorners[i]); 91 } 92 } 93 94 /** 95 * Creates the rounded corners according to @android:dimen/rounded_corner_radius, 96 * @android:dimen/rounded_corner_radius_top and @android:dimen/rounded_corner_radius_bottom 97 */ fromResources( Resources res, String displayUniqueId, int displayWidth, int displayHeight)98 public static RoundedCorners fromResources( 99 Resources res, String displayUniqueId, int displayWidth, int displayHeight) { 100 return fromRadii(loadRoundedCornerRadii(res, displayUniqueId), displayWidth, displayHeight); 101 } 102 103 /** 104 * Creates the rounded corners from radius 105 */ 106 @VisibleForTesting fromRadii(Pair<Integer, Integer> radii, int displayWidth, int displayHeight)107 public static RoundedCorners fromRadii(Pair<Integer, Integer> radii, int displayWidth, 108 int displayHeight) { 109 if (radii == null) { 110 return null; 111 } 112 113 synchronized (CACHE_LOCK) { 114 if (radii.equals(sCachedRadii) && sCachedDisplayWidth == displayWidth 115 && sCachedDisplayHeight == displayHeight) { 116 return sCachedRoundedCorners; 117 } 118 } 119 120 final RoundedCorner[] roundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH]; 121 final int topRadius = radii.first > 0 ? radii.first : 0; 122 final int bottomRadius = radii.second > 0 ? radii.second : 0; 123 for (int i = 0; i < ROUNDED_CORNER_POSITION_LENGTH; i++) { 124 roundedCorners[i] = createRoundedCorner( 125 i, 126 i <= POSITION_TOP_RIGHT ? topRadius : bottomRadius, 127 displayWidth, 128 displayHeight); 129 } 130 131 final RoundedCorners result = new RoundedCorners(roundedCorners); 132 synchronized (CACHE_LOCK) { 133 sCachedDisplayWidth = displayWidth; 134 sCachedDisplayHeight = displayHeight; 135 sCachedRadii = radii; 136 sCachedRoundedCorners = result; 137 } 138 return result; 139 } 140 141 /** 142 * Loads the rounded corner radii from resources. 143 * 144 * @param res 145 * @param displayUniqueId the display unique id. 146 * @return a Pair of radius. The first is the top rounded corner radius and second is the 147 * bottom corner radius. 148 */ 149 @Nullable loadRoundedCornerRadii( Resources res, String displayUniqueId)150 private static Pair<Integer, Integer> loadRoundedCornerRadii( 151 Resources res, String displayUniqueId) { 152 final int radiusDefault = getRoundedCornerRadius(res, displayUniqueId); 153 final int radiusTop = getRoundedCornerTopRadius(res, displayUniqueId); 154 final int radiusBottom = getRoundedCornerBottomRadius(res, displayUniqueId); 155 if (radiusDefault == 0 && radiusTop == 0 && radiusBottom == 0) { 156 return null; 157 } 158 final Pair<Integer, Integer> radii = new Pair<>( 159 radiusTop > 0 ? radiusTop : radiusDefault, 160 radiusBottom > 0 ? radiusBottom : radiusDefault); 161 return radii; 162 } 163 164 /** 165 * Gets the default rounded corner radius of a display which is determined by the 166 * given display unique id. 167 * 168 * Loads the default dimen{@link R.dimen#rounded_corner_radius} if 169 * {@link R.array#config_displayUniqueIdArray} is not set. 170 * 171 * @hide 172 */ getRoundedCornerRadius(Resources res, String displayUniqueId)173 public static int getRoundedCornerRadius(Resources res, String displayUniqueId) { 174 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 175 final TypedArray array = res.obtainTypedArray(R.array.config_roundedCornerRadiusArray); 176 int radius; 177 if (index >= 0 && index < array.length()) { 178 radius = array.getDimensionPixelSize(index, 0); 179 } else { 180 radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius); 181 } 182 array.recycle(); 183 return radius; 184 } 185 186 /** 187 * Gets the top rounded corner radius of a display which is determined by the 188 * given display unique id. 189 * 190 * Loads the default dimen{@link R.dimen#rounded_corner_radius_top} if 191 * {@link R.array#config_displayUniqueIdArray} is not set. 192 * 193 * @hide 194 */ getRoundedCornerTopRadius(Resources res, String displayUniqueId)195 public static int getRoundedCornerTopRadius(Resources res, String displayUniqueId) { 196 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 197 final TypedArray array = res.obtainTypedArray(R.array.config_roundedCornerTopRadiusArray); 198 int radius; 199 if (index >= 0 && index < array.length()) { 200 radius = array.getDimensionPixelSize(index, 0); 201 } else { 202 radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_top); 203 } 204 array.recycle(); 205 return radius; 206 } 207 208 /** 209 * Gets the bottom rounded corner radius of a display which is determined by the 210 * given display unique id. 211 * 212 * Loads the default dimen{@link R.dimen#rounded_corner_radius_bottom} if 213 * {@link R.array#config_displayUniqueIdArray} is not set. 214 * 215 * @hide 216 */ getRoundedCornerBottomRadius(Resources res, String displayUniqueId)217 public static int getRoundedCornerBottomRadius(Resources res, String displayUniqueId) { 218 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 219 final TypedArray array = res.obtainTypedArray( 220 R.array.config_roundedCornerBottomRadiusArray); 221 int radius; 222 if (index >= 0 && index < array.length()) { 223 radius = array.getDimensionPixelSize(index, 0); 224 } else { 225 radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_bottom); 226 } 227 array.recycle(); 228 return radius; 229 } 230 231 /** 232 * Gets the rounded corner radius adjustment of a display which is determined by the 233 * given display unique id. 234 * 235 * Loads the default dimen{@link R.dimen#rounded_corner_radius_adjustment} if 236 * {@link R.array#config_displayUniqueIdArray} is not set. 237 * 238 * @hide 239 */ getRoundedCornerRadiusAdjustment(Resources res, String displayUniqueId)240 public static int getRoundedCornerRadiusAdjustment(Resources res, String displayUniqueId) { 241 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 242 final TypedArray array = res.obtainTypedArray( 243 R.array.config_roundedCornerRadiusAdjustmentArray); 244 int radius; 245 if (index >= 0 && index < array.length()) { 246 radius = array.getDimensionPixelSize(index, 0); 247 } else { 248 radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_adjustment); 249 } 250 array.recycle(); 251 return radius; 252 } 253 254 /** 255 * Gets the rounded corner top radius adjustment of a display which is determined by the 256 * given display unique id. 257 * 258 * Loads the default dimen{@link R.dimen#rounded_corner_radius_top_adjustment} if 259 * {@link R.array#config_displayUniqueIdArray} is not set. 260 * 261 * @hide 262 */ getRoundedCornerRadiusTopAdjustment(Resources res, String displayUniqueId)263 public static int getRoundedCornerRadiusTopAdjustment(Resources res, String displayUniqueId) { 264 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 265 final TypedArray array = res.obtainTypedArray( 266 R.array.config_roundedCornerTopRadiusAdjustmentArray); 267 int radius; 268 if (index >= 0 && index < array.length()) { 269 radius = array.getDimensionPixelSize(index, 0); 270 } else { 271 radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_top_adjustment); 272 } 273 array.recycle(); 274 return radius; 275 } 276 277 /** 278 * Gets the rounded corner bottom radius adjustment of a display which is determined by the 279 * given display unique id. 280 * 281 * Loads the default dimen{@link R.dimen#rounded_corner_radius_bottom_adjustment} if 282 * {@link R.array#config_displayUniqueIdArray} is not set. 283 * 284 * @hide 285 */ getRoundedCornerRadiusBottomAdjustment( Resources res, String displayUniqueId)286 public static int getRoundedCornerRadiusBottomAdjustment( 287 Resources res, String displayUniqueId) { 288 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 289 final TypedArray array = res.obtainTypedArray( 290 R.array.config_roundedCornerBottomRadiusAdjustmentArray); 291 int radius; 292 if (index >= 0 && index < array.length()) { 293 radius = array.getDimensionPixelSize(index, 0); 294 } else { 295 radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_bottom_adjustment); 296 } 297 array.recycle(); 298 return radius; 299 } 300 301 /** 302 * Gets whether a built-in display is round. 303 * 304 * Loads the default config{@link R.bool#config_mainBuiltInDisplayIsRound} if 305 * {@link R.array#config_displayUniqueIdArray} is not set. 306 * 307 * @hide 308 */ getBuiltInDisplayIsRound(Resources res, String displayUniqueId)309 public static boolean getBuiltInDisplayIsRound(Resources res, String displayUniqueId) { 310 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 311 final TypedArray array = res.obtainTypedArray(R.array.config_builtInDisplayIsRoundArray); 312 boolean isRound; 313 if (index >= 0 && index < array.length()) { 314 isRound = array.getBoolean(index, false); 315 } else { 316 isRound = res.getBoolean(R.bool.config_mainBuiltInDisplayIsRound); 317 } 318 array.recycle(); 319 return isRound; 320 } 321 322 /** 323 * Insets the reference frame of the rounded corners. 324 * 325 * @return a copy of this instance which has been inset 326 */ inset(int insetLeft, int insetTop, int insetRight, int insetBottom)327 public RoundedCorners inset(int insetLeft, int insetTop, int insetRight, int insetBottom) { 328 final RoundedCorner[] roundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH]; 329 for (int i = 0; i < ROUNDED_CORNER_POSITION_LENGTH; i++) { 330 roundedCorners[i] = insetRoundedCorner(i, insetLeft, insetTop, insetRight, insetBottom); 331 } 332 return new RoundedCorners(roundedCorners); 333 } 334 insetRoundedCorner(@osition int position, int insetLeft, int insetTop, int insetRight, int insetBottom)335 private RoundedCorner insetRoundedCorner(@Position int position, int insetLeft, 336 int insetTop, int insetRight, int insetBottom) { 337 if (mRoundedCorners[position].isEmpty()) { 338 return new RoundedCorner(position); 339 } 340 341 final int radius = mRoundedCorners[position].getRadius(); 342 final Point center = mRoundedCorners[position].getCenter(); 343 boolean hasRoundedCorner; 344 switch (position) { 345 case POSITION_TOP_LEFT: 346 hasRoundedCorner = radius > insetTop && radius > insetLeft; 347 break; 348 case POSITION_TOP_RIGHT: 349 hasRoundedCorner = radius > insetTop && radius > insetRight; 350 break; 351 case POSITION_BOTTOM_RIGHT: 352 hasRoundedCorner = radius > insetBottom && radius > insetRight; 353 break; 354 case POSITION_BOTTOM_LEFT: 355 hasRoundedCorner = radius > insetBottom && radius > insetLeft; 356 break; 357 default: 358 throw new IllegalArgumentException( 359 "The position is not one of the RoundedCornerPosition =" + position); 360 } 361 return new RoundedCorner( 362 position, radius, 363 hasRoundedCorner ? center.x - insetLeft : 0, 364 hasRoundedCorner ? center.y - insetTop : 0); 365 } 366 367 /** 368 * Returns the {@link RoundedCorner} of the given position if there is one. 369 * 370 * @param position the position of the rounded corner on the display. 371 * @return the rounded corner of the given position. Returns {@code null} if 372 * {@link RoundedCorner#isEmpty()} is {@code true}. 373 */ 374 @Nullable getRoundedCorner(@osition int position)375 public RoundedCorner getRoundedCorner(@Position int position) { 376 return mRoundedCorners[position].isEmpty() 377 ? null : new RoundedCorner(mRoundedCorners[position]); 378 } 379 380 /** 381 * Sets the rounded corner of given position. 382 * 383 * @param position the position of this rounded corner 384 * @param roundedCorner the rounded corner or null if there is none 385 */ setRoundedCorner(@osition int position, @Nullable RoundedCorner roundedCorner)386 public void setRoundedCorner(@Position int position, @Nullable RoundedCorner roundedCorner) { 387 mRoundedCorners[position] = roundedCorner == null 388 ? new RoundedCorner(position) : roundedCorner; 389 } 390 391 /** 392 * Returns an array of {@link RoundedCorner}s. Ordinal value of RoundedCornerPosition is used 393 * as an index of the array. 394 * 395 * @return an array of {@link RoundedCorner}s, one for each rounded corner area. 396 */ getAllRoundedCorners()397 public RoundedCorner[] getAllRoundedCorners() { 398 RoundedCorner[] roundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH]; 399 for (int i = 0; i < ROUNDED_CORNER_POSITION_LENGTH; ++i) { 400 roundedCorners[i] = new RoundedCorner(roundedCorners[i]); 401 } 402 return roundedCorners; 403 } 404 405 /** 406 * Returns a scaled RoundedCorners. 407 */ scale(float scale)408 public RoundedCorners scale(float scale) { 409 if (scale == 1f) { 410 return this; 411 } 412 413 RoundedCorner[] roundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH]; 414 for (int i = 0; i < ROUNDED_CORNER_POSITION_LENGTH; ++i) { 415 final RoundedCorner roundedCorner = mRoundedCorners[i]; 416 roundedCorners[i] = new RoundedCorner( 417 i, 418 (int) (roundedCorner.getRadius() * scale), 419 (int) (roundedCorner.getCenter().x * scale), 420 (int) (roundedCorner.getCenter().y * scale)); 421 } 422 return new RoundedCorners(roundedCorners); 423 } 424 425 /** 426 * Returns a rotated RoundedCorners. 427 */ rotate(@urface.Rotation int rotation, int initialDisplayWidth, int initialDisplayHeight)428 public RoundedCorners rotate(@Surface.Rotation int rotation, int initialDisplayWidth, 429 int initialDisplayHeight) { 430 if (rotation == ROTATION_0) { 431 return this; 432 } 433 final boolean isSizeFlipped = rotation == ROTATION_90 || rotation == ROTATION_270; 434 RoundedCorner[] newCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH]; 435 int newPosistion; 436 for (int i = 0; i < mRoundedCorners.length; i++) { 437 newPosistion = getRotatedIndex(i, rotation); 438 newCorners[newPosistion] = createRoundedCorner( 439 newPosistion, 440 mRoundedCorners[i].getRadius(), 441 isSizeFlipped ? initialDisplayHeight : initialDisplayWidth, 442 isSizeFlipped ? initialDisplayWidth : initialDisplayHeight); 443 } 444 return new RoundedCorners(newCorners); 445 } 446 createRoundedCorner(@osition int position, int radius, int displayWidth, int displayHeight)447 private static RoundedCorner createRoundedCorner(@Position int position, 448 int radius, int displayWidth, int displayHeight) { 449 switch (position) { 450 case POSITION_TOP_LEFT: 451 return new RoundedCorner( 452 POSITION_TOP_LEFT, 453 radius, 454 radius > 0 ? radius : 0, 455 radius > 0 ? radius : 0); 456 case POSITION_TOP_RIGHT: 457 return new RoundedCorner( 458 POSITION_TOP_RIGHT, 459 radius, 460 radius > 0 ? displayWidth - radius : 0, 461 radius > 0 ? radius : 0); 462 case POSITION_BOTTOM_RIGHT: 463 return new RoundedCorner( 464 POSITION_BOTTOM_RIGHT, 465 radius, 466 radius > 0 ? displayWidth - radius : 0, 467 radius > 0 ? displayHeight - radius : 0); 468 case POSITION_BOTTOM_LEFT: 469 return new RoundedCorner( 470 POSITION_BOTTOM_LEFT, 471 radius, 472 radius > 0 ? radius : 0, 473 radius > 0 ? displayHeight - radius : 0); 474 default: 475 throw new IllegalArgumentException( 476 "The position is not one of the RoundedCornerPosition =" + position); 477 } 478 } 479 getRotatedIndex(int position, int rotation)480 private static int getRotatedIndex(int position, int rotation) { 481 return (position - rotation + ROUNDED_CORNER_POSITION_LENGTH) % 4; 482 } 483 484 @Override hashCode()485 public int hashCode() { 486 int result = 0; 487 for (RoundedCorner roundedCorner : mRoundedCorners) { 488 result = result * 31 + roundedCorner.hashCode(); 489 } 490 return result; 491 } 492 493 @Override equals(Object o)494 public boolean equals(Object o) { 495 if (o == this) { 496 return true; 497 } 498 if (o instanceof RoundedCorners) { 499 RoundedCorners r = (RoundedCorners) o; 500 return Arrays.deepEquals(mRoundedCorners, r.mRoundedCorners); 501 } 502 return false; 503 } 504 505 @Override toString()506 public String toString() { 507 return "RoundedCorners{" + Arrays.toString(mRoundedCorners) + "}"; 508 } 509 510 @Override describeContents()511 public int describeContents() { 512 return 0; 513 } 514 515 @Override writeToParcel(Parcel dest, int flags)516 public void writeToParcel(Parcel dest, int flags) { 517 if (equals(NO_ROUNDED_CORNERS)) { 518 dest.writeInt(0); 519 } else { 520 dest.writeInt(1); 521 dest.writeTypedArray(mRoundedCorners, flags); 522 } 523 } 524 525 public static final @NonNull Creator<RoundedCorners> CREATOR = new Creator<RoundedCorners>() { 526 @Override 527 public RoundedCorners createFromParcel(Parcel in) { 528 int variant = in.readInt(); 529 if (variant == 0) { 530 return NO_ROUNDED_CORNERS; 531 } 532 RoundedCorner[] roundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH]; 533 in.readTypedArray(roundedCorners, RoundedCorner.CREATOR); 534 return new RoundedCorners(roundedCorners); 535 } 536 537 @Override 538 public RoundedCorners[] newArray(int size) { 539 return new RoundedCorners[size]; 540 } 541 }; 542 } 543