پیمایش تودرتو سیستمی است که در آن چندین مؤلفه پیمایش که درون یکدیگر قرار دارند، با واکنش به یک اشاره پیمایش و انتقال دلتاهای (تغییرات) پیمایش خود، باهم کار میکنند.
سیستم پیمایش تودرتو امکان هماهنگی بین مؤلفههایی را فراهم میکند که پیمایشپذیر هستند و بهصورت سلسلهمراتبی پیوند داده شدهاند (اغلب با همرسانی والد یکسان). این سیستم محفظههای پیمایش را پیوند میدهد و امکان تعامل با دلتاهای پیمایش را که بین آنها منتشر و همرسانی میشود فراهم میکند.
Compose روشهای متعددی برای مدیریت پیمایش تودرتو بین composableها ارائه میدهد. نمونهای معمول از پیمایش تودرتو، فهرستی درون فهرستی دیگر است و موردی پیچیدهتر، نوار ابزار جمعشدنی است.
پیمایش تودرتوی خودکار
پیمایش تودرتوی ساده به هیچ اقدامی ازسوی شما نیاز ندارد. اشارههایی که کنش پیمایش را آغاز میکنند بهطور خودکار از فرزندان به والدین منتشر میشوند، بهطوریکه وقتی فرزند دیگر نمیتواند پیمایش کند، اشاره توسط عنصر والد آن مدیریت میشود.
پیمایش تودرتوی خودکار توسط برخیاز مؤلفهها و اصلاحکنندههای 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
با تعریف سلسلهمراتب پیمایش تودرتو، انعطافپذیری بیشتری به شما میدهد. As
mentioned in the previous section, some components have built-in nested scroll
support. بااینحال، برای composableهایی که بهطور خودکار پیمایشپذیر نیستند، مانند
Box یا Column، دلتاهای پیمایش در چنین مؤلفههایی در
سیستم پیمایش تودرتو منتشر نخواهند شد و دلتاها به NestedScrollConnection یا
مؤلفه والد نخواهند رسید. برای حل این مشکل، میتوانید از nestedScroll برای اعطای چنین
پشتیبانیای به مؤلفههای دیگر، ازجمله مؤلفههای سفارشی، استفاده کنید.
چرخه پیمایش تودرتو
چرخه پیمایش تودرتو جریان دلتاهای پیمایش است که در درخت سلسلهمراتب ازطریق همه مؤلفهها (یا گرهها) که بخشی از سیستم پیمایش تودرتو هستند، برای مثال بااستفاده از مؤلفهها و اصلاحکنندههای پیمایشپذیر یا nestedScroll، به بالا و پایین ارسال میشوند.
مراحل چرخه پیمایش تودرتو
وقتی رویداد راهانداز (برای مثال، اشاره) توسط مؤلفه پیمایشی شناسایی میشود، قبلاز اینکه کنش پیمایش واقعی راهاندازی شود، دلتاهای تولیدشده به سیستم پیمایش تودرتو ارسال میشوند و سه مرحله را طی میکنند: پیشپیمایش، مصرف گره، و پسپیمایش.
در فاز اول، یعنی پیشاز پیمایش، مؤلفهای که رویداد راهانداز را دریافت کرده است، تغییرات را ازطریق درخت سلسلهمراتبی به بالاترین والد ارسال میکند. سپس رویدادهای دلتا به پایین حباب میشوند، به این معنی که دلتاها از ریشهایترین والد به سمت فرزندی که چرخه پیمایش تودرتو را شروع کرده است، منتشر میشوند.
این کار به والدین پیمایش تودرتو (ترکیبپذیرهایی که از nestedScroll یا
اصلاحکنندههای پیمایشپذیر استفاده میکنند) فرصت میدهد تا قبلاز اینکه خود گره بتواند آن را مصرف کند، کاری با دلتا انجام دهند.
در مرحله مصرف گره، خود گره از هر دلتایی که توسط والدینش استفاده نشده است استفاده خواهد کرد. این زمانی است که حرکت پیمایش واقعاً انجام شده و قابلمشاهده است.
درطول این مرحله، کودک میتواند انتخاب کند که همه یا بخشی از پیمایش باقیمانده را مصرف کند. هرچیزی که باقی بماند دوباره به بالا ارسال میشود تا مرحله پساز پیمایش را طی کند.
درنهایت، در فاز پساز پیمایش، هر چیزی که خود گره مصرف نکرده است دوباره برای مصرف به اجدادش ارسال میشود.
فاز پساز پیمایش به روشی مشابه فاز پیشاز پیمایش کار میکند، که در آن هریک از والدین میتوانند انتخاب کنند که مصرف کنند یا نه.
بهطور مشابه با پیمایش، وقتی اشاره کشیدن تمام میشود، ممکن است قصد کاربر به سرعتی ترجمه شود که برای پرتاب کردن (پیمایش بااستفاده از پویانمایی) محفظه پیمایشپذیر استفاده میشود. پرتاب نیز بخشی از چرخه پیمایش تودرتو است و سرعتهای تولیدشده توسط رویداد کشیدن از مراحل مشابهی عبور میکنند: پیشپرتاب، مصرف گره، و پسپرتاب. توجه داشته باشید که پویانمایی پرتابی فقط با اشاره لمسی مرتبط است و با رویدادهای دیگر، مانند دسترسپذیری یا پیمایش سختافزاری، راهاندازی نمیشود.
شرکت در چرخه پیمایش تودرتو
مشارکت در چرخه بهمعنای رهگیری، مصرف، و گزارش کردن مصرف دلتاها در سلسلهمراتب است. 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
را ببینید.
تغییر اندازه تصویر هنگام پیمایش
وقتی کاربر پیمایش میکند، میتوانید جلوه دیداری پویایی ایجاد کنید که در آن اندازه تصویر براساس موقعیت پیمایش تغییر میکند.
تغییر اندازه تصویر براساس موقعیت پیمایش
این قطعه کد نحوه تغییر اندازه تصویر در LazyColumn را براساس
موقعیت پیمایش عمودی نشان میدهد. وقتی کاربر به پایین پیمایش میکند، تصویر کوچک میشود و وقتی به بالا پیمایش میکند، تصویر بزرگ میشود و در محدوده اندازه حداقل و حداکثر تعریفشده باقی میماند:
@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 } ) } }
نکات کلیدی درباره کد
- این کد از
NestedScrollConnectionبرای رهگیری رویدادهای پیمایش استفاده میکند. onPreScrollتغییر اندازه تصویر را براساس دلتای پیمایش محاسبه میکند.- متغیر حالت
currentImageSizeاندازه فعلی تصویر را ذخیره میکند، که بینminImageSizeوmaxImageSize. imageScaleمحدود شده است و ازcurrentImageSizeمشتق میشود. LazyColumnبراساسcurrentImageSizeانحراف دارد.Imageاز اصلاحکنندهgraphicsLayerبرای اعمال مقیاس محاسبهشده استفاده میکند.-
translationYدرونgraphicsLayerتضمین میکند که تصویر هنگام تغییر مقیاس، بهصورت عمودی در مرکز باقی بماند.
نتیجه
قطعه کد قبلی منجر به جلوه مقیاسبندی تصویر در پیمایش میشود:
تعاملپذیری پیمایش تودرتو
وقتی سعی میکنید عناصر View پیمایشی را در ترکیبشدنیهای پیمایشی تودرتو کنید، یا
برعکس، ممکن است با مشکلاتی مواجه شوید. Most noticeable ones would
happen when you scroll the child and reach its start or end bounds and expect
the parent to take the scrolling over. اما این رفتار موردانتظار ممکن است
اتفاق نیفتد یا آنطور که انتظار میرود کار نکند.
این مشکل ناشی از انتظاراتی است که در ترکیبپذیرهای پیمایشپذیر ایجاد شده است.
ترکیبشدنیهای پیمایشپذیر قانون «nested-scroll-by-default» دارند، یعنی
هر محفظه پیمایشپذیر باید در زنجیره پیمایش تودرتو شرکت کند، هم بهعنوان
والد ازطریق
NestedScrollConnection،
و هم بهعنوان فرزند ازطریق
NestedScrollDispatcher.
سپس وقتی فرزند در مرز قرار دارد، فرزند پیمایش تودرتو را برای ولی هدایت میکند. بهعنوان مثال، این قانون به Compose Pager و Compose LazyRow اجازه میدهد
بهخوبی باهم کار کنند. بااینحال، وقتی پیمایش همکنشپذیری با ViewPager2 یا RecyclerView انجام میشود، چون اینها NestedScrollingParent3 را پیادهسازی نمیکنند، پیمایش پیوسته از فرزند به والد امکانپذیر نیست.
برای فعال کردن میانای برنامهسازی کاربردی تعاملپذیری پیمایش تودرتو بین عناصر View پیمایشپذیر و
ترکیبپذیرهای پیمایشپذیر، تودرتو در هر دو جهت، میتوانید از میانای برنامهسازی کاربردی تعاملپذیری پیمایش تودرتو
برای کاهش این مشکلات در سناریوهای زیر استفاده کنید.
والد همکار View حاوی فرزند ComposeView
والد همکار View والدی است که ازقبل
NestedScrollingParent3
را پیادهسازی میکند و بنابراین میتواند دلتاهای پیمایش را از فرزند تودرتوی همکار قابلساخت دریافت کند. ComposeView در این مورد بهعنوان فرزند عمل میکند و باید
NestedScrollingChild3 را (بهطور غیرمستقیم)
پیادهسازی کند.
یک نمونه از ولی همکار
androidx.coordinatorlayout.widget.CoordinatorLayout است.
اگر به قابلیت همکاری پیمایش تودرتو بین محفظههای ولی View پیمایشی و ترکیبشدنیهای فرزند تودرتو پیمایشی نیاز دارید، میتوانید از rememberNestedScrollInteropConnection() استفاده کنید.
rememberNestedScrollInteropConnection()
NestedScrollConnection
را مجاز میکند و بهخاطر میسپارد که امکان تعاملپذیری پیمایش تودرتو را بین View والدی که
NestedScrollingParent3
را پیادهسازی میکند و فرزند Compose فراهم میکند. این باید همراه با اصلاحکننده
nestedScroll
استفاده شود. ازآنجاییکه پیمایش تودرتو بهطور پیشفرض در Compose فعال است، میتوانید از این اتصال برای فعال کردن پیمایش تودرتو در View و افزودن منطق چسب ضروری بین Views و composable استفاده کنید.
یک مورد استفاده متداول استفاده از 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>
In your Activity or Fragment, you need to set up your child composable and the
required
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 را در سمت Compose پوشش میدهد - زمانی که یک composable والد حاوی یک AndroidView فرزند دارید. AndroidView
NestedScrollDispatcher را پیادهسازی میکند،
زیرا بهعنوان فرزند برای والد پیمایشی Compose عمل میکند، و همچنین
NestedScrollingParent3
را پیادهسازی میکند، زیرا بهعنوان والد برای فرزند پیمایشی View عمل میکند. سپس Compose parent میتواند دلتاهای پیمایش تودرتو را از فرزند پیمایشپذیر تودرتو
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( 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) { // ... } } // ... }
این مثال نشان میدهد که چگونه میتوانید از 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(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
استفاده میشود تا رفتار کشیدن و رد کردن موفقیتآمیز حاصل شود:
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
«نمای» غیرهمکار نمایی است که میاناهای NestedScrolling لازم را در طرف View پیادهسازی نمیکند. توجه داشته باشید که این یعنی
تعاملپذیری پیمایش تودرتو با این Views بهطور پیشفرض
کار نمیکند. Views غیرهمکار RecyclerView و ViewPager2 هستند.
منابع بیشتر
توصیهشده برای شما
- توجه: وقتی جاوا اسکریپت خاموش باشد، نوشتار پیوند نمایش داده میشود
- آشنایی با اشارهها
- انتقال
CoordinatorLayoutبه Compose - استفاده از «نماها» در Compose