Reading font size from resource file with Jetpack Compose [duplicate] - android-jetpack-compose

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
)

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))

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.

It happens because the function dimensionResource returns a Dp value and fontSize works with Sp values.
Currently you can't use it.

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

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))

Related

Why isn't conversion from dp to px precise?

I'm trying to draw a black rectangle that covers the yellow Box composable, After converting the size to pixels. The Box is still slightly visible underneath. Is there a way around this?
val size = 50.dp
Box(
modifier = Modifier
.size(size)
.background(color = Color.Yellow)
.drawWithContent {
val sizePx = size.toPx()
drawRect(
color = Color.Black,
size = Size(width = sizePx, height = sizePx)
)
}
)
Dp to px conversion is dp.value * density and it's precise. However Modifier.layout or Layout returns dimensions in Int while conversion dpSize.toPx() and you can use dpSize.roundToPx() for Int conversion.
On my device
var text by remember { mutableStateOf("") }
val dpSize = 50.dp
Box(
modifier = Modifier
.size(dpSize)
.background(color = Color.Yellow)
.drawWithContent {
val sizePx = dpSize.toPx()
text = "drawWithContent: density: $density, dpSize in px: $sizePx, drawScope size: ${size.width}\n"
drawRect(
color = Color.Black,
size = Size(width = sizePx, height = sizePx),
)
}
)
Text(text = text)
When you draw anything inside DrawScope if you are going to draw anything that covers your Composable you don't need to pass a size.
You can use DrawScope.size. Same goes for pointerEvents either. inside pointer scope and draw scope you can get size without any extra work.
The issue you faced might be a preview bug. Even if it wasn't precise size you get from conversion is bigger than Composable size

how to progressively add drawable to a canvas?

I have points generated one by one, and when a new point is generated, I want to draw a line segment connecting with the previous point. Like this:
var x by remember { mutableStateOf( 0.0f)}
var y by remember { mutableStateOf( 0.5f)}
var pStart by remember { mutableStateOf(Offset(0f, 0.5f))}
Canvas(modifier = Modifier.fillMaxSize()) {
canvasWidth = size.width
canvasHeight = size.height
val pEnd = Offset(x * canvasWidth, (1-y) * canvasHeight)
val col = if (pEnd.y < pStart.y) Color.Green else Color.Red
drawLine(
start = pStart,
end = pEnd,
strokeWidth = 4f,
color = col
)
pStart = pEnd
}
But this only draws the segment in a flash and no segments stay on the screen.
I know I can save the points to a list and redraw all the segments whenever a new point is added. But I just hope to economize. Is it possible?
There's no practical other way. You COULD in fact, keep track of just two points, adding a whole new Canvas (all Transparent and Filling the maximum Size, stacked on top of one another), for each extra point that is added. This does seem a bit impractical, but maybe try it out and do some benchmarking to see which one checks out. This is the only OTHER way I could think of, where you do not have to store all the points and recompose every time a point is added, since all the other lines would technically be frozen in space.
In response to the somewhat (unreasonably) aggressive comment below, here's some sample code. I assume you have a stream of new points coming in so a LiveData object is assumed to be the source of that, which I shall be converting to a MutableState<T> for my use-case.
var latestPoint by liveData.collectAsState()
var recordedPoint by remember { mutableStateOf(latestPoint) }
var triggerDraw by remember { mutableStateOf(false) }
var canvasList = mutableStateListOf<#Composable () -> Unit>Canvas>() // Canvas is the Composable
if(triggerDraw){
canvasList.add(
Canvas(){
/* you have the recordedPoint, and latestPoint, simply draw a line here */
}
)
triggerDraw = false
}
LaunchedEffect(latestPoint){
triggerDraw = true
}
canvasList.forEach {
it() // Invoke the Composable here
}
Thanks Dad!

Jetpack Compose: Find how many lines a text will take before composition

I am trying to determine how many lines a certain text will occupy on the screen before composition. Is there a way to do this?
You can use onTextLayout on Text to get line count and some other features.
var lineCount = 1
Text(text= "", onTextLayout = {textLayoutResult: TextLayoutResult ->
lineCount = textLayoutResult.lineCount
})
While the accepted answer is correct there is an alternative way, which doesn't even require a Composable function:
val paragraph = androidx.compose.ui.text.Paragraph(
text = "Foo",
style = MaterialTheme.typography.body1,
constraints = Constraints(maxWidth = maxWidthInPx),
density = LocalDensity.current,
fontFamilyResolver = LocalFontFamilyResolver.current,
)
paragraph.lineCount
This might be better suited if it's required to know the lineCount beforehand.

How to add Tuples and apply a ceiling/clamp function in F#

So I am working on a project using F# for some SVG line manipulations.
I thought it would be good to represent color an RGB value as a tuple (R,G,B). It just made sense to me. Well since my project involves generating SVG lines in a loop. I decided to have a color offset, conveniently also represented in a tuple (Roffset, Goffset, Boffset)
An offset in this case represents how much each line differs from the previous.
I got to a point where I needed to add the tuples. I thought since they were of the same dimensions and types, it would be fine. But apparently not. I also checked the MSDN on tuples, but I did not find anything about how to add them or combine them.
Here is what I tried. Bear in mind I tried to omit as much irrelevant code as possible since this is a long class definition with LOTS of members.
type lineSet ( 10+ params omitted ,count, colorOff :byte*byte*byte, color :byte*byte*byte ,strokeWid , strokeWidthOff ) =
member val Color = color with get, set
member val ColorOffset = colorOff with get, set
member val lineCount = count with get, set
interface DrawingInterfaces.IRepresentable_SVG with
member __.getSVGRepresenation() =
let mutable currentColor = __.Color
for i in 1..__.lineCount do
currentColor <- currentColor + __.ColorOffset
That last line of code is what I wanted to do. However, it appears you cannot add tuples directly.
I also need a way to clamp the result so it cannot go over 255, but I suspect a simple try with block will do the trick. OR I could let the params take a type int*int*int and just use an if to reset it back to 255 each time.
As I mentioned in the comments, the clamping function in your code does not actually work - you need to convert the numbers to integers before doing the addition (and then you can check if the integer is greater than 255). You can do something like this:
let addClamp (a:byte) (b:byte) =
let r = int a + int b
if r > 255 then 255uy else byte r
Also, if you work with colors, then it might make sense to define a custom color type rather than passing colors around as tuples. That way, you can also define + on colors (with clamping) and it will make your code simpler (but still, 10 constructor arguments is a bit scary, so I'd try to think if there is a way to simplify that a bit). A color type might look like this:
type Color(r:byte, g:byte, b:byte) =
static let addClamp (a:byte) (b:byte) =
let r = int a + int b
if r > 255 then 255uy else byte r
member x.R = r
member x.B = b
member x.G = g
static member (+) (c1:Color, c2:Color) =
Color(addClamp c1.R c2.R, addClamp c1.G c2.G,addClamp c1.B c2.B)
Using the type, you can then add colors pretty easily and do not have to add clamping each time you need to do that. For example:
Color(255uy, 0uy, 0uy) + Color(1uy, 0uy, 0uy)
But I still think you could make the code more readable and more composable by refactoring some of the visual properties (like stroke & color) to a separate type and then just pass that to LineSet. This way you won't have 10+ parameters to a constructor and your code will probably be more flexible too.
Here is a modified version of your code which I think is a bit nicer
let add3DbyteTuples (tuple1:byte*byte*byte , tuple2:byte*byte*byte) =
let inline intify (a,b,c) = int a,int b,int c
let inline tripleadd (a,b,c) (d,e,f) = a+d,b+e,c+f
let clamp a = if a > 255 then 255 else a
let R,G,B = tripleadd (intify tuple1) (intify tuple2)
clamp R,clamp G,clamp B

Is there an easy way to compare how close two colors are to each other?

Is there a way to compare how close two colors are to each other? If to say both of them are blue.
At the moment the way that we compare them is to manually assign each possible color to a color family(red, green, blue...). And then just compare the strings :)
But surely that manual task can be assigned to a neat little algorithm.
You probably want to convert the colors to an HSL model (Hue, Saturation, Lightness) and then compare the values within thresholds in the order HSL. If the hue is within a tolerance deemed as "close", then check the "closeness" of the saturation, and then the lightness.
Delta-e, is a single number that represents the perceived 'distance' between two colors. The lower the number, the more similar the colors are to the human eye.
There are a few different ways to calculate it...CIE76 (aka CIE 1976 or dE76) being the most popular.
CIE76
CMC l:c
dE94
dE2000
Each one goes about things in a different way, but for the most part they all require you to convert to a better (for comparison) color model than RGB.
For CIE76 you basically just convert your colors to the LAB color space, then compute the 3 dimensional distance between them.
Wikipedia has all the formulae: http://en.wikipedia.org/wiki/Color_difference
You can check your work with online color calculators:
CIE76
CMC l:c
I'm not sure of any algorithms, you may want to consider converting RGB (Red, Green, Blue) values in to HSB (Hue, Saturation, Brightness).
Hue is essentially "color", so you can compare simply on how close the Hue values are.
See http://en.wikipedia.org/wiki/HSV_color_space
I know this question is 10 years old but extending Joe Zack's answer:
Here is my Kotlin code
//Entry point here
//Color must be hexa for example "#829381"
fun calculateColorDistance(colorA: String, colorB: String): Double {
val aColorRGBArray = getColorRGBArray(colorA)
val bColorRGBArray = getColorRGBArray(colorB)
val aColorLAB = getColorLab(aColorRGBArray)
val bColorLAB = getColorLab(bColorRGBArray)
return calculateColorDistance(aColorLAB, bColorLAB)
}
private fun calculateColorDistance(aColorLAB: DoubleArray, bColorLAB: DoubleArray): Double {
val lab = aColorLAB[0] - bColorLAB[0]
val aab = aColorLAB[1] - bColorLAB[1]
val bab = aColorLAB[2] - bColorLAB[2]
val sqrtlab = lab.pow(2)
val sqrtaab = aab.pow(2)
val sqrtbab = bab.pow(2)
val sum = sqrtlab + sqrtaab + sqrtbab
return sqrt(sum)
}
private fun getColorRGBArray(color: String): IntArray {
val cleanColor = color.replace("#", "")
val colorInt = Integer.parseInt(cleanColor, 16)
val r = Color.red(colorInt)
val g = Color.green(colorInt)
val b = Color.blue(colorInt)
return intArrayOf(r, g, b)
}
private fun getColorLab(colorRGB: IntArray): DoubleArray {
val outLab = doubleArrayOf(0.0,0.0,0.0)
ColorUtils.RGBToLAB(colorRGB[0], colorRGB[1], colorRGB[2], outLab)
return outLab
}
calculateColorDistance will return a Double value. the lower this value is the more similar the colors are.
Hope this helps someone

Resources