androidandroid-jetpack-composecompose-recompositionandroid-jetpack-compose-gesture

Value of MutableState inside Modifier.pointerInput doesn't change after remember keys updated


I have an offset value that is calculated or reset as

var offset by remember(key1 = contentScale, key2 = imageBitmap) {
    mutableStateOf(Offset.Unspecified)
}

This is basically a color detector from an Image but when ContentScale of the custom Image I wrote or ImageBitmap changes I want to reset it to Offset.Unspecified so it doesn't draw markers outside of area that is image being drawn, the grey area in gif.

enter image description here

This is simplified but reproducible code, I use Jetpack Compose 1.2.0-beta01

@Composable
fun ImageColorDetector(
    modifier: Modifier = Modifier,
    imageBitmap: ImageBitmap,
    contentScale: ContentScale = ContentScale.FillBounds,
    onColorChange: (ColorData) -> Unit
) {

    var offset by remember(key1 = contentScale, key2 = imageBitmap) {
        mutableStateOf(Offset.Unspecified)
    }

    println(
        "✊ImageColorDetector() imageBitmap: $imageBitmap\n" +
                "bitmap: ${imageBitmap.asAndroidBitmap()}\n" +
                "width: ${imageBitmap.width}, height: ${imageBitmap.height}\n" +
                "offset: $offset\n" +
                "contentScale: $contentScale\n\n"
    )

    Box(
        modifier = modifier
            .pointerInput(Unit) {
                detectDragGestures { change, dragAmount ->
                    offset = change.position
                    println("onTouchEvent:  offset: $offset")
                }
            }
    ) {
        Image(
            bitmap = imageBitmap,
            contentScale = contentScale,
            contentDescription = null
        )
    }
    Text("Offset: $offset")
}

In detectDragGestures I see that offset is updated on drag gesture and when ImageBitmap or ContentScale changes, I saw in parent and this composable that they are not the same instances, so remember should be recalculate with new keys but it doesn't seem to work.

I/System.out: ⛺️ ImageColorDetectionDemo  imageBitmap: androidx.compose.ui.graphics.AndroidImageBitmap@5552c68
I/System.out: bitmap: android.graphics.Bitmap@93bc881
I/System.out: width: 236, height: 394
I/System.out: contentScale: androidx.compose.ui.layout.ContentScale$Companion$Crop$1@3226649
I/System.out: 
I/System.out: ✊ImageColorDetector() imageBitmap: androidx.compose.ui.graphics.AndroidImageBitmap@5552c68
I/System.out: bitmap: android.graphics.Bitmap@93bc881
I/System.out: width: 236, height: 394
I/System.out: offset: Offset(0.0, 0.0)
I/System.out: contentScale: androidx.compose.ui.layout.ContentScale$Companion$Crop$1@3226649
I/System.out: 
I/System.out: ⛺️ ImageColorDetectionDemo  imageBitmap: androidx.compose.ui.graphics.AndroidImageBitmap@7607f98
I/System.out: bitmap: android.graphics.Bitmap@286d3f1
I/System.out: width: 736, height: 920
I/System.out: contentScale: androidx.compose.ui.layout.ContentScale$Companion$Crop$1@3226649
I/System.out: 
I/System.out: ✊ImageColorDetector() imageBitmap: androidx.compose.ui.graphics.AndroidImageBitmap@7607f98
I/System.out: bitmap: android.graphics.Bitmap@286d3f1
I/System.out: width: 736, height: 920
I/System.out: offset: Offset(0.0, 0.0)
I/System.out: contentScale: androidx.compose.ui.layout.ContentScale$Companion$Crop$1@3226649

ContentScale is from default image code; each is an Object, so changing from Crop to Fit means a key is set.


Solution

  • I changed

    modifier
        .pointerInput(Unit) {
            detectDragGestures { change, dragAmount ->
                offset = change.position
                println("onTouchEvent:  offset: $offset")
            }
        }
    

    to

    modifier
        .pointerInput(contentScale, imageBitmap) {
            detectDragGestures { change, dragAmount ->
                offset = change.position
                println("onTouchEvent:  offset: $offset")
            }
        }
    

    When a [pointerInput] modifier is created by composition, if [block] captures any local variables to operate on, two patterns are common for working with changes to those variables depending on the desired behavior.
    Specifying the captured value as a [key][key1] parameter will cause [block] to cancel and restart from the beginning if the value changes:

    Modifier.pointerInput holds to previous MutableState instance while new instance of MutableState is created when any remember keys change and Modifier.pointerInput has no way accessing. It still hold previous instance and gesture loop runs with old MutableState with no updates.Restarting pointerInput with keys makes sure of the latest MutableState is read.