Cử chỉ

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

Compose cung cấp một loạt API giúp bạn phát hiện các cử chỉ tạo ra khi người dùng tương tác. Có nhiều trường hợp sử dụng cho những API này:

  • Một vài trong số đó là API cấp cao và được thiết kế để xử lý những cử chỉ được sử dụng phổ biến nhất. Ví dụ: phương thức sửa đổi (modifier) clickable cho phép dễ dàng phát hiện một lượt nhấp, đồng thời cung cấp các tính năng hỗ trợ tiếp cận và hiển thị chỉ báo bằng hình ảnh khi người dùng nhấn vào (chẳng hạn như hiệu ứng gợn sóng).

  • Ngoài ra, còn có các trình phát hiện cử chỉ ít được sử dụng nhưng mang lại sự linh hoạt cao hơn ở cấp độ thấp hơn, chẳng hạn như PointerInputScope.detectTapGestures hoặc PointerInputScope.detectDragGestures nhưng không cung cấp các tính năng bổ sung.

Thao tác nhấn và thao tác nhấn và giữ

Phương thức sửa đổi clickable cho phép các ứng dụng phát hiện số lượt nhấp trên phần tử được áp dụng.

@Composable
fun ClickableSample() {
    val count = remember { mutableStateOf(0) }
    // content that you want to make clickable
    Text(
        text = count.value.toString(),
        modifier = Modifier.clickable { count.value += 1 }
    )
}

Ví dụ về thành phần trên giao diện người dùng phản hồi các lần nhấn

Khi cần linh hoạt hơn, bạn có thể cung cấp trình phát hiện cử chỉ nhấn thông qua phương thức sửa đổi pointerInput:

Modifier.pointerInput(Unit) {
    detectTapGestures(
        onPress = { /* Called when the gesture starts */ },
        onDoubleTap = { /* Called on Double Tap */ },
        onLongPress = { /* Called on Long Press */ },
        onTap = { /* Called on Tap */ }
    )
}

Thao tác cuộn

Phương thức sửa đổi thao tác cuộn

Phương thức sửa đổi verticalScrollhorizontalScroll cung cấp cách đơn giản nhất để cho phép người dùng cuộn một phần tử khi các giới hạn của nội dung lớn hơn giới hạn kích thước tối đa. Với phương thức sửa đổi verticalScrollhorizontalScroll, bạn không cần dịch hoặc bù trừ phần nội dung.

@Composable
fun ScrollBoxes() {
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

Một danh sách đơn giản theo chiều dọc phản hồi lại các cử chỉ cuộn

ScrollState cho phép bạn thay đổi vị trí cuộn hoặc xem trạng thái hiện tại của vị trí cuộn. Để tạo lớp này với các tham số mặc định, hãy sử dụng rememberScrollState().

@Composable
private fun ScrollBoxesSmooth() {

    // Smoothly scroll 100px on first composition
    val state = rememberScrollState()
    LaunchedEffect(Unit) { state.animateScrollTo(100) }

    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .padding(horizontal = 8.dp)
            .verticalScroll(state)
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

Phương thức sửa đổi đối tượng cuộn được

Phương thức sửa đổi scrollable khác với phương thức sửa đổi thao tác cuộn ở chỗ scrollable phát hiện các cử chỉ cuộn, nhưng không bù trừ phần nội dung. Cần có ScrollableState để phương thức sửa đổi này hoạt động chính xác. Khi xây dựng ScrollableState, bạn phải cung cấp một hàm consumeScrollDelta sẽ được gọi trong mỗi bước cuộn (thông qua cử chỉ nhập, cuộn hoặc hất nhẹ) với delta được tính bằng pixel. Hàm này phải trả về khoảng cách đã cuộn để đảm bảo truyền sự kiện đúng cách trong trường hợp có các phần tử lồng nhau có phương thức sửa đổi scrollable.

Đoạn mã sau đây phát hiện các cử chỉ và cho thấy một giá trị dạng số cho một phần bù trừ, nhưng không bù trừ phần tử nào:

@Composable
fun ScrollableSample() {
    // actual composable state
    var offset by remember { mutableStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                // Scrollable state: describes how to consume
                // scrolling delta and update offset
                state = rememberScrollableState { delta ->
                    offset += delta
                    delta
                }
            )
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}

Một thành phần trên giao diện người dùng phát hiện thao tác nhấn ngón tay và cho thấy giá trị dạng số cho vị trí của ngón tay

Tính năng cuộn lồng

Compose hỗ trợ tính năng cuộn lồng (nested scrolling), trong đó nhiều phần tử phản ứng lại với một thao tác cuộn duy nhất. Một ví dụ điển hình về tính năng cuộn lồng là một danh sách bên trong một danh sách khác. Trường hợp phức tạp hơn là thanh công cụ thu gọn.

Cuộn lồng tự động

Bạn không cần làm gì đối với cuộn lồng đơn giản. Những cử chỉ khởi tạo thao tác cuộn sẽ tự động được truyền tải từ phần tử con đến phần tử mẹ, chẳng hạn như khi phần tử con không thể cuộn thêm nữa thì cử chỉ sẽ do phần tử mẹ xử lý.

Một số thành phần và phương thức sửa đổi của Compose hỗ trợ và cung cấp tính năng cuộn lồng tự động ngay từ đầu như: các API verticalScroll, horizontalScroll, scrollable, LazyTextField. Tức là khi người dùng cuộn một phần tử con bên trong các thành phần lồng nhau, thì các phương thức sửa đổi trước đó sẽ truyền delta cuộn đến những phần tử mẹ có hỗ trợ tính năng cuộn lồng.

Ví dụ sau đây cho thấy các phần tử có phương thức sửa đổi verticalScroll được áp dụng cho phần tử đó trong một vùng chứa cũng đã áp dụng phương thức sửa đổi verticalScroll.

val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
Box(
    modifier = Modifier
        .background(Color.LightGray)
        .verticalScroll(rememberScrollState())
        .padding(32.dp)
) {
    Column {
        repeat(6) {
            Box(
                modifier = Modifier
                    .height(128.dp)
                    .verticalScroll(rememberScrollState())
            ) {
                Text(
                    "Scroll here",
                    modifier = Modifier
                        .border(12.dp, Color.DarkGray)
                        .background(brush = gradient)
                        .padding(24.dp)
                        .height(150.dp)
                )
            }
        }
    }
}

Hai thành phần trên giao diện người dùng cuộn dọc được lồng vào nhau, phản hồi các cử chỉ bên trong và bên ngoài phần tử

Sử dụng phương thức sửa đổi nestedScroll

Nếu bạn cần tạo một thao tác cuộn phối hợp nâng cao giữa nhiều phần tử, phương thức sửa đổi nestedScroll sẽ giúp bạn tăng tính linh hoạt bằng cách xác định một hệ thống phân cấp cuộn lồng. Như đã đề cập trong phần trước, một số thành phần đã tích hợp sẵn chức năng hỗ trợ cuộn lồng. Tuy nhiên, đối với các thành phần kết hợp không tự động cuộn được, chẳng hạn như Box hoặc Column, các delta cuộn trên những thành phần đó sẽ không truyền trong hệ thống cuộn lồng đồng thời sẽ không tiếp cận được NestedScrollConnection cũng như thành phần mẹ. Để giải quyết vấn đề này, bạn có thể sử dụng nestedScroll để trao quyền hỗ trợ cho các thành phần khác, bao gồm cả thành phần tùy chỉnh.

Khả năng tương tác cuộn lồng (thử nghiệm)

Khi cố gắng lồng các phần tử View cuộn được trong thành phần kết hợp có thể cuộn được hoặc ngược lại, bạn có thể gặp một vài sự cố. Sự cố đáng kể nhất sẽ xảy ra khi bạn cuộn con và tiếp cận ranh giới bắt đầu hoặc kết thúc đồng thời mong đợi cha mẹ sẽ cuộn qua. Tuy nhiên, hành vi dự kiến này có thể không xảy ra hoặc có thể không hoạt động như mong đợi.

Vấn đề này là do việc kỳ vọng các thành phần kết hợp có thể cuộn được mà ra. Các thành phần kết hợp có thể cuộn đều có quy tắc "nested-scroll-by-default", nghĩa là bất kỳ vùng chứa nào có thể cuộn cũng đều phải tham gia vào chuỗi cuộn lồng, cả hai đều ở dạng cha mẹ thông qua NestedScrollConnection và con thông qua NestedScrollDispatcher. Sau đó thành phần con sẽ đẩy hoạt động cuộn lồng cho cha mẹ khi nó tiếp cận ranh giới. Ví dụ: quy tắc này cho phép Compose Pager và Compose LazyRow hoạt động hiệu quả cùng nhau. Tuy nhiên, khi thực hiện thao tác cuộn tương tác với ViewPager2 hoặc RecyclerView, vì các thao tác này không triển khai được NestedScrollingParent3 nên không thể cuộn liên tục từ con sang cha mẹ.

Để bật API tương tác cuộn lồng giữa các phần tử View có thể cuộn và thành phần kết hợp có thể cuộn đồng thời lồng theo cả hai hướng, bạn có thể sử dụng API tương tác có thể cuộn lồng để giảm thiểu những vấn đề này trong các trường hợp sau.

Thành phần hiển thị cha mẹ hợp tác có chứa một ComposeView con

Thành phần cha mẹ hợp tác View là một thành phần đã triển khai NestedScrollingParent3, và do đó có thể nhận các delta cuộn được từ thành phần kết hợp con có thể lồng được đang hợp tác. ComposeView sẽ đóng vai trò là con trong trường hợp này và cần (gián tiếp) triển khai NestedScrollingChild3. androidx.coordinatorlayout.widget.CoordinatorLayout là một ví dụ về vai trò hợp tác của cha mẹ.

Nếu cần khả năng tương tác cuộn lồng giữa vùng chứa cha mẹ View cuộn được và các thành phần kết hợp con có thể cuộn lồng, bạn có thể sử dụng rememberNestedScrollInteropConnection().

rememberNestedScrollInteropConnection() nhận và ghi nhớ NestedScrollConnection, cho phép bật khả năng tương tác cuộn lồng giữa một View mẹ có thể triển khai NestedScrollingParent3 và một Compose con. Bạn nên dùng thuộc tính này cùng với công cụ sửa đổi nestedScroll. Vì tính năng cuộn lồng được bật theo mặc định sẵn trên Compose nên bạn có thể sử dụng kết nối này để cho phép cả cuộn lồng từ View đồng thời thêm logic keo cần thiết giữa Views và thành phần kết hợp.

Việc dùng CoordinatorLayout, CollapsingToolbarLayout và một thành phần kết hợp con là những trường hợp sử dụng thường xuyên, như trong ví dụ sau:

<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:fitsSystemWindows="true">

            <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

             <!--...-->

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Trong Hoạt động hoặc Mảnh, bạn cần phải thiết lập thành phần kết hợp con và NestedScrollConnection bắt buộc:

@ExperimentalComposeUiApi
open class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                // Add the nested scroll connection to your top level @Composable element
                // using the nestedScroll modifier.
                LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) {
                    items(20) { item ->
                        Box(
                            modifier = Modifier
                                .padding(16.dp)
                                .height(56.dp)
                                .fillMaxWidth()
                                .background(Color.Gray),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(item.toString())
                        }
                    }
                }
            }
        }
    }
}

Thành phần kết hợp cha mẹ có thể chứa một AndroidView con

Tình huống này bao gồm việc triển khai API tương tác cuộn lồng ở phía Compose - khi bạn có một thành phần kết hợp mẹ chứa AndroidView con. AndroidView triển khai NestedScrollDispatcher, vì nó hoạt động như một phần tử con đối với cha mẹ đang cuộn trong Compose, cũng như NestedScrollingParent3 vì nó hoạt động như một phần tử cha mẹ của View con đang cuộn. Compose cha mẹ sẽ có thể nhận được các delta cuộn lồng từ một View con có thể cuộn lồng.

Hãy xem ví dụ dưới đây để biết cách bạn có thể đạt được khả năng tương tác cuộn lồng trong tình huống này, cùng với thanh công cụ thu gọn Compose:

@Composable
private fun NestedScrollInteropComposeParentWithAndroidChildExample() {
    val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() }
    val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }

    // Sets up the nested scroll connection between the Box composable parent
    // and the child AndroidView containing the RecyclerView
    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Updates the toolbar offset based on the scroll to enable
                // collapsible behaviour
                val delta = available.y
                val newOffset = toolbarOffsetHeightPx.value + delta
                toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
                return Offset.Zero
            }
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .nestedScroll(nestedScrollConnection)
    ) {
        TopAppBar(
            modifier = Modifier
                .height(ToolbarHeight)
                .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }
        )

        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply {
                        with(findViewById<RecyclerView>(R.id.main_list)) {
                            layoutManager = LinearLayoutManager(context, VERTICAL, false)
                            adapter = NestedScrollInteropAdapter()
                        }
                    }.also {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(it, true)
                    }
            },
            // ...
        )
    }
}

private class NestedScrollInteropAdapter :
    Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() {
    val items = (1..10).map { it.toString() }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): NestedScrollInteropViewHolder {
        return NestedScrollInteropViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item, parent, false)
        )
    }

    override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) {
        // ...
    }

    class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) {
        fun bind(item: String) {
            // ...
        }
    }
    // ...
}

Ví dụ này cho thấy cách bạn có thể sử dụng API với công cụ sửa đổi scrollable:

@Composable
fun ViewInComposeNestedScrollInteropExample() {
    Box(
        Modifier
            .fillMaxSize()
            .scrollable(rememberScrollableState {
                // View component deltas should be reflected in Compose
                // components that participate in nested scrolling
                it
            }, Orientation.Vertical)
    ) {
        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(android.R.layout.list_item, null)
                    .apply {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(this, true)
                    }
            }
        )
    }
}

Sau cùng, ví dụ này cho thấy cách API tương tác cuộn lồng được sử dụng với BottomSheetDialogFragment để đạt được hành vi kéo và loại bỏ thành công:

@OptIn(ExperimentalComposeUiApi::class)
class BottomSheetFragment : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false)

        rootView.findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                LazyColumn(
                    Modifier
                        .nestedScroll(nestedScrollInterop)
                        .fillMaxSize()
                ) {
                    item {
                        Text(text = "Bottom sheet title")
                    }
                    items(10) {
                        Text(
                            text = "List item number $it",
                            modifier = Modifier.fillMaxWidth()
                        )
                    }
                }
            }
            return rootView
        }
    }
}

Vui lòng lưu ý là rememberNestedScrollInteropConnection() sẽ cài đặt NestedScrollConnection trong phần tử bạn đã đính kèm vào đó. NestedScrollConnection chịu trách nhiệm truyền các delta từ cấp Compose sang cấp View. Điều này cho phép phần tử đó tham gia vào quá trình cuộn lồng, nhưng không cho phép tự động cuộn các phần tử. Đối với các thành phần kết hợp không thể tự động cuộn, chẳng hạn nhưBox hoặcColumn, các delta cuộn trên những thành phần đó sẽ không truyền trong hệ thống cuộn lồng và không tiếp cận được NestedScrollConnection do rememberNestedScrollInteropConnection() cung cấp, theo đó các delta này sẽ không tiếp cận được thành phần View cha mẹ. Để giải quyết vấn đề này, hãy đảm bảo bạn cũng đặt công cụ sửa đổi có thể cuộn cho các loại thành phần kết hợp có thể cuộn được. Bạn có thể tham khảo phần trước về Cuộn lồng để biết thêm thông tin chi tiết.

Chế độ xem cha mẹ không hợp tác chứa một ComposeView con

Chế độ xem không hợp tác là chế độ không triển khai các giao diệnNestedScrolling cần thiết ở phía View. Vui lòng lưu ý điều này có nghĩa là khả năng tương tác cuộn lồng với các Views này không hoạt động hiệu quả. Views không hợp tác là RecyclerViewViewPager2.

Thao tác kéo

Phương thức sửa đổi draggable là điểm truy cập cấp cao cho các cử chỉ kéo theo một hướng đồng thời báo cáo khoảng cách kéo bằng pixel.

Quan trọng là bạn phải lưu ý rằng phương thức sửa đổi này tương tự như scrollable, vì phương thức này chỉ phát hiện cử chỉ. Bạn cần giữ và biểu thị trạng thái trên màn hình bằng cách di chuyển phần tử qua phương thức sửa đổi offset:

var offsetX by remember { mutableStateOf(0f) }
Text(
    modifier = Modifier
        .offset { IntOffset(offsetX.roundToInt(), 0) }
        .draggable(
            orientation = Orientation.Horizontal,
            state = rememberDraggableState { delta ->
                offsetX += delta
            }
        ),
    text = "Drag me!"
)

Nếu bạn cần điều khiển toàn bộ cử chỉ kéo, hãy cân nhắc chuyển sang sử dụng trình phát hiện cử chỉ kéo, thông qua phương thức sửa đổi pointerInput.

Box(modifier = Modifier.fillMaxSize()) {
    var offsetX by remember { mutableStateOf(0f) }
    var offsetY by remember { mutableStateOf(0f) }

    Box(
        Modifier
            .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
            .background(Color.Blue)
            .size(50.dp)
            .pointerInput(Unit) {
                detectDragGestures { change, dragAmount ->
                    change.consumeAllChanges()
                    offsetX += dragAmount.x
                    offsetY += dragAmount.y
                }
            }
    )
}

Một thành phần trên giao diện người dùng bị kéo bằng thao tác nhấn ngón tay

Vuốt

Phương thức sửa đổi swipeable cho phép bạn kéo các thành phần mà khi nhả ra sẽ tạo hiệu ứng động về phía hai hoặc nhiều điểm neo được xác định theo một hướng. Có một cách sử dụng phổ biến là triển khai mẫu hình "vuốt để đóng".

Quan trọng là bạn phải lưu ý rằng phương thức sửa đổi này không di chuyển phần tử, mà chỉ phát hiện thao tác. Bạn cần giữ và biểu thị trạng thái trên màn hình bằng cách di chuyển phần tử qua phương thức sửa đổi offset:

Trạng thái có thể vuốt là bắt buộc trong phương thức sửa đổi swipeable. Bạn có thể tạo và ghi nhớ trạng thái này bằng rememberSwipeableState(). Trạng thái này cũng cung cấp một tập hợp phương thức hữu ích để lập trình hiệu ứng động đến các điểm neo (xem snapTo, animateTo, performFlingperformDrag) cũng như các thuộc tính để quan sát tiến trình kéo.

Bạn có thể định cấu hình cử chỉ vuốt để áp dụng nhiều loại ngưỡng như FixedThreshold(Dp)FractionalThreshold(Float) (những loại ngưỡng này có thể thay đổi tuỳ theo từng tổ hợp điểm neo từ-đến).

Để linh hoạt hơn, bạn có thể định cấu hình resistance khi vuốt qua ra ngoài phạm vi. Đồng thời, velocityThreshold sẽ tạo hiệu ứng vuốt sang trạng thái tiếp theo, ngay cả khi chưa đến vị trí thresholds.

@Composable
fun SwipeableSample() {
    val width = 96.dp
    val squareSize = 48.dp

    val swipeableState = rememberSwipeableState(0)
    val sizePx = with(LocalDensity.current) { squareSize.toPx() }
    val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states

    Box(
        modifier = Modifier
            .width(width)
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                thresholds = { _, _ -> FractionalThreshold(0.3f) },
                orientation = Orientation.Horizontal
            )
            .background(Color.LightGray)
    ) {
        Box(
            Modifier
                .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
                .size(squareSize)
                .background(Color.DarkGray)
        )
    }
}

Một thành phần trên giao diện người dùng phản hồi cử chỉ vuốt

Cảm ứng đa điểm: kéo (hình ảnh), thu phóng, xoay

Để phát hiện cử chỉ cảm ứng đa điểm dùng để kéo (hình ảnh), thu phóng và xoay, bạn có thể sử dụng phương thức sửa đổi transformable. Phương thức sửa đổi này không tự chuyển đổi phần tử mà chỉ phát hiện cử chỉ.

@Composable
fun TransformableSample() {
    // set up all transformation states
    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
    }
    Box(
        Modifier
            // apply other transformations like rotation and zoom
            // on the pizza slice emoji
            .graphicsLayer(
                scaleX = scale,
                scaleY = scale,
                rotationZ = rotation,
                translationX = offset.x,
                translationY = offset.y
            )
            // add transformable to listen to multitouch transformation events
            // after offset
            .transformable(state = state)
            .background(Color.Blue)
            .fillMaxSize()
    )
}

Một thành phần trên giao diện người dùng phản hồi các cử chỉ đa điểm chạm: kéo (hình ảnh), thu phóng và xoay

Nếu cần kết hợp cử chỉ thu phóng, kéo (hình ảnh) và xoay với các cử chỉ khác, bạn có thể sử dụng trình phát hiện PointerInputScope.detectTransformGestures.