Skip to content

Commit

Permalink
[NavigationView] Removed canvas clipping by default and added an opti…
Browse files Browse the repository at this point in the history
…on to enabled/disable manually

Clipping was used to clip navigation view and its children to a shape appearace. This is primarily useful when using a headerLayout that contains full bleed content that would obscure the top end corner shape. Material3 does not use as much imagery/color in the NavigationView header and disabling clipping by default is a nice performace improvement.

PiperOrigin-RevId: 513301639
  • Loading branch information
hunterstich authored and paulfthomas committed Mar 1, 2023
1 parent a5da449 commit e3b493f
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 62 deletions.
17 changes: 10 additions & 7 deletions docs/components/NavigationDrawer.md
Expand Up @@ -254,13 +254,16 @@ subtitles, and an optional scrim.

### Container attributes

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


### Header attributes

Expand Down
121 changes: 66 additions & 55 deletions lib/java/com/google/android/material/navigation/NavigationView.java
Expand Up @@ -130,6 +130,7 @@ public class NavigationView extends ScrimInsetsFrameLayout {

private int layoutGravity = Gravity.NO_GRAVITY;
@Px private int drawerLayoutCornerSize = 0;
private boolean drawerLayoutCornerClippingEnabled = false;

@Nullable private Path shapeClipPath;
private final RectF shapeClipBounds = new RectF();
Expand All @@ -153,11 +154,7 @@ public NavigationView(@NonNull Context context, @Nullable AttributeSet attrs, in
// Custom attributes
TintTypedArray a =
ThemeEnforcement.obtainTintedStyledAttributes(
context,
attrs,
R.styleable.NavigationView,
defStyleAttr,
DEF_STYLE_RES);
context, attrs, R.styleable.NavigationView, defStyleAttr, DEF_STYLE_RES);

if (a.hasValue(R.styleable.NavigationView_android_background)) {
ViewCompat.setBackground(this, a.getDrawable(R.styleable.NavigationView_android_background));
Expand All @@ -166,9 +163,12 @@ public NavigationView(@NonNull Context context, @Nullable AttributeSet attrs, in
// Get the drawer layout corner size and layout gravity to be used to shape the exposed corners
// of this view when placed inside a drawer layout.
drawerLayoutCornerSize =
a.getDimensionPixelSize(
R.styleable.NavigationView_drawerLayoutCornerSize, 0);
a.getDimensionPixelSize(R.styleable.NavigationView_drawerLayoutCornerSize, 0);
layoutGravity = a.getInt(R.styleable.NavigationView_android_layout_gravity, Gravity.NO_GRAVITY);
setDrawerLayoutCornerClippingEnabled(
a.getBoolean(
R.styleable.NavigationView_drawerLayoutCornerClippingEnabled,
drawerLayoutCornerClippingEnabled));

// Set the background to a MaterialShapeDrawable if it hasn't been set or if it can be converted
// to a MaterialShapeDrawable.
Expand Down Expand Up @@ -204,7 +204,7 @@ public NavigationView(@NonNull Context context, @Nullable AttributeSet attrs, in
}

if (subheaderTextAppearance == NavigationMenuPresenter.NO_TEXT_APPEARANCE_SET
&& subheaderColor == null) {
&& subheaderColor == null) {
// If there isn't a text appearance set, we'll use a default text color
subheaderColor = createDefaultColorStateList(android.R.attr.textColorSecondary);
}
Expand Down Expand Up @@ -241,18 +241,18 @@ public NavigationView(@NonNull Context context, @Nullable AttributeSet attrs, in
if (itemBackground == null && hasShapeAppearance(a)) {
itemBackground = createDefaultItemBackground(a);

ColorStateList itemRippleColor = MaterialResources.getColorStateList(
context, a, R.styleable.NavigationView_itemRippleColor);
ColorStateList itemRippleColor =
MaterialResources.getColorStateList(
context, a, R.styleable.NavigationView_itemRippleColor);

// Use a ripple matching the item's shape as the foreground for api level 21+ and if a ripple
// color is set. Otherwise the selectableItemBackground foreground from the item layout will
// be used
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && itemRippleColor != null) {
Drawable itemRippleMask = createDefaultItemDrawable(a, null);
RippleDrawable ripple = new RippleDrawable(
RippleUtils.sanitizeRippleDrawableColor(itemRippleColor),
null,
itemRippleMask);
RippleDrawable ripple =
new RippleDrawable(
RippleUtils.sanitizeRippleDrawableColor(itemRippleColor), null, itemRippleMask);
presenter.setItemForeground(ripple);
}
}
Expand Down Expand Up @@ -289,8 +289,7 @@ public NavigationView(@NonNull Context context, @Nullable AttributeSet attrs, in
a.getBoolean(R.styleable.NavigationView_topInsetScrimEnabled, topInsetScrimEnabled));

setBottomInsetScrimEnabled(
a.getBoolean(R.styleable.NavigationView_bottomInsetScrimEnabled, bottomInsetScrimEnabled)
);
a.getBoolean(R.styleable.NavigationView_bottomInsetScrimEnabled, bottomInsetScrimEnabled));

final int itemIconPadding =
a.getDimensionPixelSize(R.styleable.NavigationView_itemIconPadding, 0);
Expand Down Expand Up @@ -345,6 +344,37 @@ public void setOverScrollMode(int overScrollMode) {
}
}

/**
* Gets whether NavigationView will clip itself and its children to its shape appearance and
* drawer layout corner size.
*
* <p>See {@link #setDrawerLayoutCornerClippingEnabled(boolean)}.
*
* @return true if this view will clip itself and its children to its shape appearance and drawer
* layout corner size
*/
public boolean isDrawerLayoutCornerClippingEnabled() {
return drawerLayoutCornerClippingEnabled;
}

/**
* Sets whether NavigationView should clip itself and its children to its shape appearance and
* drawer layout corner size.
*
* <p>Clipping uses {@link Canvas#clipPath(Path)} which is expensive and should be used only when
* necessary. The most common example of this is when using a header layout with a full bleed
* image or other content that would obscure the top end corner shape.
*
* @param enabled true if navigation view should use canvas clipping to clip itself and all
* children to its shape appearance and drawer layout corner size.
*/
public void setDrawerLayoutCornerClippingEnabled(boolean enabled) {
if (drawerLayoutCornerClippingEnabled != enabled) {
drawerLayoutCornerClippingEnabled = enabled;
invalidate();
}
}

/**
* Determine whether this view is placed inside a drawer layout and should have its exposed
* corners shaped according to the <code>app:drawerLayoutCornerSize</code> attribute.
Expand All @@ -357,8 +387,7 @@ private void maybeUpdateCornerSizeForDrawerLayout(@Px int width, @Px int height)
&& getBackground() instanceof MaterialShapeDrawable) {
// Get the absolute gravity of this view and set the top and bottom exposed corner sizes.
MaterialShapeDrawable background = (MaterialShapeDrawable) getBackground();
ShapeAppearanceModel.Builder builder =
background.getShapeAppearanceModel().toBuilder();
ShapeAppearanceModel.Builder builder = background.getShapeAppearanceModel().toBuilder();
int absGravity =
GravityCompat.getAbsoluteGravity(layoutGravity, ViewCompat.getLayoutDirection(this));
if (absGravity == Gravity.LEFT) {
Expand Down Expand Up @@ -423,8 +452,9 @@ public void setElevation(float elevation) {
*/
@NonNull
private Drawable createDefaultItemBackground(@NonNull TintTypedArray a) {
ColorStateList fillColor = MaterialResources.getColorStateList(
getContext(), a, R.styleable.NavigationView_itemShapeFillColor);
ColorStateList fillColor =
MaterialResources.getColorStateList(
getContext(), a, R.styleable.NavigationView_itemShapeFillColor);
return createDefaultItemDrawable(a, fillColor);
}

Expand All @@ -437,7 +467,7 @@ private Drawable createDefaultItemDrawable(
MaterialShapeDrawable materialShapeDrawable =
new MaterialShapeDrawable(
ShapeAppearanceModel.builder(
getContext(), shapeAppearanceResId, shapeAppearanceOverlayResId)
getContext(), shapeAppearanceResId, shapeAppearanceOverlayResId)
.build());
materialShapeDrawable.setFillColor(fillColor);

Expand Down Expand Up @@ -499,7 +529,7 @@ protected void onMeasure(int widthSpec, int heightSpec) {

@Override
protected void dispatchDraw(@NonNull Canvas canvas) {
if (shapeClipPath == null) {
if (shapeClipPath == null || !isDrawerLayoutCornerClippingEnabled()) {
super.dispatchDraw(canvas);
return;
}
Expand Down Expand Up @@ -822,33 +852,29 @@ public int getItemMaxLines() {
return presenter.getItemMaxLines();
}

/**
* Whether or not the NavigationView will draw a scrim behind the window's top inset.
*/
/** Whether or not the NavigationView will draw a scrim behind the window's top inset. */
public boolean isTopInsetScrimEnabled() {
return this.topInsetScrimEnabled;
}

/**
* Set whether or not the NavigationView should draw a scrim behind the window's top
* inset (typically the status bar).
* Set whether or not the NavigationView should draw a scrim behind the window's top inset
* (typically the status bar).
*
* @param enabled true when the NavigationView should draw a scrim.
*/
public void setTopInsetScrimEnabled(boolean enabled) {
this.topInsetScrimEnabled = enabled;
}

/**
* Whether or not the NavigationView will draw a scrim behind the window's bottom inset.
*/
/** Whether or not the NavigationView will draw a scrim behind the window's bottom inset. */
public boolean isBottomInsetScrimEnabled() {
return this.bottomInsetScrimEnabled;
}

/**
* Set whether or not the NavigationView should draw a scrim behind the window's bottom
* inset (typically the navigation bar)
* Set whether or not the NavigationView should draw a scrim behind the window's bottom inset
* (typically the navigation bar)
*
* @param enabled true when the NavigationView should draw a scrim.
*/
Expand All @@ -871,47 +897,35 @@ public void setDividerInsetStart(@Px int dividerInsetStart) {
presenter.setDividerInsetStart(dividerInsetStart);
}

/**
* Get the distance between the end of a divider and the end of the NavigationView.
*/
/** Get the distance between the end of a divider and the end of the NavigationView. */
@Px
public int getDividerInsetEnd() {
return presenter.getDividerInsetEnd();
}

/**
* Set the distance between the end of a divider and the end of the NavigationView.
*/
/** Set the distance between the end of a divider and the end of the NavigationView. */
public void setDividerInsetEnd(@Px int dividerInsetEnd) {
presenter.setDividerInsetEnd(dividerInsetEnd);
}

/**
* Get the distance between the start of the NavigationView and the start of a menu subheader.
*/
/** Get the distance between the start of the NavigationView and the start of a menu subheader. */
@Px
public int getSubheaderInsetStart() {
return presenter.getSubheaderInsetStart();
}

/**
* Set the distance between the start of the NavigationView and the start of a menu subheader.
*/
/** Set the distance between the start of the NavigationView and the start of a menu subheader. */
public void setSubheaderInsetStart(@Px int subheaderInsetStart) {
presenter.setSubheaderInsetStart(subheaderInsetStart);
}

/**
* Get the distance between the end of a menu subheader and the end of the NavigationView.
*/
/** Get the distance between the end of a menu subheader and the end of the NavigationView. */
@Px
public int getSubheaderInsetEnd() {
return presenter.getSubheaderInsetEnd();
}

/**
* Set the distance between the end of a menu subheader and the end of the NavigationView.
*/
/** Set the distance between the end of a menu subheader and the end of the NavigationView. */
public void setSubheaderInsetEnd(@Px int subheaderInsetEnd) {
presenter.setSubheaderInsetEnd(subheaderInsetEnd);
}
Expand Down Expand Up @@ -978,8 +992,7 @@ public void onGlobalLayout() {
if (activity != null && VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
Rect displayBounds = WindowUtils.getCurrentWindowBounds(activity);

boolean isBehindSystemNav =
displayBounds.height() - getHeight() == tmpLocation[1];
boolean isBehindSystemNav = displayBounds.height() - getHeight() == tmpLocation[1];
boolean hasNonZeroAlpha =
Color.alpha(activity.getWindow().getNavigationBarColor()) != 0;
setDrawBottomInsetForeground(
Expand All @@ -996,9 +1009,7 @@ public void onGlobalLayout() {
}
};

getViewTreeObserver()
.addOnGlobalLayoutListener(
onGlobalLayoutListener);
getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
}

/** Listener for handling events on navigation items. */
Expand Down
Expand Up @@ -44,6 +44,7 @@
<public name="subheaderInsetStart" type="attr"/>
<public name="subheaderInsetEnd" type="attr"/>
<public name="drawerLayoutCornerSize" type="attr"/>
<public name="drawerLayoutCornerClippingEnabled" type="attr"/>
<public name="Widget.Design.NavigationView" type="style"/>
<public name="Widget.Material3.NavigationView" type="style"/>
<public name="ShapeAppearanceOverlay.Material3.NavigationView.Item" type="style"/>
Expand Down
Expand Up @@ -167,5 +167,8 @@
<!-- Corner size for the exposed corners (those not attached to the side
of the screen) when the navigation view is placed inside a drawer layout.-->
<attr name="drawerLayoutCornerSize" format="dimension"/>
<!-- True if the navigation view should use clip children to its shape
appearance and drawerLayoutCornerSize.-->
<attr name="drawerLayoutCornerClippingEnabled" format="boolean"/>
</declare-styleable>
</resources>

0 comments on commit e3b493f

Please sign in to comment.