21

When I plug in fontSize = dimensionResource(id = R.dimen.textLabelTextSize) where the dimens or 54sp or 60sp depending on the device, I get an error on Text() "None of the following functions can be called with the arguments supplied." But when I put a hard-coded value like 54sp it's fine. What's strange is for the padding modifier dimensionResource (in dp) is working fine.

       Text(
                text = textLabelItem.textLabel,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(id = R.dimen.textLabelPaddingVertical),
                        top = dimensionResource(id = R.dimen.textLabelPaddingHorizontalTop),
                        end = dimensionResource(id = R.dimen.textLabelPaddingVertical),
                        bottom = dimensionResource(id = R.dimen.textLabelPaddingHorizontalBottom)
                    )
                    .background(colorResource(id = R.color.textLabelBg))
                    .border(
                        width = 2.dp,
                        color = colorResource(id = R.color.textLabelBorder),
                        shape = RoundedCornerShape(8.dp)
                    ),
                color = colorResource(id = android.R.color.background_dark),
                fontSize = dimensionResource(id = R.dimen.textLabelTextSize),
                fontWeight = FontWeight.Bold
            )
galaxigirl
  • 2,390
  • 2
  • 20
  • 29

6 Answers6

17

The answer is very simple, you just forgot to handle the result from dimensionResource. You need to just use the value of it to have it as float. Then you use sp extension and you are ready to go.

I created my own extension for this:

@Composable
@ReadOnlyComposable
fun fontDimensionResource(@DimenRes id: Int) = dimensionResource(id = id).value.sp

So instead using dimensionResource(R.dimen.your_font_size) use fontDimensionResource(R.dimen.your_font_size)

Final solution:

Text(text = "", fontSize = fontDimensionResource(id = R.dimen.your_font_size))
TheComposeGuy
  • 219
  • 1
  • 3
  • 5
    This does not work. When the user changes default device text size, this does not change the text size. – Richard Le Mesurier May 19 '22 at 06:45
  • I have compared same dimension sizes one hardcoded (20.sp) and the same value from dimensionResource and it looks different. Unfortunately this is a wrong solution. Check my answer below. – aleksandrbel Jun 05 '23 at 02:38
10

To convert from dp to sp, you need to take into account font scaling - that is the point of using sp for text. This means when the user changes the system font scale, that the app responds to this change.


Does not scale the text

If we request dimensionResource() in kotlin, we get a dp value that is not scaled yet. You can confirm this in the sourcecode where that function is defined to return a Dp:

fun dimensionResource(@DimenRes id: Int): Dp {.....}

A basic conversion to a value.sp does not apply the required scaling, so any solution relying on this type of basic calculation will not work correctly.

unscaledSize = dimensionResource(R.dimen.sp_size).value.sp

(where R.dimen.sp_size is a dimension resource declared with sp sizing)

This does not scale the text size correctly.


Better solution

To do it correctly, we need to look at the DisplayMetrics and the current scaledDensity value, defined as:

/**
* A scaling factor for fonts displayed on the display.  This is the same
* as {@link #density}, except that it may be adjusted in smaller
* increments at runtime based on a user preference for the font size.
*/
public float scaledDensity;

This scaling value must be applied to the dimension that is fetched, to return something that can be used as sp:

val scaledSize = with(LocalContext.current.resources) {
    (getDimension(R.dimen.sp_size) / displayMetrics.scaledDensity).sp
}

Warning: this will only work correctly for dimensions defined as sp!


Handling different dimension types

An even better solution would check what type of dimension resource is being accessed, and would then calculate based on that i.e. dp, sp or px.

This does require working with TypedValue and TypedArray, which makes it a bit more complex, but sample code can be found in the TypedArrayUtils from the MDC Theme Adapter:

internal fun TypedArray.getTextUnitOrNull(
    index: Int,
    density: Density
): TextUnit? {
    val tv = tempTypedValue.getOrSet { TypedValue() }
    if (getValue(index, tv) && tv.type == TypedValue.TYPE_DIMENSION) {
        return when (tv.complexUnitCompat) {
            // For SP values, we convert the value directly to an TextUnit.Sp
            TypedValue.COMPLEX_UNIT_SP -> TypedValue.complexToFloat(tv.data).sp
            // For DIP values, we convert the value to an TextUnit.Em (roughly equivalent)
            TypedValue.COMPLEX_UNIT_DIP -> TypedValue.complexToFloat(tv.data).em
            // For another other types, we let the TypedArray flatten to a px value, and
            // we convert it to an Sp based on the current density
            else -> with(density) { getDimension(index, 0f).toSp() }
        }
    }
    return null
}

Best solution

Ideally, we should not be pulling out resources and converting them when working with Compose. We should be using theme constants instead.

We are probably all on this page because we have some layouts in XML with others in Compose. We are likely going through the conversion process.

The best way to deal with this type of conversion is to use the Material Components MDC-Android Compose Theme Adapter to handle all of these cases.

It works with much more than just a text size calculation and is where we should be aiming to get to as part of our migration to Compose.

Richard Le Mesurier
  • 29,432
  • 22
  • 140
  • 255
3

It happens because the function dimensionResource returns a Dp value and fontSize works with Sp values.

Currently you can't use it.

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
3

The method dimensionResource returns dp value. To get sp value from this add .value.sp at the end like this:

fontSize = dimensionResource(id = R.dimen.textLabelTextSize).value.sp

prodev
  • 575
  • 5
  • 15
1

Consider creating this

@OptIn(ExperimentalUnitApi::class)
@Composable
@ReadOnlyComposable
fun textSizeResource(@DimenRes id: Int): TextUnit {
    val context = LocalContext.current
    val density = LocalDensity.current
    val pxValue = context.resources.getDimension(id)
    return TextUnit(pxValue / density.density, TextUnitType.Sp)
}

and using it as follows

Text(text = "abc", fontSize = textSizeResource(id = R.dimen.text_large))
humblerookie
  • 4,717
  • 4
  • 25
  • 40
  • to remove `@OptIn(ExperimentalUnitApi::class)` you can change the return line to `return (pxValue / density.density).sp` – mr.kostua Feb 16 '23 at 12:03
0

The correct way to get sp from dimensions is

Text(
    fontSize = with(LocalDensity.current) {
        dimensionResource(R.dimen.textLabelTextSize).toSp()
    }
)

Here is the documentation : https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Density#(androidx.compose.ui.unit.Dp).toSp()

aleksandrbel
  • 1,422
  • 3
  • 20
  • 38