اصلاح کننده های اسکرول
اصلاحکنندههای 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
حرکات اسکرول را تشخیص میدهد و دلتاها را میگیرد، اما محتویات آن را به طور خودکار تعدیل نمیکند. در عوض از طریق 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
برای ارائه چنین پشتیبانی به سایر مؤلفهها، از جمله مؤلفههای سفارشی، استفاده کنید.
چرخه پیمایش تو در تو
چرخه پیمایش تودرتو، جریان دلتاهای پیمایشی است که از طریق تمام مؤلفهها (یا گرهها) که بخشی از سیستم پیمایش تودرتو هستند، بهعنوان مثال با استفاده از مؤلفهها و اصلاحکنندههای قابل پیمایش یا nestedScroll
به بالا و پایین درخت سلسله مراتب ارسال میشوند.
مراحل چرخه پیمایش تو در تو
هنگامی که یک رویداد ماشه (به عنوان مثال، یک حرکت) توسط یک مؤلفه قابل پیمایش شناسایی میشود، قبل از اینکه عمل پیمایش واقعی انجام شود، دلتاهای تولید شده به سیستم پیمایش تودرتو فرستاده میشوند و از سه مرحله عبور میکنند: پیش پیمایش، مصرف گره، و پس از اسکرول.
در مرحله اول، قبل از اسکرول، مؤلفه ای که دلتاهای رویداد ماشه را دریافت کرده است، آن رویدادها را از طریق درخت سلسله مراتب به بالاترین والد ارسال می کند. سپس رویدادهای دلتا به سمت پایین حباب می شوند، به این معنی که دلتاها از ریشه ترین والد به سمت فرزندی که چرخه پیمایش تودرتو را شروع کرده است، منتشر می شود.
این به والدین پیمایش تودرتو (کامپوزیشنها با استفاده از nestedScroll
یا اصلاحکنندههای قابل پیمایش) این فرصت را میدهد تا قبل از اینکه خود گره بتواند آن را مصرف کند، کاری با دلتا انجام دهد.
در مرحله مصرف گره، خود گره از دلتای استفاده می کند که توسط والدینش استفاده نشده است. این زمانی است که حرکت اسکرول در واقع انجام شده و قابل مشاهده است.
در این مرحله، کودک ممکن است تمام یا بخشی از اسکرول باقیمانده را مصرف کند. هر چیزی که باقی مانده باشد برای گذراندن مرحله پس از اسکرول به بالا ارسال می شود.
در نهایت، در مرحله پس از اسکرول، هر چیزی که خود گره مصرف نکرده باشد، دوباره برای مصرف به اجداد خود ارسال می شود.
مرحله پس از اسکرول به روشی مشابه مرحله قبل از اسکرول عمل می کند، جایی که هر یک از والدین ممکن است مصرف کنند یا نه.
به طور مشابه برای اسکرول، هنگامی که یک حرکت کشیدن به پایان می رسد، قصد کاربر ممکن است به سرعتی تبدیل شود که برای پرت کردن (پیمایش با استفاده از انیمیشن) ظرف قابل پیمایش استفاده می شود. پرت کردن نیز بخشی از چرخه پیمایش تو در تو است و سرعتهای ایجاد شده توسط رویداد درگ مراحل مشابهی را طی میکنند: پیش پرتاب، مصرف گره و پس از پرتاب. توجه داشته باشید که انیمیشن پرت کردن فقط با ژست لمسی مرتبط است و با رویدادهای دیگر مانند a11y یا اسکرول سخت افزاری فعال نمی شود.
در چرخه پیمایش تودرتو شرکت کنید
مشارکت در چرخه به معنای رهگیری، مصرف و گزارش مصرف دلتاها در امتداد سلسله مراتب است. Compose مجموعهای از ابزارها را برای تأثیرگذاری بر نحوه عملکرد سیستم پیمایش تودرتو و نحوه تعامل مستقیم با آن فراهم میکند، برای مثال زمانی که باید کاری را با دلتاهای اسکرول انجام دهید قبل از اینکه یک جزء قابل پیمایش حتی شروع به پیمایش کند.
اگر چرخه پیمایش تودرتو سیستمی است که بر روی زنجیره ای از گره ها عمل می کند، اصلاح کننده nestedScroll
راهی برای رهگیری و درج در این تغییرات و تأثیرگذاری بر داده ها (دلتاهای اسکرول) است که در زنجیره منتشر می شوند. این اصلاح کننده را می توان در هر جایی از سلسله مراتب قرار داد و با نمونه های اصلاح کننده اسکرول تودرتو در بالای درخت ارتباط برقرار می کند تا بتواند اطلاعات را از طریق این کانال به اشتراک بگذارد. بلوک های سازنده این اصلاح کننده NestedScrollConnection
و NestedScrollDispatcher
هستند.
NestedScrollConnection
راهی برای پاسخ به مراحل چرخه پیمایش تودرتو و تأثیرگذاری بر سیستم پیمایش تو در تو فراهم می کند. این متشکل از چهار روش برگشت تماس است که هر کدام یکی از مراحل مصرف را نشان میدهد: پیش/پس از اسکرول و پیش/پس از پرتاب:
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 } }
هر فراخوانی همچنین اطلاعاتی در مورد دلتای در حال انتشار می دهد: دلتای available
برای آن فاز خاص و دلتای consumed
مصرف شده در فازهای قبلی. اگر در هر نقطه ای بخواهید انتشار دلتا در سلسله مراتب را متوقف کنید، می توانید از اتصال پیمایش تودرتو برای این کار استفاده کنید:
val disabledNestedScrollConnection = remember { object : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { return if (source == NestedScrollSource.SideEffect) { available } else { Offset.Zero } } } }
همه فراخوانها اطلاعاتی در مورد نوع NestedScrollSource
ارائه میکنند.
NestedScrollDispatcher
چرخه پیمایش تودرتو را مقداردهی اولیه می کند. استفاده از دیسپچر و فراخوانی متدهای آن چرخه را راه اندازی می کند. ظروف قابل پیمایش دارای یک توزیع کننده داخلی هستند که دلتاهای گرفته شده در حین حرکات را به سیستم ارسال می کند. به همین دلیل، بیشتر موارد استفاده از سفارشی کردن پیمایش تودرتو شامل استفاده از NestedScrollConnection
به جای توزیع کننده، برای واکنش به دلتاهای موجود به جای ارسال موارد جدید است. برای استفاده بیشتر به NestedScrollDispatcherSample
مراجعه کنید.
تودرتو پیمایش interop
وقتی سعی میکنید عناصر View
قابل پیمایش را در ترکیببندیهای قابل پیمایش قرار دهید، یا برعکس، ممکن است با مشکلاتی مواجه شوید. قابل توجه ترین موارد زمانی اتفاق می افتد که شما کودک را اسکرول می کنید و به مرزهای شروع یا پایان آن می رسید و انتظار دارید که والدین پیمایش را بر عهده بگیرند. با این حال، این رفتار مورد انتظار یا ممکن است اتفاق نیفتد یا ممکن است مطابق انتظار عمل نکند.
این موضوع نتیجه انتظارات ساخته شده در کامپوزیشن های قابل پیمایش است. ترکیبهای قابل پیمایش دارای قانون «پیمایش تودرتو» هستند، به این معنی که هر ظرف قابل پیمایش باید در زنجیره پیمایش تودرتو مشارکت کند، هم بهعنوان والد از طریق NestedScrollConnection
و هم بهعنوان فرزند از طریق NestedScrollDispatcher
. هنگامی که کودک در محدوده است، کودک یک طومار تو در تو را برای والدین می راند. به عنوان مثال، این قانون به Compose Pager
و Compose LazyRow
اجازه می دهد تا به خوبی با هم کار کنند. با این حال، هنگامی که پیمایش قابلیت همکاری با ViewPager2
یا RecyclerView
انجام میشود، چون NestedScrollingParent3
را پیادهسازی نمیکنند، پیمایش مداوم از فرزند به والد امکانپذیر نیست.
برای فعال کردن Nested Scroll Interop API بین عناصر View
قابل پیمایش و composableهای قابل پیمایش، تودرتو در هر دو جهت، میتوانید از API interop پیمایش تودرتو برای کاهش این مشکلات در سناریوهای زیر استفاده کنید.
یک View
والدین همکار که حاوی ComposeView
فرزند است
یک View
والد همکار، نمایشی است که قبلاً NestedScrollingParent3
را پیادهسازی میکند و بنابراین میتواند دلتاهای پیمایشی را از یک فرزند تودرتوی همکاریکننده دریافت کند. ComposeView
در این مورد به عنوان یک کودک عمل می کند و باید (غیر مستقیم) NestedScrollingChild3
را پیاده سازی کند. یک نمونه از والدین همکار androidx.coordinatorlayout.widget.CoordinatorLayout
است.
اگر به قابلیت همکاری پیمایش تودرتو بین View
کانتینرهای والد قابل پیمایش و composableهای فرزند قابل پیمایش تودرتو نیاز دارید، می توانید از rememberNestedScrollInteropConnection()
استفاده کنید.
rememberNestedScrollInteropConnection()
اجازه می دهد و NestedScrollConnection
را به خاطر می آورد که قابلیت همکاری پیمایش تودرتو را بین یک View
والد که NestedScrollingParent3
را اجرا می کند و یک فرزند Compose را فعال می کند. این باید همراه با یک اصلاح کننده nestedScroll
استفاده شود. از آنجایی که پیمایش تودرتو به طور پیشفرض در سمت Compose فعال است، میتوانید از این اتصال برای فعال کردن پیمایش تودرتو در سمت View
و اضافه کردن منطق چسب لازم بین Views
و Composable استفاده کنید.
یک مورد استفاده مکرر استفاده از CoordinatorLayout
، CollapsingToolbarLayout
و یک فرزند composable است که در این مثال نشان داده شده است:
<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 خود، باید فرزند خود را composable و 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()) } } } } } } }
یک والد قابل ترکیب حاوی AndroidView
فرزند
این سناریو اجرای API interop پیمایش تودرتو در سمت Compose را پوشش میدهد - زمانی که یک Composable والدین حاوی AndroidView
فرزند دارید. AndroidView
NestedScrollDispatcher
را پیادهسازی میکند، زیرا بهعنوان فرزند برای یک والد پیمایشی Compose و همچنین NestedScrollingParent3
عمل میکند، زیرا بهعنوان والد برای یک فرزند درحال پیمایش View
عمل میکند. سپس والد نوشتن میتواند دلتاهای پیمایش تودرتو را از View
فرزند قابل پیمایش تودرتو دریافت کند.
مثال زیر نشان میدهد که چگونه میتوانید در این سناریو، به همراه نوار ابزار جمعشده Compose، به interop پیمایش تودرتو دست یابید:
@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) {
// ...
}
}
// ...
}
این مثال نشان می دهد که چگونه می توانید از API با یک اصلاح کننده 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)
}
}
)
}
}
و در نهایت، این مثال نشان می دهد که چگونه API interop پیمایش تودرتو با BottomSheetDialogFragment
برای دستیابی به یک رفتار کشیدن و رد کردن موفقیت آمیز استفاده می شود:
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
، دلتاهای پیمایش روی چنین مؤلفههایی در سیستم پیمایش تودرتو منتشر نمیشوند و دلتاها به NestedScrollConnection
ارائهشده توسط rememberNestedScrollInteropConnection()
نمیرسند، بنابراین آن دلتاها نمیتوانند به مؤلفه View
والد برسید. برای حل این مشکل، مطمئن شوید که اصلاحکنندههای قابل پیمایش را نیز برای این نوع ترکیبپذیرهای تودرتو تنظیم کردهاید. برای اطلاعات بیشتر می توانید به بخش قبلی در مورد پیمایش تودرتو مراجعه کنید.
یک View
والدین غیر همکار که حاوی ComposeView
فرزند است
یک View غیر همکار، نمایشی است که رابط های NestedScrolling
لازم را در سمت View
پیاده سازی نمی کند. توجه داشته باشید که این بدان معنی است که قابلیت همکاری پیمایش تودرتو با این Views
به خوبی کار نمی کند. Views
غیر همکار RecyclerView
و ViewPager2
هستند.
برای شما توصیه می شود
- توجه: وقتی جاوا اسکریپت خاموش است، متن پیوند نمایش داده می شود
- ژست ها را درک کنید
- برای نوشتن،
CoordinatorLayout
مهاجرت کنید - استفاده از Views در Compose