androidkotlinandroid-jetpack-composezoominghorizontal-pager

Zoom image outside bounders of JetpackCompose HorizontalPager


I'm trying to implement zooming an image using PointerEvent (2 fingers) and it's working perfectly (Zooming out of column bounders which is 0.4% of the screen height) when wrapped in a Column but it's not working as expected if it wrapped in a HorizontalPager. Original image

Code with screenshot for the Column implementation:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ProductImageDialog(
    properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
    onDismissRequest: () -> Unit
) {
    var scale by remember { mutableFloatStateOf(1f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    val coroutineScope = rememberCoroutineScope()

    var userScrollEnabled by remember { mutableStateOf(true) }

    val pagerState = rememberPagerState { 2 }

    Dialog(
        properties = properties,
        onDismissRequest = onDismissRequest
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(Color.Green),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.End
        ) {
            IconButton(onClick = onDismissRequest) {
                Icon(
                    imageVector = Icons.Default.Close,
                    tint = Color.White,
                    contentDescription = null
                )
            }

            Spacer(modifier = Modifier.height(8.dp))

            Column(
                modifier = Modifier
                    .background(color = Color.White)
                    .fillMaxWidth()
                    .fillMaxHeight(0.4f),
            ) {

//                items(2) { page ->
                    Image(
                        painter = painterResource(id = R.drawable.image_1),
                        contentDescription = null,
                        modifier = Modifier
                            .fillMaxSize()
                            .pointerInput(Unit) {
                                coroutineScope
                                    .launch {
                                        awaitPointerEventScope {
                                            while (true) {
                                                val event = awaitPointerEvent()
                                                when (event.changes.size) {
                                                    2 -> {
                                                        userScrollEnabled = false
                                                        val change1 = event.changes[0]
                                                        val change2 = event.changes[1]

                                                        val distanceCurrent = calculateDistance(
                                                            change1.position,
                                                            change2.position
                                                        )
                                                        val distancePrevious = calculateDistance(
                                                            change1.previousPosition,
                                                            change2.previousPosition
                                                        )
                                                        scale *= distanceCurrent / distancePrevious
                                                        offset += change1.positionChange()
                                                        Log.i(
                                                            "TAGTTTTTT",
                                                            "distanceCurrent: $distanceCurrent\ndistancePrevious: $distancePrevious\nscale: $scale\noffset: $offset"
                                                        )
                                                    }

                                                    else -> {
                                                        userScrollEnabled = true
                                                        scale = 1f
                                                        offset = Offset.Zero
                                                    }
                                                }
                                            }
                                        }
                                    }
                            }
                            .graphicsLayer(
                                scaleX = scale,
                                scaleY = scale,
                                translationX = offset.x,
                                translationY = offset.y
                            ),
                        contentScale = ContentScale.Inside
                    )
//                }

            }
        }
    }
}

private fun calculateDistance(point1: Offset, point2: Offset): Float {
    val dx = point1.x - point2.x
    val dy = point1.y - point2.y
    return sqrt(dx * dx + dy * dy)
}

Zommed in image Column Implemenation Here is the code using HorizontalPager:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ProductImageDialog(
    properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
    onDismissRequest: () -> Unit
) {
    var scale by remember { mutableFloatStateOf(1f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    val coroutineScope = rememberCoroutineScope()

    var userScrollEnabled by remember { mutableStateOf(true) }

    val pagerState = rememberPagerState { 2 }

    Dialog(
        properties = properties,
        onDismissRequest = onDismissRequest
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(Color.Green),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.End
        ) {
            IconButton(onClick = onDismissRequest) {
                Icon(
                    imageVector = Icons.Default.Close,
                    tint = Color.White,
                    contentDescription = null
                )
            }

            Spacer(modifier = Modifier.height(8.dp))

            HorizontalPager(
                modifier = Modifier
                    .background(color = Color.White)
                    .fillMaxWidth()
                    .fillMaxHeight(0.4f),
                state = pagerState,
                userScrollEnabled = userScrollEnabled,
            ) { page ->

                Image(
                    painter = if (page == 0) painterResource(id = R.drawable.image_1) else painterResource(id = R.drawable.image_2),
                    contentDescription = null,
                    modifier = Modifier
                        .fillMaxSize()
                        .pointerInput(Unit) {
                            coroutineScope
                                .launch {
                                    awaitPointerEventScope {
                                        while (true) {
                                            val event = awaitPointerEvent()
                                            when (event.changes.size) {
                                                2 -> {
                                                    userScrollEnabled = false
                                                    val change1 = event.changes[0]
                                                    val change2 = event.changes[1]

                                                    val distanceCurrent = calculateDistance(
                                                        change1.position,
                                                        change2.position
                                                    )
                                                    val distancePrevious = calculateDistance(
                                                        change1.previousPosition,
                                                        change2.previousPosition
                                                    )
                                                    scale *= distanceCurrent / distancePrevious
                                                    offset += change1.positionChange()
                                                    Log.i(
                                                        "TAGTTTTTT",
                                                        "distanceCurrent: $distanceCurrent\ndistancePrevious: $distancePrevious\nscale: $scale\noffset: $offset"
                                                    )
                                                }

                                                else -> {
                                                    userScrollEnabled = true
                                                    scale = 1f
                                                    offset = Offset.Zero
                                                }
                                            }
                                        }
                                    }
                                }
                        }
                        .graphicsLayer(
                            scaleX = scale,
                            scaleY = scale,
                            translationX = offset.x,
                            translationY = offset.y
                        ),
                    contentScale = ContentScale.Inside
                )
            }
        }
    }
}

Zoomed in image HorizontalPager implementation


Solution

  • I found the solution by adding a zooming modifier to the HorizontalPager not the Image.

    Code:

    @OptIn(ExperimentalFoundationApi::class)
    @Composable
    fun ProductImageDialog(
        properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
        onDismissRequest: () -> Unit
    ) {
        var scale by remember { mutableFloatStateOf(1f) }
        var offset by remember { mutableStateOf(Offset.Zero) }
        val coroutineScope = rememberCoroutineScope()
    
        var userScrollEnabled by remember { mutableStateOf(true) }
    
        val pagerState = rememberPagerState { 2 }
    
        Dialog(
            properties = properties,
            onDismissRequest = onDismissRequest
        ) {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Green),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.End
            ) {
                IconButton(onClick = onDismissRequest) {
                    Icon(
                        imageVector = Icons.Default.Close,
                        tint = Color.White,
                        contentDescription = null
                    )
                }
    
                Spacer(modifier = Modifier.height(8.dp))
    
                HorizontalPager(
                    modifier = Modifier
                        .background(color = Color.White)
                        .fillMaxWidth()
                        .fillMaxHeight(0.4f)
                        .pointerInput(Unit) {
                            coroutineScope
                                .launch {
                                    awaitPointerEventScope {
                                        while (true) {
                                            val event = awaitPointerEvent()
                                            when (event.changes.size) {
                                                2 -> {
                                                    userScrollEnabled = false
                                                    val change1 = event.changes[0]
                                                    val change2 = event.changes[1]
    
                                                    val distanceCurrent = calculateDistance(
                                                        change1.position,
                                                        change2.position
                                                    )
                                                    val distancePrevious = calculateDistance(
                                                        change1.previousPosition,
                                                        change2.previousPosition
                                                    )
                                                    scale *= distanceCurrent / distancePrevious
                                                    offset += change1.positionChange()
                                                    Log.i(
                                                        "TAGTTTTTT",
                                                        "distanceCurrent: $distanceCurrent\ndistancePrevious: $distancePrevious\nscale: $scale\noffset: $offset"
                                                    )
                                                }
    
                                                else -> {
                                                    userScrollEnabled = true
                                                    scale = 1f
                                                    offset = Offset.Zero
                                                }
                                            }
                                        }
                                    }
                                }
                        }
                        .graphicsLayer(
                            scaleX = scale,
                            scaleY = scale,
                            translationX = offset.x,
                            translationY = offset.y
                        ),
                    state = pagerState,
                    userScrollEnabled = userScrollEnabled,
                ) { page ->
    
                    Image(
                        painter = if (page == 0) painterResource(id = R.drawable.image_1) else painterResource(
                            id = R.drawable.image_2
                        ),
                        contentDescription = null,
                        modifier = Modifier
                            .fillMaxSize(),
                        contentScale = ContentScale.Inside
                    )
                }
            }
        }
    }
    
    private fun calculateDistance(point1: Offset, point2: Offset): Float {
        val dx = point1.x - point2.x
        val dy = point1.y - point2.y
        return sqrt(dx * dx + dy * dy)
    }