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 activityController; + + @Before + public void createActivity() { + ApplicationProvider.getApplicationContext() + .setTheme(R.style.Theme_Material3_DayNight_NoActionBar); + activityController = Robolectric.buildActivity(AppCompatActivity.class).create(); + } + + @Test + public void testGetUnscaledDimensionPixelSize_returnsSameValueWhenFontScalingIsOff() { + AppCompatActivity activity = activityController.get(); + int spResult = + MaterialResources.getUnscaledTextSize( + activity, R.style.TextAppearance_Test_UsesSP, 0); + int dpResult = + MaterialResources.getUnscaledTextSize( + activity, R.style.TextAppearance_Test_UsesDp, 0); + assertThat(spResult).isEqualTo(dpResult); + } + + @Test + public void testUnscaledDimensionPixelSize_differsFromDimensionPixelSize() { + float fontScale = 1.7F; + setFontScale(activityController, fontScale); + AppCompatActivity activity = activityController.get(); + int unscaledSp = + MaterialResources.getUnscaledTextSize( + activity, R.style.TextAppearance_Test_UsesSP, 0); + int scaledSp = activity.getResources().getDimensionPixelSize( + R.dimen.material_text_size_sp); + assertThat(unscaledSp).isNotEqualTo(scaledSp); + assertThat(scaledSp).isEqualTo(Math.round(unscaledSp * fontScale)); + } + + @Test + @Config(qualifiers = "xxxhdpi") + public void + testGetUnscaledDimensionPixelSize_returnsSameValueWhenFontScalingIsOnWithHighDensity() { + setFontScale(activityController, 1.3f); + AppCompatActivity activity = activityController.get(); + int spResult = + MaterialResources.getUnscaledTextSize( + activity, R.style.TextAppearance_Test_UsesSP, 0); + int dpResult = + MaterialResources.getUnscaledTextSize( + activity, R.style.TextAppearance_Test_UsesDp, 0); + assertThat(spResult).isEqualTo(dpResult); + } + + @Test + public void testUnscaledDimensionPixelSize_noTextSizeAvailable() { + AppCompatActivity activity = activityController.get(); + int noSizeResult = + MaterialResources.getUnscaledTextSize( + activity, R.style.TextAppearance_Test_NoTextSize, 18); + assertThat(noSizeResult).isEqualTo(18); + } + + @Test + public void testUnscaledDimensionPixelSize_noTextAppearanceAvailable() { + AppCompatActivity activity = activityController.get(); + int noTextAppearanceResult = + MaterialResources.getUnscaledTextSize( + activity, 0, 22); + assertThat(noTextAppearanceResult).isEqualTo(22); + } + + private static void setFontScale( + ActivityController activityController, float scale) { + AppCompatActivity activity = activityController.get(); + Configuration configuration = activity.getResources().getConfiguration(); + DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); + WindowManager wm = (WindowManager) activity.getSystemService(WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + configuration.fontScale = scale; + metrics.scaledDensity = configuration.fontScale * metrics.density; + activity.getResources().updateConfiguration(configuration, metrics); + activityController.configurationChange(configuration); + } +} diff --git a/lib/javatests/com/google/android/material/resources/res/values/dimens.xml b/lib/javatests/com/google/android/material/resources/res/values/dimens.xml index 3ceb5a3037e..0d6ba766da5 100644 --- a/lib/javatests/com/google/android/material/resources/res/values/dimens.xml +++ b/lib/javatests/com/google/android/material/resources/res/values/dimens.xml @@ -1,21 +1,25 @@ - - + + 100dp 16dp 100dp + 14sp + 14dp diff --git a/lib/javatests/com/google/android/material/resources/res/values/styles.xml b/lib/javatests/com/google/android/material/resources/res/values/styles.xml new file mode 100644 index 00000000000..834df18464d --- /dev/null +++ b/lib/javatests/com/google/android/material/resources/res/values/styles.xml @@ -0,0 +1,25 @@ + + + + + +