I’ve a formulary app with a lot of different input...
# compose
g
I’ve a formulary app with a lot of different input types (text, radio, check, signature, etc...) I can zoom in/out and drag the form around. I’ve notice performance issues (lots of lag when zooming and panning) when doing this:
Copy code
inputs.forEach {
   TextFieldWrapper(
       id = ...,
       text = ...,
       onTextChanged = { my callback implementation }
   )
   ...
}
More in thread.
Where:
Copy code
@Composable
fun TextFieldWrapper(
    id: String,
    text: String,
    ...
    onTextChanged: ((id: String, text: String) -> Unit)? = null
) {
 	...
	BasicTextField(
	    value = input,
	    onValueChange = {
           input = it
           onTextChanged?.invoke(id, it.text)
       },
	    ...
	)
}
But if instead, I define the callback like this:
Copy code
val onInputTextChanged: (id: String, text: String) -> Unit = remember(key) { { id, text -> ... } }
inputs.forEach {
   TextFieldWrapper(
       id = ...,
       text = ...,
       onTextChanged = onInputTextChanged
   )
   ...
}
The performance issues appear to be gone. Any thoughts on this? I mean it makes sense, but I would like to hear from you.
z
Zooming and panning alone, if you’re using
graphicsLayer
, shouldn’t trigger any recomposition or layout or text events. Can you share your transformation code?
g
Yes, here it goes:
Copy code
val imageModifier = remember(fillHeight) {
        Modifier.onGloballyPositioned { coordinates ->
            imageTopLeft = coordinates.boundsInRoot().topLeft
            imageBottomRight = coordinates.boundsInRoot().bottomRight
            if (imageDrawnSize == IntSize.Zero) {
                imageDrawnSize = coordinates.size
            }
        }
    }

...
Box(modifier = Modifier
        .fillMaxSize()
        .background(Color.LightGray.copy(alpha = 0.4f))
        .pointerInput(gesturesEnabled) {
            if (gesturesEnabled) {
                detectTransformGestures { _, pan, zoom, _ ->
                    val new = scale * zoom
                    scale = when {
                        new < 1f -> 1f
                        new > 3f -> 3f
                        else -> new
                    }
                    if (scale > 1) {
                        translateOffset += pan
                        val inBounds = imageTopLeft.x <= 0f && imageTopLeft.y <= 0f &&
                                imageBottomRight.x >= imageDrawnSize.width.toFloat() && imageBottomRight.y >= imageDrawnSize.height.toFloat()
                        if (!inBounds) {
                            var deltaX = 0f
                            if (imageTopLeft.x > 0f) {
                                deltaX = -imageTopLeft.x
                            } else if (imageBottomRight.x < imageDrawnSize.width.toFloat()) {
                                deltaX = imageDrawnSize.width.toFloat() - imageBottomRight.x
                            }
                            var deltaY = 0f
                            if (imageTopLeft.y > 0f) {
                                deltaY = -imageTopLeft.y
                            } else if (imageBottomRight.y < imageDrawnSize.height.toFloat()) {
                                deltaY = imageDrawnSize.height.toFloat() - imageBottomRight.y
                            }
                            translateOffset = Offset(translateOffset.x + deltaX, translateOffset.y + deltaY)
                        }
                    } else {
                        translateOffset = Offset(0f, 0f)
                    }
                }
            }
        }
        .graphicsLayer(scaleX = scale, scaleY = scale, translationX = translateOffset.x, translationY = translateOffset.y)
    )
The form has a .png image as background, and the fields will be drawn on top of the image
z
You always want to use the lambda form of
graphicsLayer
for stuff like this so that the layer can be adjusted during animation/gestures without recomposing.
👍 1
Also, unrelated but it looks like an antipattern that your BasicTextField is sending text events to its caller but also maintaining its own copy of what it thinks the text should be. Whatever is receiving your 2-param onTextChanged events should probably be the source of truth for the text field contents.
g
yup using the lambda form solves it. but since now it doesn’t trigger recomposition, for instance if you have a piece of text selected the manual selectors wont update their position if we drag the form
regarding the BasicTextField, the source of truth is being changed by the callback, but the element is not being recomposed upon text change, only if it gains focus or the form is dragged. That’s why I also have the
input = it
, but in the end, if recomposition is triggered, the source of truth will provide the value. Dunno if I explained myself 😅