1919import android .annotation .SuppressLint ;
2020import android .content .Context ;
2121import android .graphics .Canvas ;
22- import android .graphics .Outline ;
23- import android .graphics .Path ;
24- import android .graphics .Rect ;
2522import android .graphics .RectF ;
26- import android .os .Build .VERSION ;
27- import android .os .Build .VERSION_CODES ;
2823import android .util .AttributeSet ;
2924import android .view .MotionEvent ;
3025import android .view .View ;
31- import android .view .ViewOutlineProvider ;
3226import android .widget .FrameLayout ;
33- import androidx .annotation .DoNotInline ;
3427import androidx .annotation .NonNull ;
3528import androidx .annotation .Nullable ;
36- import androidx .annotation .RequiresApi ;
3729import androidx .annotation .RestrictTo ;
3830import androidx .annotation .RestrictTo .Scope ;
3931import androidx .annotation .VisibleForTesting ;
4032import androidx .core .math .MathUtils ;
4133import com .google .android .material .animation .AnimationUtils ;
42- import com .google .android .material .canvas .CanvasCompat .CanvasOperation ;
4334import com .google .android .material .shape .AbsoluteCornerSize ;
4435import com .google .android .material .shape .ClampedCornerSize ;
4536import com .google .android .material .shape .ShapeAppearanceModel ;
46- import com .google .android .material .shape .ShapeAppearancePathProvider ;
4737import com .google .android .material .shape .Shapeable ;
38+ import com .google .android .material .shape .ShapeableDelegate ;
4839
4940/** A {@link FrameLayout} than is able to mask itself and all children. */
5041public class MaskableFrameLayout extends FrameLayout implements Maskable , Shapeable {
@@ -53,7 +44,7 @@ public class MaskableFrameLayout extends FrameLayout implements Maskable, Shapea
5344 private final RectF maskRect = new RectF ();
5445 @ Nullable private OnMaskChangedListener onMaskChangedListener ;
5546 @ NonNull private ShapeAppearanceModel shapeAppearanceModel ;
56- private final MaskableDelegate maskableDelegate = createMaskableDelegate ( );
47+ private final ShapeableDelegate shapeableDelegate = ShapeableDelegate . create ( this );
5748 @ Nullable private Boolean savedForceCompatClippingEnabled = null ;
5849
5950 public MaskableFrameLayout (@ NonNull Context context ) {
@@ -71,16 +62,6 @@ public MaskableFrameLayout(
7162 ShapeAppearanceModel .builder (context , attrs , defStyleAttr , 0 , 0 ).build ());
7263 }
7364
74- private MaskableDelegate createMaskableDelegate () {
75- if (VERSION .SDK_INT >= VERSION_CODES .TIRAMISU ) {
76- return new MaskableDelegateV33 (this );
77- } else if (VERSION .SDK_INT >= VERSION_CODES .LOLLIPOP_MR1 ) {
78- return new MaskableDelegateV22 (this );
79- } else {
80- return new MaskableDelegateV14 ();
81- }
82- }
83-
8465 @ Override
8566 protected void onSizeChanged (int w , int h , int oldw , int oldh ) {
8667 super .onSizeChanged (w , h , oldw , oldh );
@@ -92,16 +73,16 @@ protected void onAttachedToWindow() {
9273 super .onAttachedToWindow ();
9374 // Restore any saved force compat clipping setting.
9475 if (savedForceCompatClippingEnabled != null ) {
95- maskableDelegate .setForceCompatClippingEnabled (this , savedForceCompatClippingEnabled );
76+ shapeableDelegate .setForceCompatClippingEnabled (this , savedForceCompatClippingEnabled );
9677 }
9778 }
9879
9980 @ Override
10081 protected void onDetachedFromWindow () {
10182 // When detaching from the window, force canvas clipping to avoid any transitions from releasing
10283 // the mask outline set by the MaskableDelegate's ViewOutlineProvider, if any.
103- savedForceCompatClippingEnabled = maskableDelegate .isForceCompatClippingEnabled ();
104- maskableDelegate .setForceCompatClippingEnabled (this , true );
84+ savedForceCompatClippingEnabled = shapeableDelegate .isForceCompatClippingEnabled ();
85+ shapeableDelegate .setForceCompatClippingEnabled (this , true );
10586 super .onDetachedFromWindow ();
10687 }
10788
@@ -120,7 +101,7 @@ public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanc
120101 return cornerSize ;
121102 }
122103 });
123- maskableDelegate .onShapeAppearanceChanged (this , this .shapeAppearanceModel );
104+ shapeableDelegate .onShapeAppearanceChanged (this , this .shapeAppearanceModel );
124105 }
125106
126107 @ NonNull
@@ -173,7 +154,7 @@ private void onMaskChanged() {
173154 // masked away.
174155 float maskWidth = AnimationUtils .lerp (0f , getWidth () / 2F , 0f , 1f , maskXPercentage );
175156 maskRect .set (maskWidth , 0F , (getWidth () - maskWidth ), getHeight ());
176- maskableDelegate .onMaskChanged (this , maskRect );
157+ shapeableDelegate .onMaskChanged (this , maskRect );
177158 if (onMaskChangedListener != null ) {
178159 onMaskChangedListener .onMaskChanged (maskRect );
179160 }
@@ -187,7 +168,7 @@ private void onMaskChanged() {
187168 @ VisibleForTesting
188169 @ RestrictTo (Scope .LIBRARY_GROUP )
189170 public void setForceCompatClipping (boolean forceCompatClipping ) {
190- maskableDelegate .setForceCompatClippingEnabled (this , forceCompatClipping );
171+ shapeableDelegate .setForceCompatClippingEnabled (this , forceCompatClipping );
191172 }
192173
193174 @ SuppressLint ("ClickableViewAccessibility" )
@@ -206,228 +187,6 @@ public boolean onTouchEvent(MotionEvent event) {
206187
207188 @ Override
208189 protected void dispatchDraw (Canvas canvas ) {
209- maskableDelegate .maybeClip (canvas , super ::dispatchDraw );
210- }
211-
212- /**
213- * A delegate able to handle logic for when and how to mask a View based on the View's {@link
214- * ShapeAppearanceModel} and mask bounds.
215- */
216- private abstract static class MaskableDelegate {
217-
218- boolean forceCompatClippingEnabled = false ;
219- @ Nullable ShapeAppearanceModel shapeAppearanceModel ;
220- RectF maskBounds = new RectF ();
221- final Path shapePath = new Path ();
222-
223- /**
224- * Called due to changes in a delegate's shape, mask bounds or other parameters. Delegate
225- * implementations should use this as an opportunity to ensure their method of clipping is
226- * appropriate and invalidate the client view if necessary.
227- *
228- * @param view the client view
229- */
230- abstract void invalidateClippingMethod (View view );
231-
232- /**
233- * Whether the client view should use canvas clipping to mask itself.
234- *
235- * <p>Note: It's important that no significant logic is run in this method as it is called from
236- * dispatch draw, which should be as performant as possible. Logic for determining whether
237- * compat clipping is used should be run elsewhere and stored for quick access.
238- *
239- * @return true if the client view should clip the canvas
240- */
241- abstract boolean shouldUseCompatClipping ();
242-
243- boolean isForceCompatClippingEnabled () {
244- return forceCompatClippingEnabled ;
245- }
246-
247- /**
248- * Set whether the client would like to always use compat clipping regardless of whether other
249- * means are available.
250- *
251- * @param view the client view
252- * @param enabled true if the client should always use canvas clipping
253- */
254- void setForceCompatClippingEnabled (View view , boolean enabled ) {
255- if (enabled != this .forceCompatClippingEnabled ) {
256- this .forceCompatClippingEnabled = enabled ;
257- invalidateClippingMethod (view );
258- }
259- }
260-
261- /**
262- * Called whenever the {@link ShapeAppearanceModel} of the client changes.
263- *
264- * @param view the client view
265- * @param shapeAppearanceModel the update {@link ShapeAppearanceModel}
266- */
267- void onShapeAppearanceChanged (View view , @ NonNull ShapeAppearanceModel shapeAppearanceModel ) {
268- this .shapeAppearanceModel = shapeAppearanceModel ;
269- updateShapePath ();
270- invalidateClippingMethod (view );
271- }
272-
273- /**
274- * Called whenever the bounds of the clients mask changes.
275- *
276- * @param view the client view
277- * @param maskBounds the updated bounds
278- */
279- void onMaskChanged (View view , RectF maskBounds ) {
280- this .maskBounds = maskBounds ;
281- updateShapePath ();
282- invalidateClippingMethod (view );
283- }
284-
285- private void updateShapePath () {
286- if (!maskBounds .isEmpty () && shapeAppearanceModel != null ) {
287- ShapeAppearancePathProvider .getInstance ()
288- .calculatePath (shapeAppearanceModel , 1F , maskBounds , shapePath );
289- }
290- }
291-
292- void maybeClip (Canvas canvas , CanvasOperation op ) {
293- if (shouldUseCompatClipping () && !shapePath .isEmpty ()) {
294- canvas .save ();
295- canvas .clipPath (shapePath );
296- op .run (canvas );
297- canvas .restore ();
298- } else {
299- op .run (canvas );
300- }
301- }
302- }
303-
304- /**
305- * A {@link MaskableDelegate} implementation for API 14-21 that always clips using canvas
306- * clipping.
307- */
308- private static class MaskableDelegateV14 extends MaskableDelegate {
309-
310- @ Override
311- boolean shouldUseCompatClipping () {
312- return true ;
313- }
314-
315- @ Override
316- void invalidateClippingMethod (View view ) {
317- if (shapeAppearanceModel == null || maskBounds .isEmpty ()) {
318- return ;
319- }
320-
321- if (shouldUseCompatClipping ()) {
322- view .invalidate ();
323- }
324- }
325- }
326-
327- /**
328- * A {@link MaskableDelegate} for API 22-32 that uses {@link ViewOutlineProvider} to clip when the
329- * shape being clipped is a round rect with symmetrical corners and canvas clipping for all other
330- * shapes. This way is not used for API 21 because outline invalidation is incorrectly implemented
331- * in this version.
332- *
333- * <p>{@link Outline#setRoundRect(Rect, float)} is only able to clip to a rectangle with a single
334- * corner radius for all four corners.
335- */
336- @ RequiresApi (VERSION_CODES .LOLLIPOP_MR1 )
337- private static class MaskableDelegateV22 extends MaskableDelegate {
338-
339- private boolean isShapeRoundRect = false ;
340-
341- MaskableDelegateV22 (View view ) {
342- initMaskOutlineProvider (view );
343- }
344-
345- @ Override
346- public boolean shouldUseCompatClipping () {
347- return !isShapeRoundRect || forceCompatClippingEnabled ;
348- }
349-
350- @ Override
351- void invalidateClippingMethod (View view ) {
352- updateIsShapeRoundRect ();
353- view .setClipToOutline (!shouldUseCompatClipping ());
354- if (shouldUseCompatClipping ()) {
355- view .invalidate ();
356- } else {
357- view .invalidateOutline ();
358- }
359- }
360-
361- private void updateIsShapeRoundRect () {
362- if (!maskBounds .isEmpty () && shapeAppearanceModel != null ) {
363- isShapeRoundRect = shapeAppearanceModel .isRoundRect (maskBounds );
364- }
365- }
366-
367- private float getCornerRadiusFromShapeAppearance (
368- @ NonNull ShapeAppearanceModel shapeAppearanceModel , @ NonNull RectF bounds ) {
369- return shapeAppearanceModel .getTopRightCornerSize ().getCornerSize (bounds );
370- }
371-
372- @ DoNotInline
373- private void initMaskOutlineProvider (View view ) {
374- view .setOutlineProvider (
375- new ViewOutlineProvider () {
376- @ Override
377- public void getOutline (View view , Outline outline ) {
378- if (shapeAppearanceModel != null && !maskBounds .isEmpty ()) {
379- outline .setRoundRect (
380- (int ) maskBounds .left ,
381- (int ) maskBounds .top ,
382- (int ) maskBounds .right ,
383- (int ) maskBounds .bottom ,
384- getCornerRadiusFromShapeAppearance (shapeAppearanceModel , maskBounds ));
385- }
386- }
387- });
388- }
389- }
390-
391- /**
392- * A {@link MaskableDelegate} for API 33+ that uses {@link ViewOutlineProvider} to clip for all
393- * shapes.
394- *
395- * <p>{@link Outline#setPath(Path)} was added in API 33 and allows using {@link
396- * ViewOutlineProvider} to clip for all shapes.
397- */
398- @ RequiresApi (VERSION_CODES .TIRAMISU )
399- private static class MaskableDelegateV33 extends MaskableDelegate {
400-
401- MaskableDelegateV33 (View view ) {
402- initMaskOutlineProvider (view );
403- }
404-
405- @ Override
406- public boolean shouldUseCompatClipping () {
407- return forceCompatClippingEnabled ;
408- }
409-
410- @ Override
411- void invalidateClippingMethod (View view ) {
412- view .setClipToOutline (!shouldUseCompatClipping ());
413- if (shouldUseCompatClipping ()) {
414- view .invalidate ();
415- } else {
416- view .invalidateOutline ();
417- }
418- }
419-
420- @ DoNotInline
421- private void initMaskOutlineProvider (View view ) {
422- view .setOutlineProvider (
423- new ViewOutlineProvider () {
424- @ Override
425- public void getOutline (View view , Outline outline ) {
426- if (!shapePath .isEmpty ()) {
427- outline .setPath (shapePath );
428- }
429- }
430- });
431- }
190+ shapeableDelegate .maybeClip (canvas , super ::dispatchDraw );
432191 }
433192}
0 commit comments