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