1 /* 2 * Copyright 2018 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 androidx.core.view; 18 19 import static android.os.Build.VERSION.SDK_INT; 20 21 import android.graphics.Path; 22 import android.graphics.Rect; 23 import android.os.Build; 24 import android.view.DisplayCutout; 25 26 import androidx.annotation.RequiresApi; 27 import androidx.core.graphics.Insets; 28 import androidx.core.util.ObjectsCompat; 29 30 import org.jspecify.annotations.NonNull; 31 import org.jspecify.annotations.Nullable; 32 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.List; 36 37 /** 38 * Represents the area of the display that is not functional for displaying content. 39 * 40 * <p>{@code DisplayCutoutCompat} instances are immutable. 41 */ 42 public final class DisplayCutoutCompat { 43 44 private final DisplayCutout mDisplayCutout; 45 46 /** 47 * Creates a DisplayCutout instance. 48 * 49 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 50 * {@link #getSafeInsetTop()} etc. 51 * @param boundingRects the bounding rects of the display cutouts as returned by 52 * {@link #getBoundingRects()} ()}. 53 */ DisplayCutoutCompat(@ullable Rect safeInsets, @Nullable List<Rect> boundingRects)54 public DisplayCutoutCompat(@Nullable Rect safeInsets, @Nullable List<Rect> boundingRects) { 55 this(SDK_INT >= 28 ? Api28Impl.createDisplayCutout(safeInsets, boundingRects) : null); 56 } 57 58 /** 59 * Creates a DisplayCutout instance. 60 * 61 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 62 * {@link #getSafeInsetTop()} etc. 63 * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, 64 * it's treated as an empty rectangle (0,0)-(0,0). 65 * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, 66 * it's treated as an empty rectangle (0,0)-(0,0). 67 * @param boundRight the right bounding rect of the display cutout in pixels. If null is 68 * passed, it's treated as an empty rectangle (0,0)-(0,0). 69 * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is 70 * passed, it's treated as an empty rectangle (0,0)-(0,0). 71 * @param waterfallInsets the insets for the curved areas in waterfall display. 72 */ DisplayCutoutCompat(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets)73 public DisplayCutoutCompat(@NonNull Insets safeInsets, @Nullable Rect boundLeft, 74 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, 75 @NonNull Insets waterfallInsets) { 76 this(constructDisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom, 77 waterfallInsets, null)); 78 } 79 80 /** 81 * Creates a DisplayCutout instance. 82 * 83 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 84 * {@link #getSafeInsetTop()} etc. 85 * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, 86 * it's treated as an empty rectangle (0,0)-(0,0). 87 * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, 88 * it's treated as an empty rectangle (0,0)-(0,0). 89 * @param boundRight the right bounding rect of the display cutout in pixels. If null is 90 * passed, it's treated as an empty rectangle (0,0)-(0,0). 91 * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is 92 * passed, it's treated as an empty rectangle (0,0)-(0,0). 93 * @param waterfallInsets the insets for the curved areas in waterfall display. 94 * @param cutoutPath the path of the display cutout. Specifying a path with this 95 * constructor is only supported on API 33 and above, even though a real 96 * DisplayCutout can have a cutout path on API 31 and above. On API 32 and 97 * below, this path is ignored. 98 */ DisplayCutoutCompat(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets, @Nullable Path cutoutPath)99 public DisplayCutoutCompat(@NonNull Insets safeInsets, @Nullable Rect boundLeft, 100 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, 101 @NonNull Insets waterfallInsets, @Nullable Path cutoutPath) { 102 this(constructDisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom, 103 waterfallInsets, cutoutPath)); 104 } 105 constructDisplayCutout(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets, @Nullable Path cutoutPath)106 private static DisplayCutout constructDisplayCutout(@NonNull Insets safeInsets, 107 @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, 108 @Nullable Rect boundBottom, @NonNull Insets waterfallInsets, 109 @Nullable Path cutoutPath) { 110 if (SDK_INT >= 33) { 111 return Api33Impl.createDisplayCutout(safeInsets.toPlatformInsets(), boundLeft, boundTop, 112 boundRight, boundBottom, waterfallInsets.toPlatformInsets(), cutoutPath); 113 } else if (SDK_INT >= 30) { 114 return Api30Impl.createDisplayCutout(safeInsets.toPlatformInsets(), boundLeft, boundTop, 115 boundRight, boundBottom, waterfallInsets.toPlatformInsets()); 116 } else if (SDK_INT >= Build.VERSION_CODES.Q) { 117 return Api29Impl.createDisplayCutout(safeInsets.toPlatformInsets(), boundLeft, boundTop, 118 boundRight, boundBottom); 119 } else if (SDK_INT >= Build.VERSION_CODES.P) { 120 final Rect safeInsetRect = new Rect(safeInsets.left, safeInsets.top, safeInsets.right, 121 safeInsets.bottom); 122 final ArrayList<Rect> boundingRects = new ArrayList<>(); 123 if (boundLeft != null) { 124 boundingRects.add(boundLeft); 125 } 126 if (boundTop != null) { 127 boundingRects.add(boundTop); 128 } 129 if (boundRight != null) { 130 boundingRects.add(boundRight); 131 } 132 if (boundBottom != null) { 133 boundingRects.add(boundBottom); 134 } 135 return Api28Impl.createDisplayCutout(safeInsetRect, boundingRects); 136 } else { 137 return null; 138 } 139 } 140 DisplayCutoutCompat(DisplayCutout displayCutout)141 private DisplayCutoutCompat(DisplayCutout displayCutout) { 142 mDisplayCutout = displayCutout; 143 } 144 145 /** Returns the inset from the top which avoids the display cutout in pixels. */ getSafeInsetTop()146 public int getSafeInsetTop() { 147 if (SDK_INT >= 28) { 148 return Api28Impl.getSafeInsetTop(mDisplayCutout); 149 } else { 150 return 0; 151 } 152 } 153 154 /** Returns the inset from the bottom which avoids the display cutout in pixels. */ getSafeInsetBottom()155 public int getSafeInsetBottom() { 156 if (SDK_INT >= 28) { 157 return Api28Impl.getSafeInsetBottom(mDisplayCutout); 158 } else { 159 return 0; 160 } 161 } 162 163 /** Returns the inset from the left which avoids the display cutout in pixels. */ getSafeInsetLeft()164 public int getSafeInsetLeft() { 165 if (SDK_INT >= 28) { 166 return Api28Impl.getSafeInsetLeft(mDisplayCutout); 167 } else { 168 return 0; 169 } 170 } 171 172 /** Returns the inset from the right which avoids the display cutout in pixels. */ getSafeInsetRight()173 public int getSafeInsetRight() { 174 if (SDK_INT >= 28) { 175 return Api28Impl.getSafeInsetRight(mDisplayCutout); 176 } else { 177 return 0; 178 } 179 } 180 181 /** 182 * Returns a list of {@code Rect}s, each of which is the bounding rectangle for a non-functional 183 * area on the display. 184 * 185 * There will be at most one non-functional area per short edge of the device, and none on 186 * the long edges. 187 * 188 * @return a list of bounding {@code Rect}s, one for each display cutout area. 189 */ getBoundingRects()190 public @NonNull List<Rect> getBoundingRects() { 191 if (SDK_INT >= 28) { 192 return Api28Impl.getBoundingRects(mDisplayCutout); 193 } else { 194 return Collections.emptyList(); 195 } 196 } 197 198 /** 199 * Returns the insets representing the curved areas of a waterfall display. 200 * 201 * A waterfall display has curved areas along the edges of the screen. Apps should be careful 202 * when showing UI and handling touch input in those insets because the curve may impair 203 * legibility and can frequently lead to unintended touch inputs. 204 * 205 * @return the insets for the curved areas of a waterfall display in pixels or {@code 206 * Insets.NONE} if there are no curved areas or they don't overlap with the window. 207 */ getWaterfallInsets()208 public @NonNull Insets getWaterfallInsets() { 209 if (SDK_INT >= 30) { 210 return Insets.toCompatInsets(Api30Impl.getWaterfallInsets(mDisplayCutout)); 211 } else { 212 return Insets.NONE; 213 } 214 } 215 216 /** 217 * Returns a Path that contains the cutout paths of all sides on the display. 218 * To get a cutout path for one specific side, apps can intersect the Path with the Rect 219 * obtained from getBoundingRectLeft(), getBoundingRectTop(), getBoundingRectRight() or 220 * getBoundingRectBottom(). 221 * 222 * @return the path corresponding to the cutout, or null if there is no cutout on the display. 223 */ getCutoutPath()224 public @Nullable Path getCutoutPath() { 225 if (SDK_INT >= 31) { 226 return Api31Impl.getCutoutPath(mDisplayCutout); 227 } else { 228 return null; 229 } 230 } 231 232 @Override equals(Object o)233 public boolean equals(Object o) { 234 if (this == o) { 235 return true; 236 } 237 if (o == null || getClass() != o.getClass()) { 238 return false; 239 } 240 DisplayCutoutCompat other = (DisplayCutoutCompat) o; 241 return ObjectsCompat.equals(mDisplayCutout, other.mDisplayCutout); 242 } 243 244 @Override hashCode()245 public int hashCode() { 246 return mDisplayCutout == null ? 0 : mDisplayCutout.hashCode(); 247 } 248 249 @Override toString()250 public @NonNull String toString() { 251 return "DisplayCutoutCompat{" + mDisplayCutout + "}"; 252 } 253 wrap(DisplayCutout displayCutout)254 static DisplayCutoutCompat wrap(DisplayCutout displayCutout) { 255 return displayCutout == null ? null : new DisplayCutoutCompat(displayCutout); 256 } 257 258 @RequiresApi(28) unwrap()259 DisplayCutout unwrap() { 260 return mDisplayCutout; 261 } 262 263 @RequiresApi(28) 264 static class Api28Impl { Api28Impl()265 private Api28Impl() { 266 // This class is not instantiable. 267 } 268 createDisplayCutout( @ullable Rect safeInsets, @Nullable List<Rect> boundingRects)269 static DisplayCutout createDisplayCutout( 270 @Nullable Rect safeInsets, @Nullable List<Rect> boundingRects) { 271 return new DisplayCutout(safeInsets, boundingRects); 272 } 273 getSafeInsetTop(DisplayCutout displayCutout)274 static int getSafeInsetTop(DisplayCutout displayCutout) { 275 return displayCutout.getSafeInsetTop(); 276 } 277 getSafeInsetBottom(DisplayCutout displayCutout)278 static int getSafeInsetBottom(DisplayCutout displayCutout) { 279 return displayCutout.getSafeInsetBottom(); 280 } 281 getSafeInsetLeft(DisplayCutout displayCutout)282 static int getSafeInsetLeft(DisplayCutout displayCutout) { 283 return displayCutout.getSafeInsetLeft(); 284 } 285 getSafeInsetRight(DisplayCutout displayCutout)286 static int getSafeInsetRight(DisplayCutout displayCutout) { 287 return displayCutout.getSafeInsetRight(); 288 } 289 getBoundingRects(DisplayCutout displayCutout)290 static List<Rect> getBoundingRects(DisplayCutout displayCutout) { 291 return displayCutout.getBoundingRects(); 292 } 293 } 294 295 @RequiresApi(29) 296 static class Api29Impl { Api29Impl()297 private Api29Impl() { 298 // This class is not instantiable. 299 } 300 createDisplayCutout(android.graphics.@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom)301 static DisplayCutout createDisplayCutout(android.graphics.@NonNull Insets safeInsets, 302 @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, 303 @Nullable Rect boundBottom) { 304 return new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom); 305 } 306 } 307 308 @RequiresApi(30) 309 static class Api30Impl { Api30Impl()310 private Api30Impl() { 311 // This class is not instantiable. 312 } 313 createDisplayCutout( android.graphics.@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, android.graphics.@NonNull Insets waterfallInsets)314 static DisplayCutout createDisplayCutout( 315 android.graphics.@NonNull Insets safeInsets, @Nullable Rect boundLeft, 316 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, 317 android.graphics.@NonNull Insets waterfallInsets) { 318 return new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom, 319 waterfallInsets); 320 } 321 getWaterfallInsets(DisplayCutout displayCutout)322 static android.graphics.Insets getWaterfallInsets(DisplayCutout displayCutout) { 323 return displayCutout.getWaterfallInsets(); 324 } 325 } 326 327 @RequiresApi(31) 328 static class Api31Impl { Api31Impl()329 private Api31Impl() { 330 // This class is not instantiable. 331 } 332 getCutoutPath(DisplayCutout displayCutout)333 static @Nullable Path getCutoutPath(DisplayCutout displayCutout) { 334 return displayCutout.getCutoutPath(); 335 } 336 } 337 338 @RequiresApi(33) 339 static class Api33Impl { Api33Impl()340 private Api33Impl() { 341 // This class is not instantiable. 342 } 343 createDisplayCutout( android.graphics.@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, android.graphics.@NonNull Insets waterfallInsets, @Nullable Path cutoutPath)344 static DisplayCutout createDisplayCutout( 345 android.graphics.@NonNull Insets safeInsets, @Nullable Rect boundLeft, 346 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, 347 android.graphics.@NonNull Insets waterfallInsets, @Nullable Path cutoutPath) { 348 DisplayCutout.Builder builder = new DisplayCutout.Builder() 349 .setSafeInsets(safeInsets) 350 .setWaterfallInsets(waterfallInsets); 351 352 if (boundLeft != null) { 353 builder.setBoundingRectLeft(boundLeft); 354 } 355 if (boundTop != null) { 356 builder.setBoundingRectTop(boundTop); 357 } 358 if (boundRight != null) { 359 builder.setBoundingRectRight(boundRight); 360 } 361 if (boundBottom != null) { 362 builder.setBoundingRectBottom(boundBottom); 363 } 364 if (cutoutPath != null) { 365 builder.setCutoutPath(cutoutPath); 366 } 367 return builder.build(); 368 } 369 } 370 } 371