Skip to content

Commit c031144

Browse files
hunterstichleticiarossi
authored andcommitted
[NavigationView] Updated NavigationView to use ViewOutlineProvider to handle corner clipping when possible and remove drawerLayoutCornerClippingEnabled attribute.
PiperOrigin-RevId: 527633449
1 parent 52f1737 commit c031144

File tree

10 files changed

+705
-339
lines changed

10 files changed

+705
-339
lines changed

docs/components/NavigationDrawer.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -254,16 +254,14 @@ subtitles, and an optional scrim.
254254

255255
### Container attributes
256256

257-
Element | Attribute(s) | Related method(s) | Default value
258-
----------------------- |---------------------------------------------------------------------|----------------------------------------------------------------------------------| -------------
259-
**Color** | `android:background` | `setBackground`<br>`getBackground` | `?attr/colorSurface`
260-
**Shape** | `app:shapeAppearance`<br>`app:shapeAppearanceOverlay` | N/A | `null`
261-
**Elevation** | `app:elevation` (can be used on `NavigationView` or `DrawerLayout`) | `setElevation`<br>`getElevation` | `0dp` (`NavigationView`) or `1dp` (`DrawerLayout`)
262-
**Max width** | `android:maxWidth` | N/A | `280dp`
263-
**Fits system windows** | `android:fitsSystemWindows` | `setFitsSystemWindows`<br>`getFitsSystemWindows` | `true`
264-
**Drawer corner size** | `drawerLayoutCornerSize` | N/A | `16dp`
265-
**Drawer corner clipping** | `drawerLayoutCornerClippingEnabled` | `setDrawerLayoutCornerClippingEnabled`<br/>`isDrawerLayoutCornerClippingEnabled` | `false`
266-
257+
Element | Attribute(s) | Related method(s) | Default value
258+
----------------------- | ------------------------------------------------------------------- | ------------------------------------------------ | -------------
259+
**Color** | `android:background` | `setBackground`<br>`getBackground` | `?attr/colorSurface`
260+
**Shape** | `app:shapeAppearance`<br>`app:shapeAppearanceOverlay` | N/A | `null`
261+
**Elevation** | `app:elevation` (can be used on `NavigationView` or `DrawerLayout`) | `setElevation`<br>`getElevation` | `0dp` (`NavigationView`) or `1dp` (`DrawerLayout`)
262+
**Max width** | `android:maxWidth` | N/A | `280dp`
263+
**Fits system windows** | `android:fitsSystemWindows` | `setFitsSystemWindows`<br>`getFitsSystemWindows` | `true`
264+
**Drawer corner size** | `drawerLayoutCornerSize` | N/A | `16dp`
267265

268266
### Header attributes
269267

lib/java/com/google/android/material/carousel/MaskableFrameLayout.java

Lines changed: 9 additions & 250 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,23 @@
1919
import android.annotation.SuppressLint;
2020
import android.content.Context;
2121
import android.graphics.Canvas;
22-
import android.graphics.Outline;
23-
import android.graphics.Path;
24-
import android.graphics.Rect;
2522
import android.graphics.RectF;
26-
import android.os.Build.VERSION;
27-
import android.os.Build.VERSION_CODES;
2823
import android.util.AttributeSet;
2924
import android.view.MotionEvent;
3025
import android.view.View;
31-
import android.view.ViewOutlineProvider;
3226
import android.widget.FrameLayout;
33-
import androidx.annotation.DoNotInline;
3427
import androidx.annotation.NonNull;
3528
import androidx.annotation.Nullable;
36-
import androidx.annotation.RequiresApi;
3729
import androidx.annotation.RestrictTo;
3830
import androidx.annotation.RestrictTo.Scope;
3931
import androidx.annotation.VisibleForTesting;
4032
import androidx.core.math.MathUtils;
4133
import com.google.android.material.animation.AnimationUtils;
42-
import com.google.android.material.canvas.CanvasCompat.CanvasOperation;
4334
import com.google.android.material.shape.AbsoluteCornerSize;
4435
import com.google.android.material.shape.ClampedCornerSize;
4536
import com.google.android.material.shape.ShapeAppearanceModel;
46-
import com.google.android.material.shape.ShapeAppearancePathProvider;
4737
import 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. */
5041
public 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

Comments
 (0)