Đối tượng sửa đổi cho thao tác cuộn
Đối tượng sửa đổi verticalScroll
và horizontalScroll
cung cấp cách đơn giản nhất để người dùng cuộn một phần tử khi nội dung của phần tử đó lớn hơn giới hạn kích thước tối đa. Với phương thức sửa đổi verticalScroll
và horizontalScroll
, bạn không cần dịch hoặc bù trừ phần nội dung.
@Composable private fun ScrollBoxes() { Column( modifier = Modifier .background(Color.LightGray) .size(100.dp) .verticalScroll(rememberScrollState()) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } }
ScrollState
cho phép bạn thay đổi vị trí thanh cuộn hoặc xem trạng thái hiện tại của vị trí thanh 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)) } } }
Đối tượng sửa đổi cho các mục cuộn được (scrollable)
Đối tượng sửa đổi scrollable
khác với đối tượng 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
để đối tượng 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
, hàm này 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ó đối tượng 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 private 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()) } }
Cuộn dạng lồng
Compose hỗ trợ lồng tính năng cuộn trong lớp khác (nested scrolling), trong đó nhiều phần tử phản ứng với một thao tác cuộn duy nhất. Ví dụ điển hình của tính năng cuộn trong lớp khác là một danh sách con nằm trong một danh sách lớn hơn. Ví dụ phức tạp hơn là thanh công cụ có thể thu gọn.
Tự động lồng tính năng cuộn trong lớp khác
Bạn không cần làm gì khi chỉ đơn giản lồng tính năng cuộn trong lớp khác. 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à đối tượng sửa đổi của Compose hỗ trợ và cung cấp tính năng cuộn dạng lồng tự động ngay từ đầu như: các API verticalScroll
, horizontalScroll
, scrollable
, Lazy
và TextField
. 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 đối tượng 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ó đối tượng sửa đổi verticalScroll
và nằm trong một vùng chứa cũng có đối tượng sửa đổi verticalScroll
.
@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) ) } } } } }
Sử dụng đối tượng 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ử, đối tượng 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 khi lồng ghép vào nhau.
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 tuỳ chỉnh.
Khả năng tương tác cuộn dạng lồng (Kể từ Compose 1.2.0)
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 thành phần con và tiếp cận ranh giới bắt đầu hoặc kết thúc, đồng thời mong đợi thành phần 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ả ở dạng thành phần mẹ thông qua NestedScrollConnection
và thành phần 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ừ thành phần con sang thành phần 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.
Cha mẹ hợp tác View
có chứa một trẻ ComposeView
Thành phần 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 hợp tác được lồng. 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 thành phần 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 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 kết dính 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:
open class MainActivity : ComponentActivity() { @OptIn(ExperimentalComposeUiApi::class) 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 mẹ có thể chứa 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 thành phần mẹ đang cuộn trong Compose, cũng như NestedScrollingParent3
vì nó hoạt động như một thành phần 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:
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
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.
Một cha mẹ không hợp tác View
có chứa một trẻ ComposeView
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à RecyclerView
và ViewPager2
.
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Tìm hiểu về cử chỉ
- Di chuyển
CoordinatorLayout
sang Compose - Sử dụng Thành phần hiển thị trong Compose