Jetpack Compose: Panning zooming rotating

Introduction

In the world of Android development, creating interactive and responsive user interfaces has always been a challenge, especially when it comes to handling gestures like panning, zooming, and rotating. Jetpack Compose, Android's modern toolkit for building native UI, offers a declarative approach that simplifies implementing such interactions. Leveraging Jetpack Compose’s capabilities, developers can create seamless and intuitive interfaces that respond to user gestures without writing boilerplate code.

This article walks you through an example of how to implement panning, zooming, and rotating gestures on an image using Jetpack Compose. We'll explore the key components and techniques used to make a simple yet effective interactive UI. This guide is designed to help you understand how to integrate gesture-based interactions in your apps, enhancing the user experience while keeping your code clean and efficient.

Breaking Down the Code

The application starts with a typical setup for an Android app using Jetpack Compose. The MainActivity class is where everything begins. Here, instead of using the traditional XML layout files, we utilize setContent to define the user interface directly in Kotlin using Composables. This modern approach allows developers to build dynamic and reactive UIs efficiently.

At the heart of this example is the MainContent composable function. This function serves as the primary UI component where the gesture handling and transformations take place. To allow the image to respond to touch gestures, three mutable state variables are defined: scale for zooming, rotation for rotating, and offset for panning. These variables are tracked and updated as the user interacts with the image, providing a fluid and natural feel to the interactions.

The core of the gesture handling is managed by the rememberTransformableState function. This function creates a state that listens for changes in zoom, offset, and rotation. When a user performs gestures like pinching or dragging, the corresponding changes are captured through the lambda function within rememberTransformableState. The zoomChange variable adjusts the scale, the rotationChange updates the rotation angle, and the offsetChange updates the position of the image. This way, all three gestures are handled simultaneously, giving the user a seamless multitouch experience.

Applying Transformations

Within the MainContent function, the image itself is defined using the Image composable. This composable loads a drawable resource and applies the transformations we defined earlier. The Modifier chain is essential here, allowing us to apply multiple effects to the image. First, the graphicsLayer modifier is used to adjust the scale, rotation, and translation properties based on the state variables. This ensures that any changes made to these variables immediately reflect on the UI.

The transformable modifier, combined with the state created earlier, is what enables the image to respond to touch gestures. By attaching this modifier to the Image composable, we allow the user to interact directly with the image, making it zoomable, pannable, and rotatable. Additionally, a clip modifier is applied to give the image rounded corners, adding a subtle touch of polish to the UI.

To create a visually appealing background, the outer Column composable wraps the entire content with a soft beige color. This gives the application a clean and minimalistic look while ensuring that the interactive image remains the focal point.

Previewing the Composable

In a typical Jetpack Compose project, the @Preview annotation is used to quickly visualize how a composable looks without running the full application on a device or emulator. Here, the ComposablePreview function, though commented out, serves as a placeholder for testing the MainContent composable. By uncommenting it, you can instantly see how the image responds to gestures directly within the Android Studio preview window, making the development process faster and more efficient.

Conclusion

This example demonstrates the power of Jetpack Compose in building gesture-based interactions with minimal code. By using composables and modifiers, developers can create highly interactive UIs that respond smoothly to user input. The use of state management in Jetpack Compose simplifies handling complex gestures like panning, zooming, and rotating, making it an ideal choice for modern Android development.

With this approach, you can easily extend the functionality to include additional gestures or transformations, opening up endless possibilities for creating rich, interactive applications. Whether you're working on a photo gallery, a mapping tool, or any app requiring intuitive user interactions, Jetpack Compose offers the tools to make it happen efficiently and elegantly.


MainActivity.kt

package com.cfsuman.jetpackcompose

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MainContent()
        }
    }


    @Composable
    fun MainContent(){
        var scale by remember { mutableStateOf(1f) }
        var rotation by remember { mutableStateOf(0f) }
        var offset by remember { mutableStateOf(Offset.Zero) }

        val state = rememberTransformableState {
                zoomChange, offsetChange, rotationChange ->
            scale *= zoomChange
            rotation += rotationChange
            offset += offsetChange
        }

        Column(
            Modifier
                .background(Color(0xFFEDEAE0))
                .fillMaxSize()
                .padding(16.dp)
        ) {
            Image(
                painter = painterResource(id = R.drawable.flower9),
                contentDescription = "Localized description",
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .background(Color.Transparent)
                    .fillMaxSize()
                    .graphicsLayer(
                        scaleX = scale,
                        scaleY = scale,
                        rotationZ = rotation,
                        translationX = offset.x,
                        translationY = offset.y
                    )
                    .transformable(state = state)
                    .clip(RoundedCornerShape(16.dp)),
            )
        }
    }


    @Preview
    @Composable
    fun ComposablePreview(){
        //MainContent()
    }
}
More android jetpack compose tutorials