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