fix(non linear font scaling)!: add setLineHeight(unit, lineHeight) to TextView compat classes
This uses the new APIs in Android U.
Also added TypedValueCompat.getUnitFromComplexDimension() so we can use
units in AppCompatTextView.
Tests copied from
platform/cts/tests/tests/widget/src/android/widget/cts/TextViewFontScalingTest.kt
Bug: 273326061
Fix: 278145408
Test: ./gradlew appcompat:appcompat:connectedCheck --info --daemon -Pandroid.testInstrumentationRunnerArguments.class=androidx.appcompat.widget.AppCompatTextViewFontScalingTest
Relnote: added setLineHeight(unit, lineHeight) to TextView compat classes
Change-Id: Ib2ee1334bd0396a8d92a87b8c2268029a36b1148
diff --git a/appcompat/appcompat/src/androidTest/AndroidManifest.xml b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
index fb17a4b..aa16fad 100644
--- a/appcompat/appcompat/src/androidTest/AndroidManifest.xml
+++ b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
@@ -131,6 +131,11 @@
android:theme="@style/Theme.TextColors"/>
<activity
+ android:name="androidx.appcompat.widget.AppCompatTextViewFontScalingTest$TextViewFontScalingActivity"
+ android:label="@string/app_compat_text_view_activity"
+ android:theme="@style/Theme.TextColors" />
+
+ <activity
android:name="androidx.appcompat.widget.AppCompatTextViewAutoSizeActivity"
android:label="@string/app_compat_text_view_auto_size_activity"
android:theme="@style/Theme.AppCompat.Light"/>
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatTextViewFontScalingTest.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatTextViewFontScalingTest.kt
new file mode 100644
index 0000000..a87d598
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatTextViewFontScalingTest.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2023 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 androidx.appcompat.widget
+
+import android.app.Activity
+import android.os.Build
+import android.os.Bundle
+import android.util.TypedValue
+import android.widget.TextView
+import androidx.appcompat.test.R
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.testutils.AndroidFontScaleHelper.resetSystemFontScale
+import androidx.testutils.AndroidFontScaleHelper.setSystemFontScale
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Test {@link AppCompatTextView} under non-linear font scaling.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class AppCompatTextViewFontScalingTest {
+ @get:Rule
+ val scenarioRule = ActivityScenarioRule(TextViewFontScalingActivity::class.java)
+
+ @After
+ fun teardown() {
+ // Have to manually check the version here because if we try to rely on the assumeTrue() in
+ // resetSystemFontScale(), it is called twice (again in setSystemFontScale()) and the test
+ // fails with a "TestCouldNotBeSkippedException: Test could not be skipped due to other
+ // failures" because it thinks the second assumeTrue() was a separate error.
+ // tl;dr avoids a bug in jUnit when multiple assumeTrue()s happen in @Test and @After
+ if (Build.VERSION.SDK_INT >= 29) {
+ resetSystemFontScale(scenarioRule.scenario)
+ }
+ }
+
+ @Test
+ @Throws(Throwable::class)
+ fun testNonLinearFontScaling_testSetLineHeightSpAndSetTextSizeSp() {
+ setSystemFontScale(2f, scenarioRule.scenario)
+ scenarioRule.scenario.onActivity { activity ->
+ assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+ val textView = AppCompatTextView(activity)
+ val textSizeSp = 20f
+ val lineHeightSp = 40f
+
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp)
+ textView.setLineHeight(TypedValue.COMPLEX_UNIT_SP, lineHeightSp)
+
+ verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+ }
+ }
+
+ @Test
+ @Throws(Throwable::class)
+ fun testNonLinearFontScaling_overwriteXml_testSetLineHeightSpAndSetTextSizeSp() {
+ setSystemFontScale(2f, scenarioRule.scenario)
+ scenarioRule.scenario.onActivity { activity ->
+ assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+ val textView = findTextView(activity, R.id.textview_lineheight2x)
+ val textSizeSp = 20f
+ val lineHeightSp = 40f
+
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp)
+ textView.setLineHeight(TypedValue.COMPLEX_UNIT_SP, lineHeightSp)
+
+ verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+ }
+ }
+
+ @Test
+ @Throws(Throwable::class)
+ fun testNonLinearFontScaling_xml_testLineHeightAttrSpAndTextSizeAttrSp() {
+ setSystemFontScale(2f, scenarioRule.scenario)
+ scenarioRule.scenario.onActivity { activity ->
+ assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+ val textView = findTextView(activity, R.id.textview_lineheight2x)
+ val textSizeSp = 20f
+ val lineHeightSp = 40f
+
+ verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+ }
+ }
+
+ @Test
+ @Throws(Throwable::class)
+ fun testNonLinearFontScaling_dimenXml_testLineHeightAttrSpAndTextSizeAttrSp() {
+ setSystemFontScale(2f, scenarioRule.scenario)
+ scenarioRule.scenario.onActivity { activity ->
+ assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+ val textView = findTextView(activity, R.id.textview_lineheight_dimen3x)
+ val textSizeSp = 20f
+ val lineHeightSp = 60f
+
+ verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+ }
+ }
+
+ @Test
+ @Throws(Throwable::class)
+ fun testNonLinearFontScaling_styleXml_testLineHeightAttrSpAndTextSizeAttrSp() {
+ setSystemFontScale(2f, scenarioRule.scenario)
+ scenarioRule.scenario.onActivity { activity ->
+ assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+ val textView = findTextView(activity, R.id.textview_lineheight_style3x)
+ val textSizeSp = 20f
+ val lineHeightSp = 60f
+
+ verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+ }
+ }
+
+ @Test
+ @Throws(Throwable::class)
+ fun testNonLinearFontScaling_dimenXml_testSetLineHeightSpAndTextSizeAttrSp() {
+ setSystemFontScale(2f, scenarioRule.scenario)
+ scenarioRule.scenario.onActivity { activity ->
+ assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+ val textView = findTextView(activity, R.id.textview_lineheight_dimen3x)
+ val textSizeSp = 20f
+ val lineHeightSp = 30f
+
+ textView.setLineHeight(TypedValue.COMPLEX_UNIT_SP, lineHeightSp)
+
+ verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+ }
+ }
+
+ private fun findTextView(activity: Activity, id: Int): AppCompatTextView {
+ return activity.findViewById(id)!!
+ }
+
+ class TextViewFontScalingActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.appcompat_textview_fontscaling_activity)
+ }
+ }
+
+ companion object {
+ /**
+ * Tolerance for comparing expected float lineHeight to the integer one returned by
+ * getLineHeight(). It is pretty lenient to account for integer rounding when text size is
+ * loaded from an attribute. (When loading an SP resource from an attribute for textSize,
+ * it is rounded to the nearest pixel, which can throw off calculations quite a lot. Not
+ * enough to make much of a difference to the user, but enough to need a wide tolerance in
+ * tests. See b/279456702 for more details.)
+ */
+ private const val TOLERANCE = 5f
+
+ private fun verifyLineHeightIsIntendedProportions(
+ lineHeightSp: Float,
+ textSizeSp: Float,
+ activity: Activity,
+ textView: TextView
+ ) {
+ val lineHeightMultiplier = lineHeightSp / textSizeSp
+ // Calculate what line height would be without non-linear font scaling compressing it.
+ // The trick is multiplying afterwards (by the pixel value) instead of before (by the SP
+ // value)
+ val expectedLineHeightPx = lineHeightMultiplier * TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ textSizeSp,
+ activity.resources.displayMetrics
+ )
+ assertThat(textView.lineHeight.toFloat())
+ .isWithin(TOLERANCE)
+ .of(expectedLineHeightPx)
+ }
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/res/layout/appcompat_textview_fontscaling_activity.xml b/appcompat/appcompat/src/androidTest/res/layout/appcompat_textview_fontscaling_activity.xml
new file mode 100644
index 0000000..fadabf4
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/res/layout/appcompat_textview_fontscaling_activity.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:id="@+id/layout_textviewtest"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- Tests line height 2x the text size -->
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/textview_lineheight2x"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text_long"
+ android:textSize="20sp"
+ android:lineHeight="40sp" />
+
+ <!-- Tests line height 3x the text size -->
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/textview_lineheight_dimen3x"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text_long"
+ android:textSize="@dimen/textview_fontScaling_textSize"
+ android:lineHeight="@dimen/textview_fontScaling_lineHeight" />
+
+ <!-- Tests line height 3x the text size -->
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/textview_lineheight_style3x"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text_long"
+ style="@style/TextAppearance.FontScaling" />
+
+ </LinearLayout>
+
+</ScrollView>
diff --git a/appcompat/appcompat/src/androidTest/res/values/dimens.xml b/appcompat/appcompat/src/androidTest/res/values/dimens.xml
index d4a8a0e..aed0b5d 100644
--- a/appcompat/appcompat/src/androidTest/res/values/dimens.xml
+++ b/appcompat/appcompat/src/androidTest/res/values/dimens.xml
@@ -27,4 +27,7 @@
<dimen name="textview_firstBaselineToTopHeight">100dp</dimen>
<dimen name="textview_lastBaselineToBottomHeight">30dp</dimen>
<dimen name="textview_lineHeight">60dp</dimen>
+
+ <dimen name="textview_fontScaling_textSize">20sp</dimen>
+ <dimen name="textview_fontScaling_lineHeight">60sp</dimen>
</resources>
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/res/values/donottranslate-strings.xml b/appcompat/appcompat/src/androidTest/res/values/donottranslate-strings.xml
index 151400a..7a07538 100644
--- a/appcompat/appcompat/src/androidTest/res/values/donottranslate-strings.xml
+++ b/appcompat/appcompat/src/androidTest/res/values/donottranslate-strings.xml
@@ -71,6 +71,16 @@
<string name="app_compat_button_auto_size_activity">AppCompat button auto-size</string>
<string name="sample_text1">Sample text 1</string>
<string name="sample_text2">Sample text 2</string>
+ <string name="sample_text_long">This is a really long string which exceeds the width of the
+view. New devices have a much larger screen which actually enables long strings to be displayed
+with no fading. I have made this string longer to fix this case. If you are correcting this
+text, I would love to see the kind of devices you guys now use! Guys, maybe some devices need longer
+string! I think so, so how about double this string, like copy and paste!
+This is a really long string which exceeds the width of the view.
+New devices have a much larger screen which actually enables long strings to be displayed
+with no fading. I have made this string longer to fix this case. If you are correcting this
+text, I would love to see the kind of devices you guys now use! Guys, maybe some devices need longer
+string! I think so, so how about double this string, like copy and paste!</string>
<string name="app_compat_image_button_activity">AppCompat image button</string>
<string name="app_compat_image_view_activity">AppCompat image view</string>
<string name="app_compat_button_activity">AppCompat button</string>
diff --git a/appcompat/appcompat/src/androidTest/res/values/styles.xml b/appcompat/appcompat/src/androidTest/res/values/styles.xml
index 0b540bf..a076019 100644
--- a/appcompat/appcompat/src/androidTest/res/values/styles.xml
+++ b/appcompat/appcompat/src/androidTest/res/values/styles.xml
@@ -146,4 +146,9 @@
<item name="android:colorForeground">@color/color_state_list_lilac</item>
</style>
+ <!-- Tests line height 3x the text size -->
+ <style name="TextAppearance.FontScaling">
+ <item name="android:textSize">@dimen/textview_fontScaling_textSize</item>
+ <item name="android:lineHeight">@dimen/textview_fontScaling_lineHeight</item>
+ </style>
</resources>
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java
index fda4335..4eb3371 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java
@@ -42,6 +42,7 @@
import androidx.annotation.RestrictTo;
import androidx.appcompat.R;
import androidx.core.content.res.ResourcesCompat;
+import androidx.core.util.TypedValueCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.core.widget.TextViewCompat;
@@ -326,8 +327,20 @@
R.styleable.AppCompatTextView_firstBaselineToTopHeight, -1);
final int lastBaselineToBottomHeight = a.getDimensionPixelSize(
R.styleable.AppCompatTextView_lastBaselineToBottomHeight, -1);
- final int lineHeight = a.getDimensionPixelSize(
- R.styleable.AppCompatTextView_lineHeight, -1);
+ float lineHeight = -1;
+ int lineHeightUnit = -1;
+ if (a.hasValue(R.styleable.AppCompatTextView_lineHeight)) {
+ TypedValue peekValue = a.peekValue(R.styleable.AppCompatTextView_lineHeight);
+ if (peekValue != null && peekValue.type == TypedValue.TYPE_DIMENSION) {
+ lineHeightUnit = TypedValueCompat.getUnitFromComplexDimension(peekValue.data);
+ lineHeight = TypedValue.complexToFloat(peekValue.data);
+ } else {
+ lineHeight = a.getDimensionPixelSize(
+ R.styleable.AppCompatTextView_lineHeight,
+ -1
+ );
+ }
+ }
a.recycle();
if (firstBaselineToTopHeight != -1) {
@@ -337,7 +350,11 @@
TextViewCompat.setLastBaselineToBottomHeight(mView, lastBaselineToBottomHeight);
}
if (lineHeight != -1) {
- TextViewCompat.setLineHeight(mView, lineHeight);
+ if (lineHeightUnit == -1) {
+ TextViewCompat.setLineHeight(mView, (int) lineHeight);
+ } else {
+ TextViewCompat.setLineHeight(mView, lineHeightUnit, lineHeight);
+ }
}
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextView.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextView.java
index 1e92e4a..1f0e15a 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextView.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextView.java
@@ -37,6 +37,7 @@
import android.widget.TextView;
import androidx.annotation.DrawableRes;
+import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -477,6 +478,15 @@
TextViewCompat.setLineHeight(this, lineHeight);
}
+ @Override
+ public void setLineHeight(int unit, @FloatRange(from = 0) float lineHeight) {
+ if (Build.VERSION.SDK_INT >= 34) {
+ getSuperCaller().setLineHeight(unit, lineHeight);
+ } else {
+ TextViewCompat.setLineHeight(this, unit, lineHeight);
+ }
+ }
+
/**
* See
* {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
@@ -789,7 +799,9 @@
@RequiresApi(api = 26)
SuperCaller getSuperCaller() {
if (mSuperCaller == null) {
- if (Build.VERSION.SDK_INT >= 28) {
+ if (Build.VERSION.SDK_INT >= 34) {
+ mSuperCaller = new SuperCallerApi34();
+ } else if (Build.VERSION.SDK_INT >= 28) {
mSuperCaller = new SuperCallerApi28();
} else if (Build.VERSION.SDK_INT >= 26) {
mSuperCaller = new SuperCallerApi26();
@@ -817,6 +829,9 @@
// api 28
void setFirstBaselineToTopHeight(@Px int firstBaselineToTopHeight);
void setLastBaselineToBottomHeight(@Px int lastBaselineToBottomHeight);
+
+ // api 34
+ void setLineHeight(int unit, @FloatRange(from = 0) float lineHeight);
}
@RequiresApi(api = 26)
@@ -878,6 +893,9 @@
@Override
public void setLastBaselineToBottomHeight(int lastBaselineToBottomHeight) {}
+
+ @Override
+ public void setLineHeight(int unit, float lineHeight) {}
}
@RequiresApi(api = 28)
@@ -893,4 +911,12 @@
AppCompatTextView.super.setLastBaselineToBottomHeight(lastBaselineToBottomHeight);
}
}
+
+ @RequiresApi(api = 34)
+ class SuperCallerApi34 extends SuperCallerApi28 {
+ @Override
+ public void setLineHeight(int unit, float lineHeight) {
+ AppCompatTextView.super.setLineHeight(unit, lineHeight);
+ }
+ }
}
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index b85e5f0..16f2c54 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -2389,6 +2389,7 @@
public class TypedValueCompat {
method public static float deriveDimension(int, float, android.util.DisplayMetrics);
method public static float dpToPx(float, android.util.DisplayMetrics);
+ method public static int getUnitFromComplexDimension(int);
method public static float pxToDp(float, android.util.DisplayMetrics);
method public static float pxToSp(float, android.util.DisplayMetrics);
method public static float spToPx(float, android.util.DisplayMetrics);
@@ -4156,6 +4157,7 @@
method public static void setFirstBaselineToTopHeight(android.widget.TextView, @IntRange(from=0) @Px int);
method public static void setLastBaselineToBottomHeight(android.widget.TextView, @IntRange(from=0) @Px int);
method public static void setLineHeight(android.widget.TextView, @IntRange(from=0) @Px int);
+ method public static void setLineHeight(android.widget.TextView, int, @FloatRange(from=0) float);
method public static void setPrecomputedText(android.widget.TextView, androidx.core.text.PrecomputedTextCompat);
method public static void setTextAppearance(android.widget.TextView, @StyleRes int);
method public static void setTextMetricsParams(android.widget.TextView, androidx.core.text.PrecomputedTextCompat.Params);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index c8b618c..0f6c6a4 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2813,6 +2813,7 @@
public class TypedValueCompat {
method public static float deriveDimension(int, float, android.util.DisplayMetrics);
method public static float dpToPx(float, android.util.DisplayMetrics);
+ method public static int getUnitFromComplexDimension(int);
method public static float pxToDp(float, android.util.DisplayMetrics);
method public static float pxToSp(float, android.util.DisplayMetrics);
method public static float spToPx(float, android.util.DisplayMetrics);
@@ -4657,6 +4658,7 @@
method public static void setFirstBaselineToTopHeight(android.widget.TextView, @IntRange(from=0) @Px int);
method public static void setLastBaselineToBottomHeight(android.widget.TextView, @IntRange(from=0) @Px int);
method public static void setLineHeight(android.widget.TextView, @IntRange(from=0) @Px int);
+ method public static void setLineHeight(android.widget.TextView, int, @FloatRange(from=0) float);
method public static void setPrecomputedText(android.widget.TextView, androidx.core.text.PrecomputedTextCompat);
method public static void setTextAppearance(android.widget.TextView, @StyleRes int);
method public static void setTextMetricsParams(android.widget.TextView, androidx.core.text.PrecomputedTextCompat.Params);
diff --git a/core/core/src/main/java/androidx/core/util/TypedValueCompat.java b/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
index 49f3bab..55f102d 100644
--- a/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
+++ b/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
@@ -44,6 +44,17 @@
private TypedValueCompat() {}
/**
+ * Return the complex unit type for the given complex dimension. For example, a dimen type
+ * with value 12sp will return {@link TypedValue#COMPLEX_UNIT_SP}.
+ *
+ * @param complexDimension the dimension, typically {@link TypedValue#data}
+ * @return The complex unit type
+ */
+ public static int getUnitFromComplexDimension(int complexDimension) {
+ return TypedValue.COMPLEX_UNIT_MASK & (complexDimension >> TypedValue.COMPLEX_UNIT_SHIFT);
+ }
+
+ /**
* Converts a pixel value to the given dimension, e.g. PX to DP.
*
* <p>This is the inverse of {@link TypedValue#applyDimension(int, float, DisplayMetrics)}
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
index 915d0af2..0e58694 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -54,6 +54,7 @@
import androidx.annotation.DoNotInline;
import androidx.annotation.DrawableRes;
+import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
@@ -806,6 +807,7 @@
* Sets an explicit line height for this TextView. This is equivalent to the vertical distance
* between subsequent baselines in the TextView.
*
+ * @param textView the TextView to modify
* @param lineHeight the line height in pixels
*
* @see TextView#setLineSpacing(float, float)
@@ -827,6 +829,38 @@
}
/**
+ * Sets an explicit line height to a given unit and value for the TextView. This is equivalent
+ * to the vertical distance between subsequent baselines in the TextView. See {@link
+ * TypedValue} for the possible dimension units.
+ *
+ * @param textView the TextView to modify
+ * @param unit The desired dimension unit. SP units are strongly recommended so that line height
+ * stays proportional to the text size when fonts are scaled up for accessibility.
+ * @param lineHeight The desired line height in the given units.
+ *
+ * @see TextView#setLineSpacing(float, float)
+ * @see TextView#getLineSpacingExtra()
+ *
+ * @attr ref android.R.styleable#TextView_lineHeight
+ */
+ public static void setLineHeight(
+ @NonNull TextView textView,
+ int unit,
+ @FloatRange(from = 0) float lineHeight
+ ) {
+ if (Build.VERSION.SDK_INT >= 34) {
+ Api34Impl.setLineHeight(textView, unit, lineHeight);
+ } else {
+ float lineHeightPx = TypedValue.applyDimension(
+ unit,
+ lineHeight,
+ textView.getResources().getDisplayMetrics()
+ );
+ setLineHeight(textView, Math.round(lineHeightPx));
+ }
+ }
+
+ /**
* Gets the parameters for text layout precomputation, for use with
* {@link PrecomputedTextCompat}.
*
@@ -1283,4 +1317,20 @@
return DecimalFormatSymbols.getInstance(locale);
}
}
+
+ @RequiresApi(34)
+ static class Api34Impl {
+ private Api34Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ public static void setLineHeight(
+ @NonNull TextView textView,
+ int unit,
+ @FloatRange(from = 0) float lineHeight
+ ) {
+ textView.setLineHeight(unit, lineHeight);
+ }
+ }
}