1 /* 2 * Copyright 2025 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.insets; 18 19 import android.animation.ValueAnimator; 20 import android.graphics.drawable.Drawable; 21 import android.view.animation.Interpolator; 22 import android.view.animation.PathInterpolator; 23 24 import androidx.annotation.FloatRange; 25 import androidx.core.graphics.Insets; 26 import androidx.core.view.WindowInsetsCompat; 27 import androidx.core.view.WindowInsetsCompat.Side.InsetsSide; 28 29 import org.jspecify.annotations.NonNull; 30 import org.jspecify.annotations.Nullable; 31 32 /** 33 * An abstract class which describes a layer to be placed on the content of a window and underneath 34 * the system bar (which has a transparent background) to ensure the readability of the foreground 35 * elements (e.g., text, icons, ..., etc) of the system bar. 36 * 37 * <p>Concrete derived classes would describe how the protection should be drawn by supplying the 38 * {@link Drawable}. 39 * 40 * <p>The object of this class is stateful, and can only be used by one {@link ProtectionLayout} at 41 * a time. 42 * 43 * @see ColorProtection 44 * @see GradientProtection 45 * @see ProtectionLayout 46 */ 47 public abstract class Protection { 48 49 private static final Interpolator DEFAULT_INTERPOLATOR_MOVE_IN = 50 new PathInterpolator(0f, 0f, 0f, 1f); 51 private static final Interpolator DEFAULT_INTERPOLATOR_MOVE_OUT = 52 new PathInterpolator(0.6f, 0f, 1f, 1f); 53 private static final Interpolator DEFAULT_INTERPOLATOR_FADE_IN = 54 new PathInterpolator(0f, 0f, 0.2f, 1f); 55 private static final Interpolator DEFAULT_INTERPOLATOR_FADE_OUT = 56 new PathInterpolator(0.4f, 0f, 1f, 1f); 57 58 // In milliseconds 59 private static final long DEFAULT_DURATION_IN = 333; 60 private static final long DEFAULT_DURATION_OUT = 166; 61 62 @InsetsSide 63 private final int mSide; 64 65 private final Attributes mAttributes = new Attributes(); 66 private Insets mInsets = Insets.NONE; 67 private Insets mInsetsIgnoringVisibility = Insets.NONE; 68 private float mSystemAlpha = 1f; 69 private float mUserAlpha = 1f; 70 private float mSystemInsetAmount = 1f; 71 private float mUserInsetAmount = 1f; 72 private Object mController = null; 73 74 // These animators are driven by the explicit calls to {@link #animateAlpha()} or 75 // {@link #animateInsetsAmount()}, not by the system bar animation. 76 private ValueAnimator mUserAlphaAnimator = null; 77 private ValueAnimator mUserInsetAmountAnimator = null; 78 79 /** 80 * Creates an instance associated with a {@link WindowInsetsCompat.Side}. 81 * 82 * @param side the given {@link WindowInsetsCompat.Side}. 83 * @throws IllegalArgumentException if the given side is not one of the four sides. 84 */ Protection(@nsetsSide int side)85 public Protection(@InsetsSide int side) { 86 switch (side) { 87 case WindowInsetsCompat.Side.LEFT: 88 case WindowInsetsCompat.Side.TOP: 89 case WindowInsetsCompat.Side.RIGHT: 90 case WindowInsetsCompat.Side.BOTTOM: 91 break; 92 default: 93 throw new IllegalArgumentException("Unexpected side: " + side); 94 } 95 mSide = side; 96 } 97 98 /** 99 * Gets the side of this protection. 100 * 101 * @return the side this protection is associated with. 102 */ 103 @InsetsSide getSide()104 public int getSide() { 105 return mSide; 106 } 107 108 /** 109 * Gets the attributes of this protection. 110 * 111 * @return the attributes this protection is associated with. 112 */ 113 @NonNull getAttributes()114 Attributes getAttributes() { 115 return mAttributes; 116 } 117 118 /** 119 * Returns the expected thickness of the protection. 120 * 121 * <p>Derived classes can override this class to specify a different thickness. 122 * 123 * @param inset the actual thickness of the intersection between this window and system bars at 124 * the {@link #mSide}. 125 * @return the expected thickness of the side. 126 */ getThickness(int inset)127 int getThickness(int inset) { 128 return inset; 129 } 130 131 /** 132 * Indicates if this protection excludes adjacent protections with lower z-orders from drawing 133 * into the sharing corners. 134 * 135 * <p>Derived classes can override this class to specify whether to occupy the adjacent corners. 136 * <p>If this returns {@code true}, the protection will 137 * <ol> 138 * <li> get a higher z-order than the ones which returns {@code false}, and 139 * <li> prevent adjacent protections with lower z-orders from drawing into the sharing corners. 140 * </ol> 141 * If this returns {@code false}, the protection can still draw into a sharing corner if no one 142 * occupies it. 143 * 144 * @return {@code true}, if the protection occupies the corners; {@code false}, otherwise. 145 */ occupiesCorners()146 boolean occupiesCorners() { 147 return false; 148 } 149 dispatchInsets(Insets insets, Insets insetsIgnoringVisibility, Insets consumed)150 Insets dispatchInsets(Insets insets, Insets insetsIgnoringVisibility, Insets consumed) { 151 mInsets = insets; 152 mInsetsIgnoringVisibility = insetsIgnoringVisibility; 153 mAttributes.setMargin(consumed); 154 return updateLayout(); 155 } 156 updateLayout()157 @NonNull Insets updateLayout() { 158 Insets consumed = Insets.NONE; 159 final int inset; 160 switch (mSide) { 161 case WindowInsetsCompat.Side.LEFT: 162 inset = mInsets.left; 163 mAttributes.setWidth(getThickness(mInsetsIgnoringVisibility.left)); 164 if (occupiesCorners()) { 165 consumed = Insets.of(getThickness(inset), 0, 0, 0); 166 } 167 break; 168 case WindowInsetsCompat.Side.TOP: 169 inset = mInsets.top; 170 mAttributes.setHeight(getThickness(mInsetsIgnoringVisibility.top)); 171 if (occupiesCorners()) { 172 consumed = Insets.of(0, getThickness(inset), 0, 0); 173 } 174 break; 175 case WindowInsetsCompat.Side.RIGHT: 176 inset = mInsets.right; 177 mAttributes.setWidth(getThickness(mInsetsIgnoringVisibility.right)); 178 if (occupiesCorners()) { 179 consumed = Insets.of(0, 0, getThickness(inset), 0); 180 } 181 break; 182 case WindowInsetsCompat.Side.BOTTOM: 183 inset = mInsets.bottom; 184 mAttributes.setHeight(getThickness(mInsetsIgnoringVisibility.bottom)); 185 if (occupiesCorners()) { 186 consumed = Insets.of(0, 0, 0, getThickness(inset)); 187 } 188 break; 189 default: 190 inset = 0; 191 } 192 setSystemVisible(inset > 0); 193 setSystemAlpha(inset > 0 ? 1f : 0); 194 setSystemInsetAmount(inset > 0 ? 1f : 0); 195 return consumed; 196 } 197 dispatchColorHint(int color)198 void dispatchColorHint(int color) { 199 } 200 getController()201 Object getController() { 202 return mController; 203 } 204 setController(Object controller)205 void setController(Object controller) { 206 mController = controller; 207 } 208 setSystemVisible(boolean visible)209 void setSystemVisible(boolean visible) { 210 mAttributes.setVisible(visible); 211 } 212 setSystemAlpha(@loatRangefrom = 0.0, to = 1.0) float alpha)213 void setSystemAlpha(@FloatRange(from = 0.0, to = 1.0) float alpha) { 214 mSystemAlpha = alpha; 215 updateAlpha(); 216 } 217 218 /** 219 * Sets the opacity of the protection to a value from 0 to 1, where 0 means the protection is 220 * completely transparent and 1 means the protection is completely opaque. 221 * 222 * @param alpha The opacity of the protection. 223 * @throws IllegalArgumentException if the given alpha is not in a range of [0, 1]. 224 */ setAlpha(@loatRangefrom = 0.0, to = 1.0) float alpha)225 public void setAlpha(@FloatRange(from = 0.0, to = 1.0) float alpha) { 226 if (alpha < 0 || alpha > 1f) { 227 throw new IllegalArgumentException("Alpha must in a range of [0, 1]. Got: " + alpha); 228 } 229 cancelUserAlphaAnimation(); 230 setAlphaInternal(alpha); 231 } 232 233 // Sets the alpha without cancelling the user animation. setAlphaInternal(float alpha)234 private void setAlphaInternal(float alpha) { 235 mUserAlpha = alpha; 236 updateAlpha(); 237 } 238 239 /** 240 * Gets the opacity of the protection. This is a value from 0 to 1, where 0 means the protection 241 * is completely transparent and 1 means the protection is completely opaque. 242 * 243 * @return The opacity of the protection. 244 */ 245 @FloatRange(from = 0.0, to = 1.0) getAlpha()246 public float getAlpha() { 247 return mUserAlpha; 248 } 249 updateAlpha()250 private void updateAlpha() { 251 mAttributes.setAlpha(mSystemAlpha * mUserAlpha); 252 } 253 cancelUserAlphaAnimation()254 private void cancelUserAlphaAnimation() { 255 if (mUserAlphaAnimator != null) { 256 mUserAlphaAnimator.cancel(); 257 mUserAlphaAnimator = null; 258 } 259 } 260 261 /** 262 * Animates the alpha from the current value to the specified one. 263 * 264 * <p>Calling {@link #setAlpha(float)} during the animation will cancel the existing alpha 265 * animation. 266 * 267 * @param toAlpha The alpha that will be animated to. The range is [0, 1]. 268 */ animateAlpha(float toAlpha)269 public void animateAlpha(float toAlpha) { 270 cancelUserAlphaAnimation(); 271 if (toAlpha == mUserAlpha) { 272 return; 273 } 274 mUserAlphaAnimator = ValueAnimator.ofFloat(mUserAlpha, toAlpha); 275 if (mUserAlpha < toAlpha) { 276 mUserAlphaAnimator.setDuration(DEFAULT_DURATION_IN); 277 mUserAlphaAnimator.setInterpolator(DEFAULT_INTERPOLATOR_FADE_IN); 278 } else { 279 mUserAlphaAnimator.setDuration(DEFAULT_DURATION_OUT); 280 mUserAlphaAnimator.setInterpolator(DEFAULT_INTERPOLATOR_FADE_OUT); 281 } 282 mUserAlphaAnimator.addUpdateListener( 283 animation -> setAlphaInternal((float) animation.getAnimatedValue())); 284 mUserAlphaAnimator.start(); 285 } 286 setSystemInsetAmount(@loatRangefrom = 0.0, to = 1.0) float insetAmount)287 void setSystemInsetAmount(@FloatRange(from = 0.0, to = 1.0) float insetAmount) { 288 mSystemInsetAmount = insetAmount; 289 updateInsetAmount(); 290 } 291 292 /** 293 * Sets the depth of the protection to a value from 0 to 1, where 0 means the protection is 294 * completely outside the window and 1 means the protection is completely inside the window. 295 * 296 * @param insetAmount The depth of the protection. 297 * @throws IllegalArgumentException if the given inset amount is not in a range of [0, 1]. 298 */ setInsetAmount(@loatRangefrom = 0.0, to = 1.0) float insetAmount)299 public void setInsetAmount(@FloatRange(from = 0.0, to = 1.0) float insetAmount) { 300 if (insetAmount < 0 || insetAmount > 1f) { 301 throw new IllegalArgumentException("Inset amount must in a range of [0, 1]. Got: " 302 + insetAmount); 303 } 304 cancelUserInsetsAmountAnimation(); 305 setInsetAmountInternal(insetAmount); 306 } 307 308 // Sets the insets amount without cancelling the user animation. setInsetAmountInternal(float insetAmount)309 private void setInsetAmountInternal(float insetAmount) { 310 mUserInsetAmount = insetAmount; 311 updateInsetAmount(); 312 } 313 314 /** 315 * Gets the depth of the protection. This is a value from 0 to 1, where 0 means the protection 316 * completely inside the window and 1 means the protection is completely outside the window. 317 * 318 * @return The depth of the protection. 319 */ getInsetAmount()320 public float getInsetAmount() { 321 return mUserInsetAmount; 322 } 323 updateInsetAmount()324 private void updateInsetAmount() { 325 final float finalInsetAmount = mUserInsetAmount * mSystemInsetAmount; 326 switch (mSide) { 327 case WindowInsetsCompat.Side.LEFT: 328 mAttributes.setTranslationX(-(1f - finalInsetAmount) * mAttributes.mWidth); 329 break; 330 case WindowInsetsCompat.Side.TOP: 331 mAttributes.setTranslationY(-(1f - finalInsetAmount) * mAttributes.mHeight); 332 break; 333 case WindowInsetsCompat.Side.RIGHT: 334 mAttributes.setTranslationX((1f - finalInsetAmount) * mAttributes.mWidth); 335 break; 336 case WindowInsetsCompat.Side.BOTTOM: 337 mAttributes.setTranslationY((1f - finalInsetAmount) * mAttributes.mHeight); 338 break; 339 } 340 } 341 cancelUserInsetsAmountAnimation()342 private void cancelUserInsetsAmountAnimation() { 343 if (mUserInsetAmountAnimator != null) { 344 mUserInsetAmountAnimator.cancel(); 345 mUserInsetAmountAnimator = null; 346 } 347 } 348 349 /** 350 * Animates the insets amount from the current value to the specified one. 351 * 352 * <p>Calling {@link #setInsetAmount(float)} during the animation will cancel the existing 353 * animation of insets amount. 354 * 355 * @param toInsetsAmount The insets amount that will be animated to. The range is [0, 1]. 356 */ animateInsetsAmount(float toInsetsAmount)357 public void animateInsetsAmount(float toInsetsAmount) { 358 cancelUserInsetsAmountAnimation(); 359 if (toInsetsAmount == mUserInsetAmount) { 360 return; 361 } 362 mUserInsetAmountAnimator = ValueAnimator.ofFloat(mUserInsetAmount, toInsetsAmount); 363 if (mUserInsetAmount < toInsetsAmount) { 364 mUserInsetAmountAnimator.setDuration(DEFAULT_DURATION_IN); 365 mUserInsetAmountAnimator.setInterpolator(DEFAULT_INTERPOLATOR_MOVE_IN); 366 } else { 367 mUserInsetAmountAnimator.setDuration(DEFAULT_DURATION_OUT); 368 mUserInsetAmountAnimator.setInterpolator(DEFAULT_INTERPOLATOR_MOVE_OUT); 369 } 370 mUserInsetAmountAnimator.addUpdateListener( 371 animation -> setAlphaInternal((float) animation.getAnimatedValue())); 372 mUserInsetAmountAnimator.start(); 373 } 374 setDrawable(@ullable Drawable drawable)375 void setDrawable(@Nullable Drawable drawable) { 376 mAttributes.setDrawable(drawable); 377 } 378 379 /** 380 * Describes the final appearance of the protection. 381 */ 382 static class Attributes { 383 384 private static final int UNSPECIFIED = -1; 385 386 private int mWidth = UNSPECIFIED; 387 private int mHeight = UNSPECIFIED; 388 private Insets mMargin = Insets.NONE; 389 private boolean mVisible = false; 390 private Drawable mDrawable = null; 391 private float mTranslationX = 0; 392 private float mTranslationY = 0; 393 private float mAlpha = 1f; 394 private @Nullable Callback mCallback; 395 396 /** 397 * Returns the width of the protection in pixels. 398 * 399 * @return the width of the protection in pixels. 400 */ getWidth()401 int getWidth() { 402 return mWidth; 403 } 404 405 /** 406 * Returns the height of the protection in pixels. 407 * 408 * @return the height of the protection in pixels. 409 */ getHeight()410 int getHeight() { 411 return mHeight; 412 } 413 414 /** 415 * Returns the margin of the protection in pixels. 416 * 417 * @return the margin of the protection in pixels. 418 */ getMargin()419 @NonNull Insets getMargin() { 420 return mMargin; 421 } 422 423 /** 424 * Returns {@code true} if the protection is visible. {@code false} otherwise. 425 * 426 * @return {@code true} if the protection is visible. {@code false} otherwise. 427 */ isVisible()428 boolean isVisible() { 429 return mVisible; 430 } 431 432 /** 433 * Returns the {@link Drawable} that fills the protection. 434 * 435 * @return the {@link Drawable} that fills the protection. 436 */ getDrawable()437 @Nullable Drawable getDrawable() { 438 return mDrawable; 439 } 440 441 /** 442 * Returns the translation of the protection along the x-axis. 443 * 444 * @return the translation of the protection along the x-axis. 445 */ getTranslationX()446 float getTranslationX() { 447 return mTranslationX; 448 } 449 450 /** 451 * Returns the translation of the protection along the y-axis in pixels. 452 * 453 * @return the translation of the protection along the y-axis in pixels. 454 */ getTranslationY()455 float getTranslationY() { 456 return mTranslationY; 457 } 458 459 /** 460 * Returns the transparency of the protection. 461 * 462 * @return the transparency of the protection. 463 */ getAlpha()464 float getAlpha() { 465 return mAlpha; 466 } 467 setWidth(int width)468 private void setWidth(int width) { 469 if (mWidth != width) { 470 mWidth = width; 471 if (mCallback != null) { 472 mCallback.onWidthChanged(width); 473 } 474 } 475 } 476 setHeight(int height)477 private void setHeight(int height) { 478 if (mHeight != height) { 479 mHeight = height; 480 if (mCallback != null) { 481 mCallback.onHeightChanged(height); 482 } 483 } 484 } 485 setMargin(Insets margin)486 private void setMargin(Insets margin) { 487 if (!mMargin.equals(margin)) { 488 mMargin = margin; 489 if (mCallback != null) { 490 mCallback.onMarginChanged(margin); 491 } 492 } 493 } 494 setVisible(boolean visible)495 private void setVisible(boolean visible) { 496 if (mVisible != visible) { 497 mVisible = visible; 498 if (mCallback != null) { 499 mCallback.onVisibilityChanged(visible); 500 } 501 } 502 } 503 setDrawable(@ullable Drawable drawable)504 private void setDrawable(@Nullable Drawable drawable) { 505 mDrawable = drawable; 506 if (mCallback != null) { 507 mCallback.onDrawableChanged(drawable); 508 } 509 } 510 setTranslationX(float translationX)511 private void setTranslationX(float translationX) { 512 if (mTranslationX != translationX) { 513 mTranslationX = translationX; 514 if (mCallback != null) { 515 mCallback.onTranslationXChanged(translationX); 516 } 517 } 518 } 519 setTranslationY(float translationY)520 private void setTranslationY(float translationY) { 521 if (mTranslationY != translationY) { 522 mTranslationY = translationY; 523 if (mCallback != null) { 524 mCallback.onTranslationYChanged(translationY); 525 } 526 } 527 } 528 setAlpha(float alpha)529 private void setAlpha(float alpha) { 530 if (mAlpha != alpha) { 531 mAlpha = alpha; 532 if (mCallback != null) { 533 mCallback.onAlphaChanged(alpha); 534 } 535 } 536 } 537 538 /** 539 * Callbacks for monitoring the attribute change. 540 */ 541 interface Callback { 542 543 /** Called when the width is changed */ onWidthChanged(int width)544 default void onWidthChanged(int width) {} 545 546 /** Called when the height is changed */ onHeightChanged(int height)547 default void onHeightChanged(int height) {} 548 549 /** Called when the margin is changed */ onMarginChanged(@onNull Insets margin)550 default void onMarginChanged(@NonNull Insets margin) {} 551 552 /** Called when the visibility is changed */ onVisibilityChanged(boolean visible)553 default void onVisibilityChanged(boolean visible) {} 554 555 /** Called when the drawable is changed */ onDrawableChanged(@onNull Drawable drawable)556 default void onDrawableChanged(@NonNull Drawable drawable) {} 557 558 /** Called when the translation along the x-axis is changed */ onTranslationXChanged(float translationX)559 default void onTranslationXChanged(float translationX) {} 560 561 /** Called when the translation along the y-axis is changed */ onTranslationYChanged(float translationY)562 default void onTranslationYChanged(float translationY) {} 563 564 /** Called when the transparency is changed */ onAlphaChanged(float alpha)565 default void onAlphaChanged(float alpha) {} 566 } 567 568 /** 569 * Sets a {@link Callback} to monitor attribute change. 570 * 571 * @param callback the given callback. 572 */ setCallback(@ullable Callback callback)573 void setCallback(@Nullable Callback callback) { 574 if (mCallback != null && callback != null) { 575 throw new IllegalStateException("Trying to overwrite the existing callback." 576 + " Did you send one protection to multiple ProtectionLayouts?"); 577 } 578 mCallback = callback; 579 } 580 } 581 } 582