اسکرول، اسکرول

اصلاح‌کننده‌های اسکرول

اصلاح‌کننده‌های 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 با اصلاحگرهای scroll متفاوت است، زیرا scrollable حرکات اسکرول را تشخیص داده و دلتاها را ثبت می‌کند، اما محتویات آن را به طور خودکار جابجا نمی‌کند. در عوض، این کار از طریق ScrollableState به کاربر واگذار می‌شود که برای عملکرد صحیح این اصلاحگر ضروری است.

هنگام ساخت ScrollableState ، باید یک تابع consumeScrollDelta ارائه دهید که در هر مرحله اسکرول (با ورودی حرکتی، اسکرول نرم یا پرتاب کردن) با دلتا بر حسب پیکسل فراخوانی می‌شود. این تابع باید مقدار فاصله اسکرول مصرفی را برگرداند تا اطمینان حاصل شود که رویداد در مواردی که عناصر تو در تو با اصلاح‌کننده scrollable وجود دارند، به درستی منتشر می‌شود.

قطعه کد زیر حرکات را تشخیص می‌دهد و یک مقدار عددی برای یک جابجایی نمایش می‌دهد، اما هیچ عنصری را جابجا نمی‌کند:

@Composable
private fun ScrollableSample() {
    // actual composable state
    var offset by remember { mutableFloatStateOf(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 روش‌های متعددی برای مدیریت پیمایش تو در تو بین composableها ارائه می‌دهد. یک مثال معمول از پیمایش تو در تو، یک لیست درون لیست دیگر است و یک مورد پیچیده‌تر، جمع شدن نوار ابزار است.

پیمایش خودکار تو در تو

پیمایش تو در تو ساده نیازی به هیچ اقدامی از جانب شما ندارد. حرکاتی که یک عمل پیمایش را آغاز می‌کنند، به طور خودکار از عناصر فرزند به عناصر والد منتقل می‌شوند، به طوری که وقتی عنصر فرزند نمی‌تواند بیشتر پیمایش کند، حرکت توسط عنصر والد آن مدیریت می‌شود.

پیمایش خودکار تو در تو توسط برخی از کامپوننت‌ها و اصلاح‌کننده‌های Compose پشتیبانی و ارائه می‌شود: verticalScroll ، horizontalScroll ، scrollable ، Lazy APIs و 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 .

Phases of nested scrolling cycle

وقتی یک رویداد محرک (مثلاً یک حرکت) توسط یک مؤلفه‌ی قابل پیمایش تشخیص داده می‌شود، قبل از اینکه عمل پیمایش واقعی حتی آغاز شود، دلتاهای تولید شده به سیستم پیمایش تودرتو ارسال می‌شوند و سه مرحله را طی می‌کنند: پیش پیمایش، مصرف گره و پس پیمایش.

مراحل چرخه پیمایش تو در تو

در مرحله اول، پیش از اسکرول، کامپوننتی که دلتاهای رویداد تریگر را دریافت کرده است، آن رویدادها را از طریق درخت سلسله مراتبی به بالاترین والد ارسال می‌کند. سپس رویدادهای دلتا به صورت حبابی پایین می‌آیند، به این معنی که دلتاها از والد ریشه به سمت فرزندی که چرخه اسکرول تو در تو را آغاز کرده است، منتشر می‌شوند.

مرحله پیش از اسکرول - اعزام به بالا

این به والدهای تودرتوی اسکرول (کامپوزبل‌هایی که از اصلاح‌کننده‌های nestedScroll یا scrollable استفاده می‌کنند) این فرصت را می‌دهد که قبل از اینکه خود گره بتواند دلتا را مصرف کند، کاری با آن انجام دهند.

مرحله پیش از اسکرول - حباب زدن به پایین

در مرحله مصرف گره، خود گره از هر دلتایی که توسط والدینش استفاده نشده است، استفاده خواهد کرد. این زمانی است که حرکت پیمایش واقعاً انجام شده و قابل مشاهده است.

فاز مصرف گره

در طول این مرحله، کودک می‌تواند تمام یا بخشی از طومار باقی‌مانده را مصرف کند. هر چیزی که باقی بماند، برای عبور از مرحله پس از طومار، به بالا فرستاده می‌شود.

در نهایت، در مرحله پس از پیمایش، هر چیزی که خود گره مصرف نکرده باشد، دوباره برای مصرف به اجدادش ارسال می‌شود.

مرحله پس از پیمایش - اعزام به بالا

مرحله پس از پیمایش به روشی مشابه مرحله قبل از پیمایش عمل می‌کند، که در آن هر یک از والدین می‌توانند مصرف کنند یا نکنند.

مرحله پس از اسکرول - حباب زدن به پایین

مشابه اسکرول، وقتی یک حرکت کشیدن (drag) تمام می‌شود، هدف کاربر ممکن است به سرعتی تبدیل شود که برای پرتاب کردن (پیمایش با استفاده از انیمیشن) کانتینر قابل پیمایش استفاده می‌شود. پرتاب کردن همچنین بخشی از چرخه پیمایش تو در تو است و سرعت‌های ایجاد شده توسط رویداد کشیدن، مراحل مشابهی را طی می‌کنند: پیش از پرتاب کردن، مصرف گره و پس از پرتاب کردن. توجه داشته باشید که انیمیشن پرتاب کردن فقط با حرکت لمسی مرتبط است و توسط رویدادهای دیگر مانند a11y یا پیمایش سخت‌افزاری فعال نمی‌شود.

در چرخه پیمایش تو در تو شرکت کنید

مشارکت در چرخه به معنای رهگیری، مصرف و گزارش مصرف دلتاها در امتداد سلسله مراتب است. Compose مجموعه‌ای از ابزارها را برای تأثیرگذاری بر نحوه عملکرد سیستم پیمایش تو در تو و نحوه تعامل مستقیم با آن فراهم می‌کند، به عنوان مثال وقتی که شما نیاز دارید کاری با دلتاهای پیمایش قبل از اینکه یک کامپوننت پیمایش‌پذیر حتی شروع به پیمایش کند، انجام دهید.

اگر چرخه پیمایش تو در تو سیستمی باشد که بر روی زنجیره‌ای از گره‌ها عمل می‌کند، اصلاحگر nestedScroll راهی برای رهگیری و درج در این تغییرات و تأثیرگذاری بر داده‌هایی (دلتاهای پیمایش) است که در زنجیره منتشر می‌شوند. این اصلاحگر می‌تواند در هر جایی از سلسله مراتب قرار گیرد و با نمونه‌های اصلاحگر پیمایش تو در تو در بالای درخت ارتباط برقرار می‌کند تا بتواند اطلاعات را از طریق این کانال به اشتراک بگذارد. بلوک‌های سازنده این اصلاحگر عبارتند از NestedScrollConnection و NestedScrollDispatcher .

NestedScrollConnection روشی برای پاسخ به مراحل چرخه پیمایش تودرتو و تأثیرگذاری بر سیستم پیمایش تودرتو ارائه می‌دهد. این روش از چهار متد فراخوانی تشکیل شده است که هر کدام نمایانگر یکی از مراحل مصرف هستند: pre/post-scroll و pre/post-fling:

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
            }
        }
    }
}

تمام callbackها اطلاعاتی در مورد نوع NestedScrollSource ارائه می‌دهند.

NestedScrollDispatcher چرخه پیمایش تو در تو را مقداردهی اولیه می‌کند. استفاده از یک dispatcher و فراخوانی متدهای آن، چرخه را فعال می‌کند. کانتینرهای قابل پیمایش دارای یک dispatcher داخلی هستند که دلتاهای گرفته شده در طول حرکات را به سیستم ارسال می‌کند. به همین دلیل، اکثر موارد استفاده از سفارشی‌سازی پیمایش تو در تو شامل استفاده از NestedScrollConnection به جای dispatcher است تا به دلتاهای موجود واکنش نشان دهد و نه ارسال دلتاهای جدید. برای کاربردهای بیشتر به 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 پیمایش‌پذیر را در کامپوننت‌های پیمایش‌پذیر تودرتو کنید، یا برعکس، ممکن است با مشکلاتی مواجه شوید. قابل توجه‌ترین این مشکلات زمانی اتفاق می‌افتد که عنصر فرزند را پیمایش می‌کنید و به مرزهای شروع یا پایان آن می‌رسید و انتظار دارید که عنصر والد پیمایش را به عهده بگیرد. با این حال، این رفتار مورد انتظار یا ممکن است اتفاق نیفتد یا ممکن است مطابق انتظار عمل نکند.

این مشکل نتیجه‌ی انتظاراتی است که در کامپوننت‌های اسکرول‌شونده وجود دارد. کامپوننت‌های اسکرول‌شونده دارای قانون "اسکرول تو در تو به صورت پیش‌فرض" هستند، به این معنی که هر کانتینر اسکرول‌شونده باید در زنجیره اسکرول تو در تو، هم به عنوان والد از طریق NestedScrollConnection و هم به عنوان فرزند از طریق NestedScrollDispatcher ، شرکت کند. سپس فرزند وقتی به مرز می‌رسد، یک اسکرول تو در تو برای والد هدایت می‌کند. به عنوان مثال، این قانون به Compose Pager و Compose LazyRow اجازه می‌دهد تا به خوبی با هم کار کنند. با این حال، هنگامی که اسکرول کردن با قابلیت همکاری با ViewPager2 یا RecyclerView انجام می‌شود، از آنجایی که این کامپوننت‌ها NestedScrollingParent3 پیاده‌سازی نمی‌کنند، اسکرول مداوم از فرزند به والد امکان‌پذیر نیست.

برای فعال کردن API interop پیمایش تو در تو بین عناصر View پیمایش‌پذیر و ترکیب‌های پیمایش‌پذیر، که در هر دو جهت تو در تو هستند، می‌توانید از API interop پیمایش تو در تو برای کاهش این مشکلات، در سناریوهای زیر استفاده کنید.

یک View والد همکار که شامل یک نمای فرزند ComposeView است.

یک والد همکار، View ای است که از قبل NestedScrollingParent3 پیاده‌سازی کرده است و بنابراین قادر به دریافت دلتاهای پیمایشی از یک فرزند همکار تو در تو با composable است. ComposeView در این مورد به عنوان یک فرزند عمل می‌کند و باید (به طور غیرمستقیم) NestedScrollingChild3 پیاده‌سازی کند. یک نمونه از والد همکار androidx.coordinatorlayout.widget.CoordinatorLayout است.

اگر به قابلیت همکاری پیمایش تو در تو بین View والد قابل پیمایش و کامپوننت‌های فرزند قابل پیمایش تو در تو نیاز دارید، می‌توانید از rememberNestedScrollInteropConnection() استفاده کنید.

rememberNestedScrollInteropConnection() NestedScrollConnection که قابلیت همکاری پیمایش تو در تو را بین یک والد View که NestedScrollingParent3 پیاده‌سازی می‌کند و یک فرزند Compose فعال می‌کند، مجاز و به خاطر می‌سپارد. این باید همراه با یک اصلاح‌کننده nestedScroll استفاده شود. از آنجایی که پیمایش تو در تو به طور پیش‌فرض در سمت Compose فعال است، می‌توانید از این اتصال برای فعال کردن پیمایش تو در تو در سمت View و اضافه کردن منطق اتصال لازم بین Views و composableها استفاده کنید.

یک مورد استفاده‌ی رایج، استفاده از CoordinatorLayout ، CollapsingToolbarLayout و یک child 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 خود، باید child 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 فرزند پیمایشی عمل می‌کند. سپس 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) {
            // ...
        }
    }
    // ...
}

این مثال نشان می‌دهد که چگونه می‌توانید از 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 است. این امر عنصر را قادر می‌سازد تا در پیمایش تو در تو شرکت کند، اما پیمایش خودکار عناصر را فعال نمی‌کند. برای composableهایی که به طور خودکار پیمایش نمی‌شوند، مانند Box یا Column ، دلتاهای پیمایش روی چنین کامپوننت‌هایی در سیستم پیمایش تو در تو منتشر نمی‌شوند و دلتاها به NestedScrollConnection ارائه شده توسط rememberNestedScrollInteropConnection() نمی‌رسند، بنابراین آن دلتاها به کامپوننت View والد نمی‌رسند. برای حل این مشکل، مطمئن شوید که اصلاح‌کننده‌های پیمایش را نیز برای این نوع composableهای تو در تو تنظیم کرده‌اید. برای اطلاعات دقیق‌تر می‌توانید به بخش قبلی در مورد پیمایش تو در تو مراجعه کنید.

یک View والد غیر همکار که حاوی یک نمای فرزند ComposeView است

یک نمای غیر همکار، نمایی است که رابط‌های NestedScrolling لازم را در سمت View پیاده‌سازی نمی‌کند. توجه داشته باشید که این بدان معناست که قابلیت همکاری پیمایش تو در تو با این Views به طور خودکار کار نمی‌کند. Views غیر همکار عبارتند از RecyclerView و ViewPager2 .

منابع اضافی

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}