Skip to content

Commit fd23c6b

Browse files
hunterstichdsn5ft
authored andcommitted
[NavigationRail] Added support for unlabeled items to automatically resize their active indicator.
PiperOrigin-RevId: 399689605
1 parent 2db68ee commit fd23c6b

File tree

3 files changed

+146
-21
lines changed

3 files changed

+146
-21
lines changed

lib/java/com/google/android/material/navigation/NavigationBarItemView.java

Lines changed: 123 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,21 @@ public abstract class NavigationBarItemView extends FrameLayout implements MenuV
9898
@Nullable private Drawable originalIconDrawable;
9999
@Nullable private Drawable wrappedIconDrawable;
100100

101+
private static final ActiveIndicatorTransform ACTIVE_INDICATOR_LABELED_TRANSFORM =
102+
new ActiveIndicatorTransform();
103+
private static final ActiveIndicatorTransform ACTIVE_INDICATOR_UNLABELED_TRANSFORM =
104+
new ActiveIndicatorUnlabeledTransform();
105+
101106
private ValueAnimator activeIndicatorAnimator;
107+
private ActiveIndicatorTransform activeIndicatorTransform = ACTIVE_INDICATOR_LABELED_TRANSFORM;
102108
private float activeIndicatorProgress = 0F;
103109
private boolean activeIndicatorEnabled = false;
104110
// The desired width of the indicator. This is not necessarily the actual size of the rendered
105111
// indicator depending on whether the width of this view is wide enough to accommodate the full
106112
// desired width.
107113
private int activeIndicatorDesiredWidth = 0;
114+
private int activeIndicatorDesiredHeight = 0;
115+
private boolean activeIndicatorResizeable = false;
108116
// The margin from the start and end of this view which the active indicator should respect. If
109117
// the indicator width is greater than the total width minus the horizontal margins, the active
110118
// indicator will assume the max width of the view's total width minus horizontal margins.
@@ -246,6 +254,8 @@ public void setShifting(boolean shifting) {
246254
public void setLabelVisibilityMode(@NavigationBarView.LabelVisibility int mode) {
247255
if (labelVisibilityMode != mode) {
248256
labelVisibilityMode = mode;
257+
updateActiveIndicatorTransform();
258+
updateActiveIndicatorLayoutParams(getWidth());
249259
refreshChecked();
250260
}
251261
}
@@ -291,11 +301,19 @@ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
291301
new Runnable() {
292302
@Override
293303
public void run() {
294-
updateActiveIndicatorAvailableWidth(width);
304+
updateActiveIndicatorLayoutParams(width);
295305
}
296306
});
297307
}
298308

309+
private void updateActiveIndicatorTransform() {
310+
if (isActiveIndicatorResizeableAndUnlabeled()) {
311+
activeIndicatorTransform = ACTIVE_INDICATOR_UNLABELED_TRANSFORM;
312+
} else {
313+
activeIndicatorTransform = ACTIVE_INDICATOR_LABELED_TRANSFORM;
314+
}
315+
}
316+
299317
/**
300318
* Update the active indicator for a given 0-1 value.
301319
*
@@ -307,14 +325,8 @@ public void run() {
307325
*/
308326
private void setActiveIndicatorProgress(
309327
@FloatRange(from = 0F, to = 1F) float progress, float target) {
310-
// TODO: Consider refactoring into a transform class.
311328
if (activeIndicatorView != null) {
312-
activeIndicatorView.setScaleX(AnimationUtils.lerp(.4f, 1f, progress));
313-
// Animate the alpha of the indicator over the first 1/5th of the animation
314-
float startAlphaFraction = target == 0F ? .8F : 0F;
315-
float endAlphaFraction = target == 0F ? 1F : .2F;
316-
activeIndicatorView.setAlpha(
317-
AnimationUtils.lerp(0f, 1f, startAlphaFraction, endAlphaFraction, progress));
329+
activeIndicatorTransform.updateForProgress(progress, target, activeIndicatorView);
318330
}
319331
activeIndicatorProgress = progress;
320332
}
@@ -681,15 +693,16 @@ public void setActiveIndicatorEnabled(boolean enabled) {
681693
*/
682694
public void setActiveIndicatorWidth(int width) {
683695
this.activeIndicatorDesiredWidth = width;
684-
updateActiveIndicatorAvailableWidth(getWidth());
696+
updateActiveIndicatorLayoutParams(getWidth());
685697
}
686698

687699
/**
688-
* Update the active indicator to always be within the width of this item layout.
700+
* Update the active indicators width and height for the available width and label
701+
* visibility mode.
689702
*
690703
* @param availableWidth The total width of this item layout.
691704
*/
692-
private void updateActiveIndicatorAvailableWidth(int availableWidth) {
705+
private void updateActiveIndicatorLayoutParams(int availableWidth) {
693706
// Set width to the min of either the desired indicator width or the available width minus
694707
// a horizontal margin.
695708
if (activeIndicatorView == null) {
@@ -698,38 +711,42 @@ private void updateActiveIndicatorAvailableWidth(int availableWidth) {
698711

699712
int newWidth =
700713
min(activeIndicatorDesiredWidth, availableWidth - (activeIndicatorMarginHorizontal * 2));
714+
701715
LayoutParams indicatorParams = (LayoutParams) activeIndicatorView.getLayoutParams();
716+
// If the label visibility is unlabeled, make the active indicator's height equal to it's width.
717+
indicatorParams.height =
718+
isActiveIndicatorResizeableAndUnlabeled() ? newWidth : activeIndicatorDesiredHeight;
702719
indicatorParams.width = newWidth;
703720
activeIndicatorView.setLayoutParams(indicatorParams);
704721
}
705722

723+
private boolean isActiveIndicatorResizeableAndUnlabeled() {
724+
return activeIndicatorResizeable
725+
&& labelVisibilityMode == NavigationBarView.LABEL_VISIBILITY_UNLABELED;
726+
}
727+
706728
/**
707729
* Set the desired height of the active indicator.
708730
*
709-
* <p>TODO: Consider adjusting based on available height.
731+
* <p>TODO: Consider adjusting based on available height
710732
*
711733
* @param height The desired height of the active indicator.
712734
*/
713735
public void setActiveIndicatorHeight(int height) {
714-
if (activeIndicatorView == null) {
715-
return;
716-
}
717-
718-
LayoutParams indicatorParams = (LayoutParams) activeIndicatorView.getLayoutParams();
719-
indicatorParams.height = height;
720-
activeIndicatorView.setLayoutParams(indicatorParams);
736+
activeIndicatorDesiredHeight = height;
737+
updateActiveIndicatorLayoutParams(getWidth());
721738
}
722739

723740
/**
724741
* Set the horizontal margin that will be maintained at the start and end of the active indicator,
725742
* making sure the indicator remains the given distance from the edge of this item view.
726743
*
727-
* @see #updateActiveIndicatorAvailableWidth(int)
744+
* @see #updateActiveIndicatorLayoutParams(int)
728745
* @param marginHorizontal The horizontal margin, in pixels.
729746
*/
730747
public void setActiveIndicatorMarginHorizontal(@Px int marginHorizontal) {
731748
this.activeIndicatorMarginHorizontal = marginHorizontal;
732-
updateActiveIndicatorAvailableWidth(getWidth());
749+
updateActiveIndicatorLayoutParams(getWidth());
733750
}
734751

735752
/** Get the drawable used as the active indicator. */
@@ -751,6 +768,11 @@ public void setActiveIndicatorDrawable(@Nullable Drawable activeIndicatorDrawabl
751768
activeIndicatorView.setBackgroundDrawable(activeIndicatorDrawable);
752769
}
753770

771+
/** Set whether the indicator can be automatically resized. */
772+
public void setActiveIndicatorResizeable(boolean resizeable) {
773+
this.activeIndicatorResizeable = resizeable;
774+
}
775+
754776
void setBadge(@NonNull BadgeDrawable badgeDrawable) {
755777
this.badgeDrawable = badgeDrawable;
756778
if (icon != null) {
@@ -869,4 +891,84 @@ protected int getItemDefaultMarginResId() {
869891
*/
870892
@LayoutRes
871893
protected abstract int getItemLayoutResId();
894+
895+
/**
896+
* A class used to manipulate the {@link NavigationBarItemView}'s active indicator view when
897+
* animating between hidden and shown.
898+
*
899+
* <p>By default, this class scales the indicator in the x direction to reveal the default pill
900+
* shape.
901+
*
902+
* <p>Subclasses can override {@link #updateForProgress(float, float, View)} to manipulate the
903+
* view in any way appropriate.
904+
*/
905+
private static class ActiveIndicatorTransform {
906+
907+
private static final float SCALE_X_HIDDEN = .4F;
908+
private static final float SCALE_X_SHOWN = 1F;
909+
910+
// The fraction of the animation's total duration over which the indicator will be faded in or
911+
// out.
912+
private static final float ALPHA_FRACTION = 1F / 5F;
913+
914+
/**
915+
* Calculate the alpha value, based on a progress and target value, that has the indicator
916+
* appear or disappear over the first 1/5th of the transform.
917+
*/
918+
protected float calculateAlpha(
919+
@FloatRange(from = 0F, to = 1F) float progress,
920+
@FloatRange(from = 0F, to = 1F) float targetValue) {
921+
// Animate the alpha of the indicator over the first ALPHA_FRACTION of the animation
922+
float startAlphaFraction = targetValue == 0F ? 1F - ALPHA_FRACTION : 0F;
923+
float endAlphaFraction = targetValue == 0F ? 1F : 0F + ALPHA_FRACTION;
924+
return AnimationUtils.lerp(0F, 1F, startAlphaFraction, endAlphaFraction, progress);
925+
}
926+
927+
protected float calculateScaleX(
928+
@FloatRange(from = 0F, to = 1F) float progress,
929+
@FloatRange(from = 0F, to = 1F) float targetValue) {
930+
return AnimationUtils.lerp(SCALE_X_HIDDEN, SCALE_X_SHOWN, progress);
931+
}
932+
933+
protected float calculateScaleY(
934+
@FloatRange(from = 0F, to = 1F) float progress,
935+
@FloatRange(from = 0F, to = 1F) float targetValue) {
936+
return 1F;
937+
}
938+
939+
/**
940+
* Called whenever the {@code indicator} should update its parameters (scale, alpha, etc.) in
941+
* response to a change in progress.
942+
*
943+
* @param progress A value between 0 and 1 where 0 represents a fully hidden indicator and 1
944+
* indicates a fully shown indicator.
945+
* @param targetValue The final value towards which the progress is moving. This will be either
946+
* 0 and 1 and can be used to determine whether the indicator is showing or hiding if show
947+
* and hide animations differ.
948+
* @param indicator The active indicator {@link View}.
949+
*/
950+
public void updateForProgress(
951+
@FloatRange(from = 0F, to = 1F) float progress,
952+
@FloatRange(from = 0F, to = 1F) float targetValue,
953+
@NonNull View indicator) {
954+
indicator.setScaleX(calculateScaleX(progress, targetValue));
955+
indicator.setScaleY(calculateScaleY(progress, targetValue));
956+
indicator.setAlpha(calculateAlpha(progress, targetValue));
957+
}
958+
}
959+
960+
/**
961+
* A transform class used to animate the active indicator of a {@link NavigationBarItemView} that
962+
* is unlabeled.
963+
*
964+
* <p>This differs from the default {@link ActiveIndicatorTransform} class by uniformly scaling in
965+
* the X and Y axis.
966+
*/
967+
private static class ActiveIndicatorUnlabeledTransform extends ActiveIndicatorTransform {
968+
969+
@Override
970+
protected float calculateScaleY(float progress, float targetValue) {
971+
return calculateScaleX(progress, targetValue);
972+
}
973+
}
872974
}

lib/java/com/google/android/material/navigation/NavigationBarMenuView.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public abstract class NavigationBarMenuView extends ViewGroup implements MenuVie
9999
private int itemActiveIndicatorHeight;
100100
private int itemActiveIndicatorMarginHorizontal;
101101
private ShapeAppearanceModel itemActiveIndicatorShapeAppearance;
102+
private boolean itemActiveIndicatorResizeable = false;
102103
private ColorStateList itemActiveIndicatorColor;
103104

104105
private NavigationBarPresenter presenter;
@@ -466,6 +467,26 @@ public void setItemActiveIndicatorShapeAppearance(
466467
}
467468
}
468469

470+
/**
471+
* Get whether the active indicator can be resized.
472+
*/
473+
protected boolean isItemActiveIndicatorResizeable() {
474+
return this.itemActiveIndicatorResizeable;
475+
}
476+
477+
/**
478+
* Set whether the active indicator can be resized. If true, the indicator will automatically
479+
* change size in response to label visibility modes.
480+
*/
481+
protected void setItemActiveIndicatorResizeable(boolean resizeable) {
482+
this.itemActiveIndicatorResizeable = resizeable;
483+
if (buttons != null) {
484+
for (NavigationBarItemView item : buttons) {
485+
item.setActiveIndicatorResizeable(resizeable);
486+
}
487+
}
488+
}
489+
469490
/**
470491
* Get the color of the active indicator drawable.
471492
*
@@ -668,6 +689,7 @@ public void buildMenuView() {
668689
child.setActiveIndicatorHeight(itemActiveIndicatorHeight);
669690
child.setActiveIndicatorMarginHorizontal(itemActiveIndicatorMarginHorizontal);
670691
child.setActiveIndicatorDrawable(createItemActiveIndicatorDrawable());
692+
child.setActiveIndicatorResizeable(itemActiveIndicatorResizeable);
671693
child.setActiveIndicatorEnabled(itemActiveIndicatorEnabled);
672694
if (itemBackground != null) {
673695
child.setItemBackground(itemBackground);

lib/java/com/google/android/material/navigationrail/NavigationRailMenuView.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public NavigationRailMenuView(@NonNull Context context) {
4747

4848
layoutParams.gravity = DEFAULT_MENU_GRAVITY;
4949
setLayoutParams(layoutParams);
50+
setItemActiveIndicatorResizeable(true);
5051
}
5152

5253
@Override

0 commit comments

Comments
 (0)