Đố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í cuộn hoặc xem trạng thái hiện tại. Để 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)
Chiến lược phát hành đĩa đơn
scrollable
đối tượng sửa đổi khác với đối tượng sửa đổi cuộn ở chỗ scrollable
phát hiện
cử chỉ cuộn và chụp delta nhưng không bù trừ nội dung của nó
tự động. Thay vào đó, thao tác này được uỷ quyền cho người dùng thông qua
ScrollableState
. Đây là điều kiện bắt buộc để đối tượng sửa đổi này hoạt động chính xác.
Khi tạo ScrollableState
, bạn phải cung cấp consumeScrollDelta
được gọi trên mỗi bước cuộn (bằng phương thức nhập bằng cử chỉ, các thao tác mượt mà
cuộn hoặc hất) với delta tính bằng pixel. Hàm này phải trả về
khoảng cách cuộn đã tiêu thụ, để đảm bảo sự kiện diễn ra đúng cách
được phổ biến trong trường hợp có các phần tử lồng nhau có scrollable
đối tượng sửa đổi.
Đ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
Tính năng cuộn lồng là một hệ thống chứa nhiều thành phần cuộn hoạt động cùng nhau bằng cách phản ứng với một cử chỉ cuộn và truyền đạt các delta cuộn (thay đổi).
Hệ thống cuộn lồng cho phép phối hợp giữa các thành phần có thể cuộn và được liên kết theo thứ bậc (thường là bằng cách chia sẻ cùng một thư mục mẹ). Hệ thống này liên kết các vùng chứa cuộn và cho phép tương tác với chức năng cuộn các delta đang được truyền và chia sẻ.
Compose cung cấp nhiều cách xử lý thao tác cuộn lồng giữa các thành phần kết hợp. Một ví dụ điển hình về thao tác cuộn lồng nhau là một danh sách bên trong một danh sách khác và trường hợp phức tạp là trường hợp thu gọn thanh công cụ.
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ý.
Tính năng cuộn lồng tự động được hỗ trợ và cung cấp ngay từ đầu bởi một số
Các thành phần và đối tượng sửa đổi của Compose:
verticalScroll
!
horizontalScroll
,
scrollable
,
Các API Lazy
và TextField
. Điều này có nghĩa là khi người dùng cuộn
con của các thành phần lồng nhau, các đối tượng sửa đổi trước đó sẽ truyền thao tác cuộn
delta cho thành phần mẹ có hỗ trợ cuộn lồng.
Ví dụ sau đây cho thấy các phần tử có
verticalScroll
đối tượng sửa đổi được áp dụng cho các đối tượng đó bên trong một vùng chứa cũng có verticalScroll
được áp dụng đối tượng sửa đổi.
@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 cuộn lồng nhau
của Google. Tuy nhiên, đối với các thành phần kết hợp không thể cuộn tự động, chẳng hạn như
Box
hoặc Column
, các delta cuộn trên các thành phần như vậy sẽ không lan truyền trong
hệ thống cuộn lồng nhau và delta sẽ không đạt đến NestedScrollConnection
thành phần mẹ. Để giải quyết vấn đề này, bạn có thể sử dụng nestedScroll
để trao
khả năng hỗ trợ các thành phần khác, bao gồm cả các thành phần tuỳ chỉnh.
Chu kỳ cuộn lồng
Chu kỳ cuộn lồng nhau là luồng các delta cuộn được điều phối lên và xuống
cây phân cấp thông qua tất cả các thành phần (hoặc nút) là một phần của mục lồng ghép
hệ thống cuộn, ví dụ: bằng cách sử dụng các thành phần có thể cuộn và đối tượng sửa đổi, hoặc
nestedScroll
.
Các giai đoạn của chu kỳ cuộn lồng nhau
Khi một sự kiện kích hoạt (ví dụ: một cử chỉ) được phát hiện bằng một tính năng cuộn thậm chí trước khi hành động cuộn thực tế được kích hoạt, giá trị delta được gửi đến hệ thống cuộn lồng nhau và trải qua ba giai đoạn: cuộn trước, tiêu thụ nút và cuộn sau.
Trong giai đoạn đầu tiên là giai đoạn cuộn trước, thành phần đã nhận được sự kiện kích hoạt deltas sẽ điều phối những sự kiện đó lên thông qua cây phân cấp, cha mẹ. Sau đó, các sự kiện delta sẽ xuất hiện bong bóng, có nghĩa là delta sẽ truyền từ phần tử mẹ gốc xa nhất xuống phần tử con đã khởi động chu kỳ cuộn lồng nhau.
Thao tác này cung cấp các thành phần mẹ cuộn lồng (thành phần kết hợp sử dụng nestedScroll
hoặc
các đối tượng sửa đổi có thể cuộn) cơ hội thực hiện thao tác nào đó với delta trước khi
nút chính có thể sử dụng mã đó.
Trong giai đoạn sử dụng nút, chính nút đó sẽ sử dụng bất kỳ delta nào không được cha mẹ sử dụng. Đây là khi chuyển động cuộn thực sự được thực hiện và hiển thị.
Trong giai đoạn này, trẻ có thể chọn tiêu thụ toàn bộ hoặc một phần phần còn lại cuộn. Mọi phần còn lại sẽ được gửi lại để trải qua giai đoạn sau khi cuộn.
Cuối cùng, trong giai đoạn sau khi cuộn, mọi thứ mà bản thân nút không sử dụng sẽ được gửi lại cho đối tượng cấp trên để tiêu thụ.
Giai đoạn sau cuộn hoạt động theo cách tương tự như giai đoạn cuộn trước, trong đó bất kỳ có thể chọn xem hoặc không sử dụng.
Tương tự như cuộn, khi kết thúc cử chỉ kéo, ý định của người dùng có thể chuyển thành vận tốc dùng để hất (cuộn bằng ảnh động) vùng chứa có thể cuộn. Thao tác hất cũng là một phần của chu kỳ cuộn lồng nhau và vận tốc do sự kiện kéo tạo ra sẽ trải qua các giai đoạn tương tự nhau: hất trước, tiêu thụ nút và sau khi hất. Lưu ý rằng ảnh động dựa trên cử chỉ hất chỉ được liên kết bằng cử chỉ chạm và sẽ không được kích hoạt bằng các sự kiện khác, chẳng hạn như a11y hoặc cuộn phần cứng.
Tham gia vào chu kỳ cuộn lồng ghép
Tham gia vào chu trình có nghĩa là chặn, sử dụng và báo cáo mức tiêu thụ delta theo hệ phân cấp. Compose cung cấp một bộ công cụ để ảnh hưởng đến cách hoạt động của hệ thống cuộn lồng nhau cũng như cách tương tác trực tiếp với nó, chẳng hạn như khi bạn cần thực hiện tác vụ nào đó với các delta cuộn trước một thành phần có thể cuộn thậm chí bắt đầu cuộn.
Nếu chu kỳ cuộn lồng là một hệ thống tác động trên một chuỗi các nút,
nestedScroll
đối tượng sửa đổi là một cách để chặn và chèn vào những thay đổi này, và
ảnh hưởng đến dữ liệu (delta cuộn) được truyền trong chuỗi. Chiến dịch này
đối tượng sửa đổi có thể được đặt ở bất cứ đâu trong hệ phân cấp và giao tiếp với
các thực thể sửa đổi cuộn lồng lên cây để có thể chia sẻ thông tin qua
kênh này. Các thành phần của đối tượng sửa đổi này là NestedScrollConnection
và NestedScrollDispatcher
.
NestedScrollConnection
đưa ra cách phản hồi các giai đoạn của chu kỳ cuộn lồng nhau và tác động
hệ thống cuộn lồng. API này bao gồm 4 phương thức gọi lại, mỗi phương thức
thể hiện một trong các giai đoạn tiêu thụ: trước/sau khi cuộn và trước/sau hất:
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 } }
Mỗi lệnh gọi lại cũng cung cấp thông tin về delta đang được truyền:
available
delta cho giai đoạn cụ thể đó và consumed
delta tiêu thụ trong
các giai đoạn trước đó. Nếu tại bất kỳ thời điểm nào bạn muốn ngừng lan truyền delta
, bạn có thể sử dụng kết nối cuộn lồng nhau để làm như vậy:
val disabledNestedScrollConnection = remember { object : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { return if (source == NestedScrollSource.SideEffect) { available } else { Offset.Zero } } } }
Tất cả các lệnh gọi lại đều cung cấp thông tin về
NestedScrollSource
loại.
NestedScrollDispatcher
khởi tạo chu kỳ cuộn lồng nhau. Sử dụng trình điều phối và gọi các phương thức của trình điều phối
kích hoạt chu kỳ này. Vùng chứa có thể cuộn được tích hợp sẵn trình điều phối sẽ gửi
delta được thu thập trong các cử chỉ vào hệ thống. Vì lý do này, hầu hết các trường hợp sử dụng
việc tuỳ chỉnh chức năng cuộn lồng nhau sẽ liên quan đến việc sử dụng NestedScrollConnection
của trình điều phối, để phản ứng với các delta hiện có thay vì gửi các delta mới.
Xem
NestedScrollDispatcherSample
để sử dụng thêm.
Khả năng tương tác cuộn dạng lồng
Khi bạn cố gắng lồng các phần tử View
cuộn được trong các thành phần kết hợp có thể cuộn, hoặc
nhưng ngược lại, bạn có thể
gặp phải vấn đề. Những lượt xem đáng chú ý nhất
xảy ra khi bạn cuộn thành phần con và đạt đến giới hạn bắt đầu hoặc kết thúc và mong đợi
cha mẹ cuộn qua. Tuy nhiên, hành vi được mong đợi này
không thể xảy ra hoặc 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.
Một tài khoản cha mẹ hợp tác View
có con ComposeView
Cha mẹ hợp tác View
là một thành viên đã triển khai
NestedScrollingParent3
và do đó có thể nhận các delta cuộn từ một mục được lồng
thành phần kết hợp con. ComposeView
sẽ đóng vai trò là con trong trường hợp này và sẽ
cần (gián tiếp) triển khai
NestedScrollingChild3
.
Một ví dụ về cha mẹ hợp tác là
androidx.coordinatorlayout.widget.CoordinatorLayout
.
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 ở phía Compose, nên bạn
có thể sử dụng kết nối này để cho phép cả hai tính năng cuộn lồng ở phía View
và thêm
logic kết nối cần thiết giữa Views
và các thành phần kết hợp.
Một trường hợp sử dụng thường xuyên là sử dụng CoordinatorLayout
, CollapsingToolbarLayout
và
một thành phần kết hợp con 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 thiết lập thành phần kết hợp con và
bắt buộc
NestedScrollConnection
:
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()) } } } } } } }
Một thành phần kết hợp mẹ 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. Cha mẹ Compose sẽ
thì có thể nhận được các delta cuộn lồng từ một con có thể cuộn lồng
View
.
Ví dụ sau cho thấy cách bạn có thể đạt được khả năng tương tác cuộn lồng trong tính nă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
}
}
}
Lưu ý rằng
rememberNestedScrollInteropConnection()
sẽ cài đặt một
NestedScrollConnection
trong phần tử mà bạn đính kèm. 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 thao tác cuộn lồng nhau, 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 cuộn được
tự động (chẳng hạn như Box
hoặc Column
), các delta cuộn trên các thành phần đó sẽ không
lan truyền trong hệ thống cuộn lồng nhau và delta sẽ không tiếp cận được
NestedScrollConnection
do rememberNestedScrollInteropConnection()
cung cấp,
do đó, các delta đó sẽ không tiếp cận thành phần View
chính. Để giải quyết vấn đề này,
hãy đảm bảo bạn cũng đặt đối tượng sửa đổi có thể cuộn thành các loại thuộc tính
thành phần kết hợp. Bạn có thể tham khảo phần trước về nội dung Lồng nhau
cuộn để xem thông tin chi tiết hơn
của bạn.
Cha mẹ không hợp tác View
có con 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