Scroll bertingkat adalah sistem tempat beberapa komponen scroll yang terdapat dalam satu sama lain bekerja sama dengan bereaksi terhadap satu gestur scroll dan mengomunikasikan delta (perubahan) scroll-nya.
Sistem scroll bertingkat memungkinkan koordinasi antara komponen yang dapat di-scroll dan ditautkan secara hierarkis (paling sering dengan berbagi induk yang sama). Sistem ini menautkan penampung scroll dan memungkinkan interaksi dengan delta scroll yang disebarkan dan dibagikan di antaranya.
Compose menyediakan beberapa cara untuk menangani scroll bertingkat di antara composable. Contoh umum scroll bertingkat adalah daftar di dalam daftar lain, dan kasus yang lebih kompleks adalah toolbar yang dapat diciutkan.
Scrolling bertingkat otomatis
Scroll bertingkat sederhana tidak memerlukan tindakan dari Anda. Gestur yang memulai tindakan scroll diterapkan dari turunan ke induk secara otomatis, sehingga saat turunan tidak dapat men-scroll lagi, gestur tersebut akan ditangani oleh elemen induknya.
Scroll bertingkat otomatis didukung dan disediakan secara langsung oleh beberapa
komponen dan pengubah Compose:
verticalScroll,
horizontalScroll,
scrollable,
Lazy API, dan TextField. Artinya, ketika pengguna men-scroll turunan
internal komponen bertingkat, pengubah sebelumnya akan menyebarkan delta
scroll ke induk yang memiliki dukungan scroll bertingkat.
Contoh berikut menunjukkan elemen dengan pengubah
verticalScroll
yang diterapkan di dalamnya dalam penampung yang juga memiliki pengubah verticalScroll
yang diterapkan ke elemen tersebut.
@Composable private fun AutomaticNestedScroll() { 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) ) } } } } }
Menggunakan pengubah nestedScroll
Jika Anda perlu membuat scroll terkoordinasi lanjutan di antara beberapa elemen, pengubah nestedScroll memberi Anda lebih banyak fleksibilitas dengan menentukan hierarki scroll bertingkat. Seperti
yang disebutkan di bagian sebelumnya, beberapa komponen memiliki dukungan scroll bertingkat
bawaan. Namun, untuk composable yang tidak dapat di-scroll secara otomatis, seperti
Box atau Column, delta scroll pada komponen tersebut tidak akan menyebar di
sistem scroll bertingkat dan delta tidak akan menjangkau NestedScrollConnection atau
komponen induk. Untuk mengatasi hal ini, Anda dapat menggunakan nestedScroll untuk memberikan dukungan tersebut kepada komponen lain, termasuk komponen kustom.
Siklus scroll bertingkat
Siklus scroll bertingkat adalah alur delta scroll yang dikirim ke atas dan ke bawah
hierarki hierarki melalui semua komponen (atau node) yang merupakan bagian dari sistem
scroll bertingkat, misalnya dengan menggunakan komponen dan pengubah yang dapat di-scroll, atau
nestedScroll.
Fase siklus scroll bertingkat
Saat peristiwa pemicu (misalnya, gestur) terdeteksi oleh komponen yang dapat di-scroll, sebelum tindakan scroll yang sebenarnya dipicu, delta yang dihasilkan akan dikirim ke sistem scroll bertingkat dan melalui tiga fase: pra-scroll, pemakaian node, dan pasca-scroll.
Pada fase pertama, pra-scroll, komponen yang menerima delta peristiwa pemicu akan mengirimkan peristiwa tersebut ke atas, melalui hierarki, ke induk paling atas. Peristiwa delta kemudian akan menggelembung ke bawah, yang berarti delta akan disebarkan dari induk paling root ke bawah menuju turunan yang memulai siklus scroll bertingkat.
Hal ini memberi induk scroll bertingkat (composable yang menggunakan pengubah nestedScroll atau
scrollable) kesempatan untuk melakukan sesuatu dengan delta sebelum
node itu sendiri dapat menggunakannya.
Pada fase konsumsi node, node itu sendiri akan menggunakan delta apa pun yang tidak digunakan oleh induknya. Ini adalah saat gerakan scroll benar-benar dilakukan dan terlihat.
Selama fase ini, turunan dapat memilih untuk menggunakan semua atau sebagian scroll yang tersisa. Semua yang tersisa akan dikirim kembali ke atas untuk melalui fase pasca-scroll.
Terakhir, dalam fase pasca-scroll, apa pun yang tidak digunakan oleh node itu sendiri akan dikirim lagi ke ancestor-nya untuk digunakan.
Fase pasca-scroll berfungsi dengan cara yang mirip dengan fase pra-scroll, saat salah satu induk dapat memilih untuk menggunakan atau tidak.
Serupa dengan scroll, saat gestur tarik selesai, niat pengguna dapat diterjemahkan menjadi kecepatan yang digunakan untuk menggeser (scroll menggunakan animasi) penampung yang dapat di-scroll. Fling juga merupakan bagian dari siklus scroll bertingkat, dan kecepatan yang dihasilkan oleh peristiwa tarik melewati fase yang serupa: pra-fling, konsumsi node, dan pasca-fling. Perhatikan bahwa animasi ayun hanya dikaitkan dengan gestur sentuh dan tidak akan dipicu oleh peristiwa lain, seperti a11y atau scroll hardware.
Berpartisipasi dalam siklus scroll bertingkat
Partisipasi dalam siklus berarti mencegat, menggunakan, dan melaporkan penggunaan delta di sepanjang hierarki. Compose menyediakan serangkaian alat untuk memengaruhi cara kerja sistem scroll bertingkat dan cara berinteraksi langsung dengannya, misalnya saat Anda perlu melakukan sesuatu dengan delta scroll sebelum komponen yang dapat di-scroll bahkan mulai di-scroll.
Jika siklus scroll bertingkat adalah sistem yang bertindak pada rantai node, pengubah
nestedScroll
adalah cara untuk menangkap dan menyisipkan ke dalam perubahan ini, serta
memengaruhi data (delta scroll) yang disebarkan dalam rantai. Pengubah
ini dapat ditempatkan di mana saja dalam hierarki, dan berkomunikasi dengan
instance pengubah scroll bertingkat di atas hierarki sehingga dapat berbagi informasi melalui
saluran ini. Elemen penyusun pengubah ini adalah NestedScrollConnection
dan NestedScrollDispatcher.
NestedScrollConnection
menyediakan cara untuk merespons fase siklus scroll bertingkat dan memengaruhi
sistem scroll bertingkat. Terdiri dari empat metode callback, yang masing-masing
mewakili salah satu fase konsumsi: pra/pasca-scroll dan pra/pasca-fling:
val nestedScrollConnection = object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { println("Received onPreScroll callback.") return Offset.Zero } override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { println("Received onPostScroll callback.") return Offset.Zero } }
Setiap callback juga memberikan informasi tentang delta yang disebarkan:
delta available untuk fase tertentu, dan delta consumed yang digunakan dalam
fase sebelumnya. Jika suatu saat Anda ingin berhenti menyebarkan delta ke atas
hierarki, Anda dapat menggunakan koneksi scroll bertingkat untuk melakukannya:
val disabledNestedScrollConnection = remember { object : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { return if (source == NestedScrollSource.SideEffect) { available } else { Offset.Zero } } } }
Semua callback memberikan informasi tentang jenis
NestedScrollSource.
NestedScrollDispatcher
melakukan inisialisasi siklus scroll bertingkat. Menggunakan dispatcher dan memanggil metodenya akan memicu siklus. Penampung yang dapat di-scroll memiliki dispatcher bawaan yang mengirimkan
delta yang diambil selama gestur ke dalam sistem. Oleh karena itu, sebagian besar kasus penggunaan
penyesuaian scroll bertingkat melibatkan penggunaan NestedScrollConnection, bukan
dispatcher, untuk bereaksi terhadap delta yang sudah ada, bukan mengirim delta baru.
Lihat
NestedScrollDispatcherSample
untuk penggunaan lainnya.
Mengubah ukuran gambar saat di-scroll
Saat pengguna men-scroll, Anda dapat membuat efek visual dinamis dengan gambar yang berubah ukuran berdasarkan posisi scroll.
Mengubah ukuran gambar berdasarkan posisi scroll
Cuplikan ini menunjukkan pengubahan ukuran gambar dalam LazyColumn berdasarkan
posisi scroll vertikal. Gambar akan menyusut saat pengguna men-scroll ke bawah, dan membesar
saat mereka men-scroll ke atas, tetap berada dalam batas ukuran minimum dan maksimum yang ditentukan:
@Composable fun ImageResizeOnScrollExample( modifier: Modifier = Modifier, maxImageSize: Dp = 300.dp, minImageSize: Dp = 100.dp ) { var currentImageSize by remember { mutableStateOf(maxImageSize) } var imageScale by remember { mutableFloatStateOf(1f) } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // Calculate the change in image size based on scroll delta val delta = available.y val newImageSize = currentImageSize + delta.dp val previousImageSize = currentImageSize // Constrain the image size within the allowed bounds currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize) val consumed = currentImageSize - previousImageSize // Calculate the scale for the image imageScale = currentImageSize / maxImageSize // Return the consumed scroll amount return Offset(0f, consumed.value) } } } Box(Modifier.nestedScroll(nestedScrollConnection)) { LazyColumn( Modifier .fillMaxWidth() .padding(15.dp) .offset { IntOffset(0, currentImageSize.roundToPx()) } ) { // Placeholder list items items(100, key = { it }) { Text( text = "Item: $it", style = MaterialTheme.typography.bodyLarge ) } } Image( painter = ColorPainter(Color.Red), contentDescription = "Red color image", Modifier .size(maxImageSize) .align(Alignment.TopCenter) .graphicsLayer { scaleX = imageScale scaleY = imageScale // Center the image vertically as it scales translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f } ) } }
Poin penting tentang kode
- Kode ini menggunakan
NestedScrollConnectionuntuk menangkap peristiwa scroll. onPreScrollmenghitung perubahan ukuran gambar berdasarkan delta scroll.- Variabel status
currentImageSizemenyimpan ukuran gambar saat ini, yang dibatasi antaraminImageSizedanmaxImageSize. imageScaleyang berasal daricurrentImageSize. LazyColumnmengimbangi berdasarkancurrentImageSize.Imagemenggunakan pengubahgraphicsLayeruntuk menerapkan skala yang dihitung.translationYdalamgraphicsLayermemastikan gambar tetap berada di tengah secara vertikal saat diskalakan.
Hasil
Cuplikan sebelumnya menghasilkan efek penskalaan gambar saat di-scroll:
Interop scrolling bertingkat
Saat mencoba menyusun bertingkat elemen View yang dapat di-scroll dalam composable yang dapat di-scroll, atau
sebaliknya, Anda mungkin mengalami masalah. Masalah yang paling terlihat akan terjadi saat Anda men-scroll turunan dan mencapai batas awal atau akhir, dan induk diharapkan mengambil alih scroll. Namun, perilaku yang diharapkan ini mungkin tidak terjadi atau mungkin tidak berfungsi seperti yang diharapkan.
Masalah ini adalah hasil dari ekspektasi yang dibuat di composable yang dapat di-scroll.
Composable yang dapat di-scroll memiliki aturan "nested-scroll-by-default", yang berarti
setiap penampung yang dapat di-scroll harus berpartisipasi dalam rantai scroll bertingkat, baik sebagai
induk melalui
NestedScrollConnection
maupun sebagai turunan melalui
NestedScrollDispatcher.
Turunan kemudian akan mendorong scroll bertingkat untuk induk saat turunan berada di
batas. Sebagai contoh, aturan ini memungkinkan Compose Pager dan Compose LazyRow
untuk bekerja sama dengan baik. Namun, jika scroll interoperabilitas dilakukan
dengan ViewPager2 atau RecyclerView, karena scroll ini tidak mengimplementasikan
NestedScrollingParent3,
scrolling berkelanjutan dari turunan ke induk tidak dapat dilakukan.
Untuk mengaktifkan API interop scroll bertingkat antara elemen View yang dapat di-scroll dan
composable yang dapat di-scroll, yang ditumpuk di kedua arah, Anda dapat menggunakan API interop
scroll bertingkat untuk mengurangi masalah ini, dalam skenario berikut.
View induk yang bekerja sama berisi ComposeView turunan
View induk yang bekerja sama adalah yang sudah mengimplementasikan
NestedScrollingParent3
sehingga dapat menerima delta scroll dari composable turunan
bertingkat yang bekerja sama. Dalam hal ini, ComposeView akan bertindak sebagai turunan dan
harus (secara tidak langsung) mengimplementasikan
NestedScrollingChild3.
Salah satu contoh induk yang bekerja sama adalah
androidx.coordinatorlayout.widget.CoordinatorLayout.
Jika memerlukan interoperabilitas scroll bertingkat antara penampung induk View yang
dapat di-scroll dan composable turunan yang dapat di-scroll, Anda dapat menggunakan
rememberNestedScrollInteropConnection().
rememberNestedScrollInteropConnection()
mengizinkan dan mengingat
NestedScrollConnection
yang memungkinkan interoperabilitas scroll bertingkat antara induk View yang
mengimplementasikan
NestedScrollingParent3
dan turunan Compose. Ini harus digunakan bersama pengubah
nestedScroll. Karena scroll bertingkat diaktifkan secara default di sisi Compose, Anda
dapat menggunakan koneksi ini untuk mengaktifkan scroll bertingkat di sisi View dan menambahkan
logika glue yang diperlukan antara Views dan composable.
Kasus penggunaan sering menggunakan CoordinatorLayout, CollapsingToolbarLayout, dan
composable turunan, seperti ditunjukkan dalam contoh ini:
<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>
Dalam Activity atau Fragment, Anda perlu menyiapkan composable turunan dan
NestedScrollConnection
yang diperlukan:
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()) } } } } } } }
Composable induk yang berisi AndroidView turunan
Skenario ini mencakup implementasi API interop scroll bertingkat di
sisi Compose - jika Anda memiliki composable induk yang berisi AndroidView
turunan. AndroidView akan mengimplementasikan
NestedScrollDispatcher,
karena berfungsi sebagai turunan dari induk scroll Compose, serta
NestedScrollingParent3
, karena berfungsi sebagai induk untuk turunan scrolling View. Induk Compose
akan dapat menerima delta scroll bertingkat dari View turunan
bertingkat yang dapat di-scroll.
Contoh berikut menunjukkan cara melakukan interop scroll bertingkat dalam skenario ini, beserta toolbar Compose yang diciutkan:
@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( title = {}, 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) { // ... } } // ... }
Contoh ini menunjukkan cara menggunakan API dengan pengubah 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(R.layout.list_item, null) .apply { // Nested scrolling interop is enabled when // nested scroll is enabled for the root View ViewCompat.setNestedScrollingEnabled(this, true) } } ) } }
Terakhir, contoh ini menunjukkan cara API interop scroll bertingkat digunakan dengan
BottomSheetDialogFragment
untuk mencapai perilaku tarik lalu tutup yang sukses:
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 } } }
Perhatikan bahwa
rememberNestedScrollInteropConnection()
akan menginstal
NestedScrollConnection
di elemen yang Anda pasangkan. NestedScrollConnection bertanggung jawab untuk
mengirim delta dari level Compose ke level View. Hal ini memungkinkan
elemen berpartisipasi dalam scroll bertingkat, tetapi tidak mengaktifkan
scrolling elemen secara otomatis. Untuk composable yang tidak dapat di-scroll
secara otomatis, seperti Box atau Column, delta scroll pada komponen tersebut tidak akan
menyebar di sistem scroll bertingkat dan delta tidak akan menjangkau
NestedScrollConnection yang disediakan oleh rememberNestedScrollInteropConnection().
Oleh karena itu, delta tersebut tidak akan menjangkau komponen View induk. Untuk mengatasi hal ini,
pastikan Anda juga menetapkan pengubah yang dapat di-scroll ke jenis composable
bertingkat ini. Anda dapat melihat bagian sebelumnya di scrolling bertingkat untuk informasi yang lebih mendetail.
View induk yang tidak bekerja sama berisi ComposeView turunan
View yang tidak bekerja sama adalah View yang tidak mengimplementasikan antarmuka
NestedScrolling yang diperlukan di sisi View. Perlu diperhatikan bahwa ini berarti
interoperabilitas scroll bertingkat dengan Views ini tidak langsung berfungsi
dari awal. Views yang tidak bekerja sama adalah RecyclerView dan ViewPager2.
Referensi lainnya
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Memahami gestur
- Memigrasikan
CoordinatorLayoutke Compose - Menggunakan View di Compose