Why isn't conversion from dp to px precise? - android-jetpack-compose

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

Related

Compose zIndex in isometric grid

I have an isometric grid with images and need to make an overlay for each of them. Tried to use Modifier.zIndex recommended in this answer but didn't succeed. So, I decided to pass images second time after my first layer is completed, adding an extra loop. Can it be simplified with zIndex or other methods?
Box {
repeat(2) {
for (y in 0 until gridHeight) {
for (x in 0 until gridWidth) {
Box(
modifier = Modifier.padding(start = start.dp, top = top.dp)
) {
if (it == 0)
itemContent(index, data[index]) //The basic image
else
Box(
modifier = Modifier.size(Dimens.imageWidth.dp),
contentAlignment = BiasAlignment(0f, 0.5f)
)
{
Icon( //The overlay
imageVector = Icons.Filled.Image,
contentDescription = null,
modifier = Modifier.size(32.dp)
)
}
}
}
}
}
}

Why .width() and .height() modifiers apply wrong value?

I want two independent rows to have the same width and scroll simultaneously. I have tried to achieve this in this way:
#Composable
fun TwoRows() {
val scrollState = rememberScrollState()
Column(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().horizontalScroll(scrollState)) {
for (i in 0 until 100) {
Box(Modifier.width(10.dp).height(10.dp).background(Color.Red))
Spacer(Modifier.width(90.dp))
}
}
Row(Modifier.fillMaxWidth().horizontalScroll(scrollState)) {
for (i in 0 until 100) {
Box(Modifier.width(100.dp).height(10.dp).background(Color.Green))
}
}
}
}
The first row consists of 10 dp width red rectangles and 90 dp width spacers. The second row consists of only 100 dp width green rectangles. I'm expecting these rows to have the same width but the layout inspector shows they're not:
Because of this the elements do not match expecting positions:
How can I fix it?
This is happening because each of .size(), width(), .height() modifiers transform incoming value into pixel just in time it applied. I.e. they apply independently - the first modifier converts its value to pixels, and then the second, and so on. There is no guarantee that you get the same dp value after transforming it to px and back because of rounding. Here is how dp->px->dp transformation works for your case:
dp = 10.dp // Incoming dp value
density = 2.625 // Screen density scale value of your emulator
unrounded_px = 10 * density = 26.25
px = 26
restored_dp = 26 / density = 9.904762.dp
Most likely that the LayoutInspector doesn't round computed dp value to the nearest integer, but simply discards the fractional part. I think this is the reason why it shows different value. Such behavior is especially likely to occur when you work with relatively small views on low pixel density screens.
You can achieve desired behavior wrapping your red Box and Spacer into the Box with specified size:
Box(Modifier.width(100.dp).height(10.dp)) {
Box(Modifier.width(10.dp).height(10.dp).background(Color.Red))
Spacer(Modifier.width(90.dp))
}

Is there a way to scale the text fontsize using react konva on transform end

I have the following code below where once the text has been transformed on the screen the font size has gotten bigger but when i log fontsize it is the exact same as before.Is there anyway to get the updated fontsize as when I reload the page after the value have been saved to the db it is the previous font size.
<Text
name={eid}
zIndex={i}
x={x}
y={y}
text={text}
fontSize={fontSize}
fontFamily={fontFamily}
fill={fill}
width={width}
height={height}
align={align}
draggable={draggable}
onDragMove={(e) => {
let transformedData = {
'x' : e.target.x(),
'y' : e.target.y(),
}
socket.emit('element:update',{data:transformedData})
}}
onTransformEnd={(e) => {
let text = e.target
let transformedData = {
'x': text.x(),
'y': text.y(),
'rotation': text.rotation(),
'width': text.width() * text.scaleX(),
'height': text.height() * text.scaleY(),
'fontSize': text.fontSize() //this font size never changes ...not sure why
}
socket.emit('element:updatedata'{data:tranformedData})
}}
/>
When you scale a shape its dimensions do not change. Internally, what happens is that a transformation matrix is applied that changes the scale at which the shape is drawn, but none of the shape attributes other than scaleX() and scaleY() are changed.
A common question concerns how to get the size of the shape on the stage. The answer to that is
shapeSize.width = shape.width() * shape.scaleX();
shapeSize.height= shape.height() * shape.scaleY();
And the same approach would be taken for the font size:
shapeSize.fontSize = textShape.fontSize () * textShape.scaleY();

Reading font size from resource file with Jetpack Compose [duplicate]

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

android jetpack compose rememberInfiniteTransition tween durationMillis >= 90195.The animation will stay at the last stitch

Sorry, I can hardly speak English.
android jetpack compose rememberInfiniteTransition tween durationMillis >= 90195.The animation will stay at the last stitch.
jetpack compose: v1.0.0-alpha12
machine translation:
The animation will stay at the last stitch. DurationMillis less than or equal to 90194 is animation work, but it is very stuck.
/**
* #see rememberInfiniteTransition
* #see updateTransition
*/
val translation by rememberInfiniteTransition().animateValue(
initialValue = 60.dp,
targetValue = (-60).dp,
typeConverter = Dp.VectorConverter,
animationSpec = infiniteRepeatable(
animation = tween(90195, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
)
Image(
painterResource(id = R.drawable.welcome_page_posters),
contentDescription = "",
modifier = Modifier
.fillMaxHeight()
.scale(3.8f)
.offset(x = translation, y = (-5).dp),
)
This is a known issue. It has been fixed and you can expect to get the fix in the beta02 release. :-)

Resources