捲動修飾符
在元素的內容邊界超過尺寸上限時,使用 verticalScroll
和 horizontalScroll
修飾符可以讓使用者以最簡單的方式捲動元素。只要使用 verticalScroll
和 horizontalScroll
修飾符,就不必平移或偏移內容。
@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
可讓您變更捲動位置或取得其目前狀態。如要使用預設參數建立,請使用 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)) } } }
scrollable 修飾符
scrollable
修飾符和 scroll 修飾符不同,scrollable
會偵測捲動手勢,但不會使其內容位移。這個修飾符需要 ScrollableState
才能正確運作。建構 ScrollableState
時,您必須提供 consumeScrollDelta
函式,利用手勢輸入、流暢捲動或快速滑過執行每個捲動步驟時,系統都會叫用該函式,並以像素呈現差異。此函式必須傳回使用的捲動距離量,以確保事件在含有 scrollable
修飾符的巢狀元素時能正確傳播。
下列程式碼片段會偵測手勢並顯示位移數值,但不會使任何元素位移:
@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()) } }
巢狀捲動
Compose 支援「巢狀捲動」,多個元素可回應單一捲動手勢。在清單中包含另一個清單,是典型的巢狀捲動運用,而較複雜的用途則是收合工具列。
自動巢狀捲動
簡易的巢狀捲動功能無需使用者執行任何動作。觸發捲動動作的手勢會自動從子項套用到父項,因此,當子項無法再捲動畫面時,就會由父項元素處理該手勢。
自動巢狀捲動功能是由部分 Compose 元件和修飾符直接支援並提供,包括:verticalScroll
、horizontalScroll
、scrollable
、Lazy
API 與 TextField
。這表示當使用者捲動巢狀元件的內部子項時,先前的修飾符會將捲動差異傳播至支援巢狀捲動功能的父項。
以下範例顯示已套用 verticalScroll
修飾符的元素,位於同樣套用 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) ) } } } } }
使用 nestedScroll
修飾符
如果您需要在多個元素間建立進階協調捲動功能,nestedScroll
修飾符可定義巢狀捲動階層,讓您擁有更多彈性。如前所述,部分元件已內建支援巢狀捲動。不過,像是 Box
或 Column
這類無法自動捲動的可組合元件,便無法在巢狀捲動系統中傳播捲動差異,且差異也無法進入 NestedScrollConnection
或父項元件。如果想解決這個問題,您可以使用 nestedScroll
將這類支援權限授予其他元件,包括自訂元件。
巢狀捲動互通性 (從 Compose 1.2.0 開始)
如果想在捲動式可組合元件中建立巢狀捲動 View
元素 (或相反情況),可能會發生問題。最容易判別的情況,就是當將子項捲動到開頭或尾端時,本應由父項接續捲動,但是並未發生預期的行為,或是實際運作方式和預期內容不同。
這可能是由於捲動式可組合元件內建的預期情況所導致的。捲動式可組合元件具有「預設建立巢狀捲動」的規則,因此任何可以捲動的容器都必須加入巢狀捲動鏈結,包括透過 NestedScrollConnection
方式作為父項,以及透過 NestedScrollDispatcher
方式作為子項。當子項抵達邊界時,就會對父項驅動巢狀捲動。舉例來說,這項規則可以讓您同時正常使用 Compose Pager
和 Compose LazyRow
。不過,如果使用 ViewPager2
或 RecyclerView
達成互通性捲動,則由於這些元件並未實作 NestedScrollingParent3
,因此子項無法將捲動延續到父項。
如果想啟用可在捲動式 View
與捲動式可組合元件之間同時使用的巢狀捲動互通性 API,讓兩個方向皆使用巢狀結構,那麼在以下情況中,您可以使用巢狀捲動互通性 API 緩解這類問題。
含有子項 ComposeView
的合作執行父項 View
合作執行父項 View
已經實作 NestedScrollingParent3
,因此可以從合作執行的巢狀子項可組合元件接收捲動差異。在這種情況下,ComposeView
會作為子項,並需用間接方式實作 NestedScrollingChild3
。合作執行父項的範例之一就是 androidx.coordinatorlayout.widget.CoordinatorLayout
。
如需在捲動式 View
父項容器和巢狀捲動式子項可組合元件之間建立巢狀捲動互通性,可使用 rememberNestedScrollInteropConnection()
。
rememberNestedScrollInteropConnection()
可允許並記憶在實作 NestedScrollingParent3
的 View
父項和 Compose 子項之間啟用巢狀捲動互通性的 NestedScrollConnection
。此內容應和 nestedScroll
修飾符一同使用。由於 Compose 端會預設啟用巢狀捲動,因此您可以利用這個連結,在 View
端啟用巢狀捲動,同時在 Views
和可組合元件之間新增必要的緊連邏輯。
常見的作法是使用 CoordinatorLayout
、CollapsingToolbarLayout
和一個子項可組合元件,請看範例:
<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>
您需要在「Activity」(活動) 或「Fragment」(片段) 中設定子項可組合元件,以及必要的 NestedScrollConnection
:
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()) } } } } } } }
含有子項 AndroidView
的父項可組合元件
在這個案例中,當您有父項內含子項 AndroidView
時,可藉此在 Compose 端實作巢狀捲動互通性 API。AndroidView
是 Compose 捲動父項的子項,因此會實作 NestedScrollDispatcher
;同時,由於它也是 View
捲動子項的父項,因此會實作 NestedScrollingParent3
。接下來,Compose 父項便可以從巢狀捲動子項 View
接收巢狀捲動的差異。
以下範例說明如何在這種情況和 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) {
// ...
}
}
// ...
}
本範例可以說明如何透過 scrollable
修飾符使用 API:
@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)
}
}
)
}
}
最後,這個範例可以說明如何透過 BottomSheetDialogFragment
使用巢狀捲動互通性 API,藉此成功達成拖曳和關閉行為:
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
}
}
}
請注意,rememberNestedScrollInteropConnection()
會在您附加的元素中安裝 NestedScrollConnection
。NestedScrollConnection
負責從 Compose 層級傳送差異到 View
層級。這樣做即可讓元素加入巢狀捲動,但是無法自動捲動元素。像是 Box
或 Column
這類無法自動捲動的可組合元件,便無法在巢狀捲動系統中傳播捲動差異,且差異無法進入 rememberNestedScrollInteropConnection()
提供的 NestedScrollConnection
,因此,差異無法進入父項 View
元件。如果要解決這個問題,請務必同時將捲動修飾符設定為這些巢狀可組合元件的類型。欲知更多詳情,請參考上述的巢狀捲動一節。
含有子項 ComposeView
的非合作執行父項 View
非合作執行的 View 並未在 View
端實作必要的 NestedScrolling
介面。請注意,這代表具有 Views
的巢狀捲動互通性無法供您馬上使用。非合作執行的 Views
為 RecyclerView
和 ViewPager2
。
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 瞭解手勢
- 將
CoordinatorLayout
遷移至 Compose - 在 Compose 中使用 View