diff --git a/lib/java/com/google/android/material/navigation/NavigationBarItemView.java b/lib/java/com/google/android/material/navigation/NavigationBarItemView.java index de6e84195b7..5bf9faf769b 100644 --- a/lib/java/com/google/android/material/navigation/NavigationBarItemView.java +++ b/lib/java/com/google/android/material/navigation/NavigationBarItemView.java @@ -34,6 +34,7 @@ import androidx.appcompat.widget.TooltipCompat; import android.text.TextUtils; import android.util.Log; +import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -63,6 +64,7 @@ import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.badge.BadgeUtils; import com.google.android.material.motion.MotionUtils; +import com.google.android.material.resources.MaterialResources; /** * Provides a view that will be used to render destination items inside a {@link @@ -214,7 +216,7 @@ public void initialize(@NonNull MenuItemImpl itemData, int menuType) { /** * Remove state so this View can be reused. * - * Item Views are held in a pool and reused when the number of menu items to be shown changes. + *
Item Views are held in a pool and reused when the number of menu items to be shown changes. * This will be called when this View is released from the pool. * * @see NavigationBarMenuView#buildMenuView() @@ -230,8 +232,8 @@ void clear() { * If this item's layout contains a container which holds the icon and active indicator, return * the container. Otherwise, return the icon image view. * - * This is needed for clients who subclass this view and set their own item layout resource which - * might not container an icon container or active indicator view. + *
This is needed for clients who subclass this view and set their own item layout resource + * which might not container an icon container or active indicator view. */ private View getIconOrContainer() { return iconContainer != null ? iconContainer : icon; @@ -371,8 +373,8 @@ public void onAnimationUpdate(ValueAnimator animation) { /** * Refresh the state of this item if it has been initialized. * - * This is useful if parameters calculated based on this item's checked state (label visibility, - * indicator state, iconContainer position) have changed and should be recalculated. + *
This is useful if parameters calculated based on this item's checked state (label + * visibility, indicator state, iconContainer position) have changed and should be recalculated. */ private void refreshChecked() { if (itemData != null) { @@ -621,15 +623,32 @@ public void setIconSize(int iconSize) { } public void setTextAppearanceInactive(@StyleRes int inactiveTextAppearance) { - TextViewCompat.setTextAppearance(smallLabel, inactiveTextAppearance); + setTextAppearanceWithoutFontScaling(smallLabel, inactiveTextAppearance); calculateTextScaleFactors(smallLabel.getTextSize(), largeLabel.getTextSize()); } public void setTextAppearanceActive(@StyleRes int activeTextAppearance) { - TextViewCompat.setTextAppearance(largeLabel, activeTextAppearance); + setTextAppearanceWithoutFontScaling(largeLabel, activeTextAppearance); calculateTextScaleFactors(smallLabel.getTextSize(), largeLabel.getTextSize()); } + /** + * Remove font scaling if the text size is in scaled pixels. + * + *
Labels are instead made accessible by showing a scaled tooltip on long press of a + * destination. If the given {@code textAppearance} is 0 or does not have a textSize, this method + * will not remove the existing scaling from the {@code textView}. + */ + private static void setTextAppearanceWithoutFontScaling( + TextView textView, @StyleRes int textAppearance) { + TextViewCompat.setTextAppearance(textView, textAppearance); + int unscaledSize = + MaterialResources.getUnscaledTextSize(textView.getContext(), textAppearance, 0); + if (unscaledSize != 0) { + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, unscaledSize); + } + } + public void setTextColor(@Nullable ColorStateList color) { if (color != null) { smallLabel.setTextColor(color); @@ -698,8 +717,8 @@ public void setActiveIndicatorWidth(int width) { } /** - * Update the active indicators width and height for the available width and label - * visibility mode. + * Update the active indicators width and height for the available width and label visibility + * mode. * * @param availableWidth The total width of this item layout. */ diff --git a/lib/java/com/google/android/material/resources/MaterialResources.java b/lib/java/com/google/android/material/resources/MaterialResources.java index f40f613e78a..fc0c944f70b 100644 --- a/lib/java/com/google/android/material/resources/MaterialResources.java +++ b/lib/java/com/google/android/material/resources/MaterialResources.java @@ -16,12 +16,16 @@ package com.google.android.material.resources; +import com.google.android.material.R; + +import static android.util.TypedValue.COMPLEX_UNIT_MASK; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import androidx.appcompat.content.res.AppCompatResources; @@ -30,6 +34,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; +import androidx.annotation.StyleRes; import androidx.annotation.StyleableRes; /** @@ -190,6 +195,60 @@ public static boolean isFontScaleAtLeast2_0(@NonNull Context context) { return context.getResources().getConfiguration().fontScale >= FONT_SCALE_2_0; } + /** + * Return the {@code R.styleable.TextAppearance_android_textSize} value from a text appearance + * style at its density scaled value only. + * + * If the text size from the text appearance is using dp, the density scaled value will be + * returned. If the text size is using sp, the raw value will be scaled according to display + * density but not font scale, as if it were a dp value. + * + * This is used for components that do not scale their text due to being space constrained and + * instead offer alternative methods of showing scaled text. + */ + public static int getUnscaledTextSize( + @NonNull Context context, @StyleRes int textAppearance, int defValue) { + if (textAppearance == 0) { + return defValue; + } + + TypedArray a = context.obtainStyledAttributes(textAppearance, R.styleable.TextAppearance); + TypedValue v = new TypedValue(); + boolean available = a.getValue(R.styleable.TextAppearance_android_textSize, v); + a.recycle(); + + if (!available) { + return defValue; + } + + // If the resource is in scaled pixels (sp) manually unpack the resource and scale to density + // but not font scale. + if (getComplexUnit(v) == TypedValue.COMPLEX_UNIT_SP) { + // Get the raw value. If text size is set to 14sp in the dimen file, this will return 14. + // Scale the raw value using density and round to avoid truncating. + return Math.round( + TypedValue.complexToFloat(v.data) * context.getResources().getDisplayMetrics().density); + } + + // If the resource is not is sp, return with regular resource system scaling. + return TypedValue.complexToDimensionPixelSize( + v.data, context.getResources().getDisplayMetrics()); + } + + /** + * Return the complex unit type for the given value. + * + *
This is a compat method of {@link TypedValue#getComplexUnit()}, which is only available on
+ * API 22 and above.
+ */
+ private static int getComplexUnit(TypedValue tv) {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
+ return tv.getComplexUnit();
+ } else {
+ return (COMPLEX_UNIT_MASK & (tv.data >> TypedValue.COMPLEX_UNIT_SHIFT));
+ }
+ }
+
/**
* Returns the @StyleableRes index that contains value in the attributes array. If both indices
* contain values, the first given index takes precedence and is returned.
diff --git a/lib/javatests/com/google/android/material/resources/MaterialResourcesTest.java b/lib/javatests/com/google/android/material/resources/MaterialResourcesTest.java
new file mode 100644
index 00000000000..21ecc20c122
--- /dev/null
+++ b/lib/javatests/com/google/android/material/resources/MaterialResourcesTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.material.resources;
+
+import com.google.android.material.R;
+
+import static android.content.Context.WINDOW_SERVICE;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.res.Configuration;
+import androidx.appcompat.app.AppCompatActivity;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+import androidx.test.core.app.ApplicationProvider;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class MaterialResourcesTest {
+
+ private ActivityController