جهاز نداء في "إنشاء"

لقلب المحتوى لليسار واليمين أو للأعلى والأسفل، يمكنك استخدام العنصرين التاليين: HorizontalPager و VerticalPager على التوالي. تؤدي هذه العناصر القابلة للتجميع وظائف مشابهة لتلك التي يؤديها رمز ViewPager في نظام العرض. يشغل الرمز HorizontalPager تلقائيًا عرض الشاشة بالكامل، ويشغل الرمز VerticalPager الارتفاع بالكامل، ولا تعرِض أدوات التنقّل بين الصفحات سوى صفحة واحدة في كل مرة. ويمكن ضبط كل هذه الإعدادات التلقائية.

HorizontalPager

لإنشاء شريط تنقّل يتيح الانتقال أفقيًا لليسار واليمين، استخدِم HorizontalPager:

الشكل 1. عرض توضيحي لـ HorizontalPager

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

VerticalPager

لإنشاء شريط تنقّل يتيح الانتقال للأعلى وللأسفل، استخدِم VerticalPager:

الشكل 2. عرض توضيحي لـ VerticalPager

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
VerticalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

إنشاء المحتوى بدون بذل الكثير من الجهد

يتم تجميع الصفحات في كلٍّ من HorizontalPager وVerticalPager ببطء وتنسيقها عند الحاجة. بينما يتنقّل المستخدم في الصفحات، تزيل الشبكة القابلة للتجميع أي صفحات لم تعد مطلوبة.

تحميل المزيد من الصفحات خارج الشاشة

لا تحمِّل ميزة "تنقّل الصفحات" تلقائيًا سوى الصفحات المرئية على الشاشة. لتحميل المزيد من الصفحات خارج الشاشة، اضبط beyondBoundsPageCount على قيمة أكبر من صفر.

الانتقال إلى عنصر في شريط التنقل

للانتقال إلى صفحة معيّنة في أداة التنقّل، أنشئ عنصرًا PagerState باستخدام rememberPagerState() وقدِّمه كمعلمة state لأداة التنقّل. يمكنك الاتصال بـ PagerState#scrollToPage() في هذه الحالة، داخل CoroutineScope:

val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.scrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

إذا كنت تريد إضافة تأثير متحرك إلى الصفحة، استخدِم الدالة PagerState#animateScrollToPage():

val pagerState = rememberPagerState(pageCount = {
    10
})

HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.animateScrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

تلقّي إشعارات بشأن تغييرات حالة الصفحة

يحتوي PagerState على ثلاث سمات تتضمّن معلومات عن الصفحات: currentPage، settledPage، و targetPage.

  • currentPage: أقرب صفحة إلى موضع التصاق المحتوى يكون موضع التصاق تلقائيًا في بداية التنسيق.
  • settledPage: رقم الصفحة عندما لا يكون هناك حركة أو انتقال إلى أعلى أو أسفل الصفحة يختلف هذا الإعداد عن السمة currentPage من حيث أنّ السمة currentPage يتم تعديلها على الفور إذا كانت الصفحة قريبة بما يكفي من موضع التصاق الصورة، ولكن تظل السمة settledPage كما هي إلى أن تنتهي جميع الصور المتحركة.
  • targetPage: موضع التوقف المقترَح لحركة التمرير

يمكنك استخدام الدالة snapshotFlow لمراقبة التغييرات في هذه المتغيّرات والاستجابة لها. على سبيل المثال، لإرسال حدث إحصاءات عند كل تغيير في الصفحة، يمكنك إجراء ما يلي:

val pagerState = rememberPagerState(pageCount = {
    10
})

LaunchedEffect(pagerState) {
    // Collect from the a snapshotFlow reading the currentPage
    snapshotFlow { pagerState.currentPage }.collect { page ->
        // Do something with each page change, for example:
        // viewModel.sendPageSelectedEvent(page)
        Log.d("Page change", "Page changed to $page")
    }
}

VerticalPager(
    state = pagerState,
) { page ->
    Text(text = "Page: $page")
}

إضافة مؤشر صفحة

لإضافة مؤشر إلى صفحة، استخدِم العنصر PagerState للحصول على معلومات حول الصفحة التي تم اختيارها من بين عدد الصفحات، وارسم مؤشرك المخصّص.

على سبيل المثال، إذا كنت تريد مؤشرًا دائريًا بسيطًا، يمكنك تكرار عدد الدوائر وتغيير لون الدائرة استنادًا إلى ما إذا كانت الصفحة محدّدة أم لا، باستخدام pagerState.currentPage:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    modifier = Modifier.fillMaxSize()
) { page ->
    // Our page content
    Text(
        text = "Page: $page",
    )
}
Row(
    Modifier
        .wrapContentHeight()
        .fillMaxWidth()
        .align(Alignment.BottomCenter)
        .padding(bottom = 8.dp),
    horizontalArrangement = Arrangement.Center
) {
    repeat(pagerState.pageCount) { iteration ->
        val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
        Box(
            modifier = Modifier
                .padding(2.dp)
                .clip(CircleShape)
                .background(color)
                .size(16.dp)
        )
    }
}

مؤشر صفحات يعرض سهمًا دائريًا أسفل المحتوى
الشكل 3. مؤشر صفحات يعرض سهمًا دائريًا أسفل المحتوى

تطبيق تأثيرات التمرير في العناصر على المحتوى

ومن حالات الاستخدام الشائعة استخدام موضع التمرير لتطبيق تأثيرات على عناصر ملف التمرير. لمعرفة مدى قرب صفحة معيّنة من الصفحة المحدّدة حاليًا، يمكنك استخدام PagerState.currentPageOffsetFraction. يمكنك بعد ذلك تطبيق تأثيرات التحويل على المحتوى استنادًا إلى المسافة من الصفحة المحدّدة.

الشكل 4. تطبيق عمليات التحويل على محتوى Pager

على سبيل المثال، لتعديل مستوى شفافية العناصر استنادًا إلى المسافة بينها وبين المركز، يمكنك تغيير alpha باستخدام Modifier.graphicsLayer على عنصر داخل شريط التنقل:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(state = pagerState) { page ->
    Card(
        Modifier
            .size(200.dp)
            .graphicsLayer {
                // Calculate the absolute offset for the current page from the
                // scroll position. We use the absolute value which allows us to mirror
                // any effects for both directions
                val pageOffset = (
                    (pagerState.currentPage - page) + pagerState
                        .currentPageOffsetFraction
                    ).absoluteValue

                // We animate the alpha, between 50% and 100%
                alpha = lerp(
                    start = 0.5f,
                    stop = 1f,
                    fraction = 1f - pageOffset.coerceIn(0f, 1f)
                )
            }
    ) {
        // Card content
    }
}

أحجام الصفحات المخصّصة

يشغل الرمزان HorizontalPager وVerticalPager تلقائيًا العرض الكامل أو الارتفاع الكامل، على التوالي. يمكنك ضبط المتغيّر pageSize على استخدام إما Fixed أو Fill (الإعداد التلقائي) أو حساب حجم مخصّص.

على سبيل المثال، لضبط صفحة بعرض ثابت يبلغ 100.dp:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp)
) { page ->
    // page content
}

لتحديد حجم الصفحات استنادًا إلى حجم إطار العرض، استخدِم عملية حساب مخصّصة لحجم الصفحة. أنشئ كائنًا مخصّصًا PageSize وقسِّم availableSpace على ثلاثة، مع مراعاة المسافة بين العناصر:

private val threePagesPerViewport = object : PageSize {
    override fun Density.calculateMainAxisPageSize(
        availableSpace: Int,
        pageSpacing: Int
    ): Int {
        return (availableSpace - 2 * pageSpacing) / 3
    }
}

تعبئة المحتوى

يتيح كلّ من HorizontalPager وVerticalPager تغيير سمة "ملء المحتوى"، ما يتيح لك التأثير في الحد الأقصى لحجم الصفحات ومحاذاتها.

على سبيل المثال، يؤدي ضبط سمة الحشو start إلى محاذاة الصفحات باتجاه النهاية:

فهرس صفحات مع مساحة تملأ المساحة الفارغة في بداية الصفحة ويعرض المحتوى على يمين الصفحة

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(start = 64.dp),
) { page ->
    // page content
}

يؤدي ضبط كلٍّ من الفراغين start وend على القيمة نفسها إلى وضع العنصر في منتصف الشاشة أفقيًا:

فهرس صفحات مع مساحة فارغة في بداية الصفحة ونهايتها يعرض المحتوى في المنتصف

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(horizontal = 32.dp),
) { page ->
    // page content
}

يؤدي ضبط سمة الحشو end إلى محاذاة الصفحات باتجاه البداية:

فهرس صفحات يتضمّن مساحة فارغة في بداية الصفحة وآخرها ويعرض المحتوى على يمين الصفحة

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(end = 64.dp),
) { page ->
    // page content
}

يمكنك ضبط قيم top وbottom لتحقيق تأثيرات مشابهة لمحاولة VerticalPager. لا تُستخدَم القيمة 32.dp هنا إلا كمثال، ويمكنك ضبط كل سمة من سمات الحشو على أي قيمة.

تخصيص سلوك الانتقال للأعلى أو للأسفل

تحدِّد العناصر القابلة للتجميع التلقائية HorizontalPager وVerticalPager كيفية عمل إيماءات التمرير مع عارض الصفحة. ومع ذلك، يمكنك تخصيص الإعدادات التلقائية وتغييرها، مثل pagerSnapDistance أو flingBehavior.

مسافة اللقطة

يحدِّد الزرَّان HorizontalPager وVerticalPager تلقائيًا الحد الأقصى لعدد الصفحات التي يمكن أن تتخطّاها إيماءة التمرير السريع لصفحة واحدة في المرة الواحدة. لتغيير هذا الإعداد، اضبط pagerSnapDistance على flingBehavior:

val pagerState = rememberPagerState(pageCount = { 10 })

val fling = PagerDefaults.flingBehavior(
    state = pagerState,
    pagerSnapDistance = PagerSnapDistance.atMost(10)
)

Column(modifier = Modifier.fillMaxSize()) {
    HorizontalPager(
        state = pagerState,
        pageSize = PageSize.Fixed(200.dp),
        beyondViewportPageCount = 10,
        flingBehavior = fling
    ) {
        PagerSampleItem(page = it)
    }
}

إنشاء جدول زمني للتنقّل التلقائي

يوضّح هذا القسم كيفية إنشاء فهرس صفحات ينتقل تلقائيًا باستخدام مؤشرات الصفحات في ميزة "الإنشاء". يتم تلقائيًا التمرير في مجموعة العناصر أفقيًا، ولكن يمكن للمستخدمين أيضًا التمرير سريعًا يدويًا بين العناصر. إذا تفاعل مستخدم مع ميزة "تنقّل سريع"، سيتم إيقاف التقدّم التلقائي.

مثال أساسي

معًا، تنشئ المقتطفات التالية رمزًا أساسيًا لوحدة تنقّل متقدّمة تلقائيًا مع مؤشر مرئي، يتم فيه عرض كل صفحة بأحد الألوان المختلفة:

@Composable
fun AutoAdvancePager(pageItems: List<Color>, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        val pagerState = rememberPagerState(pageCount = { pageItems.size })
        val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState()

        val pageInteractionSource = remember { MutableInteractionSource() }
        val pageIsPressed by pageInteractionSource.collectIsPressedAsState()

        // Stop auto-advancing when pager is dragged or one of the pages is pressed
        val autoAdvance = !pagerIsDragged && !pageIsPressed

        if (autoAdvance) {
            LaunchedEffect(pagerState, pageInteractionSource) {
                while (true) {
                    delay(2000)
                    val nextPage = (pagerState.currentPage + 1) % pageItems.size
                    pagerState.animateScrollToPage(nextPage)
                }
            }
        }

        HorizontalPager(
            state = pagerState
        ) { page ->
            Text(
                text = "Page: $page",
                textAlign = TextAlign.Center,
                modifier = modifier
                    .fillMaxSize()
                    .background(pageItems[page])
                    .clickable(
                        interactionSource = pageInteractionSource,
                        indication = LocalIndication.current
                    ) {
                        // Handle page click
                    }
                    .wrapContentSize(align = Alignment.Center)
            )
        }

        PagerIndicator(pageItems.size, pagerState.currentPage)
    }
}

النقاط الرئيسية حول الرمز

  • تنشئ الدالة AutoAdvancePager عرضًا للتنقّل أفقيًا مع التقدم التلقائي. يأخذ هذا المقياس قائمة بعناصر Color كمدخلات، والتي يتم استخدامها كألوان خلفية لكل صفحة.
  • يتم إنشاء pagerState باستخدام rememberPagerState، الذي يحتفظ بالحالة لصفحة الفوترة.
  • تتتبّع pagerIsDragged وpageIsPressed تفاعل المستخدم.
  • ينقل الزر LaunchedEffect مؤشر التنقل تلقائيًا كل ثانيتَين ما لم يسحب المستخدِم مؤشر التنقل أو يضغط على إحدى الصفحات.
  • يعرض HorizontalPager قائمة بالصفحات، ولكل صفحة Text قابل للتركيب يعرض رقم الصفحة. يملأ المُعدِّل الصفحة ويضبط لون الخلفية من pageItems ويجعل الصفحة قابلة للنقر.

@Composable
fun PagerIndicator(pageCount: Int, currentPageIndex: Int, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        Row(
            modifier = Modifier
                .wrapContentHeight()
                .fillMaxWidth()
                .align(Alignment.BottomCenter)
                .padding(bottom = 8.dp),
            horizontalArrangement = Arrangement.Center
        ) {
            repeat(pageCount) { iteration ->
                val color = if (currentPageIndex == iteration) Color.DarkGray else Color.LightGray
                Box(
                    modifier = modifier
                        .padding(2.dp)
                        .clip(CircleShape)
                        .background(color)
                        .size(16.dp)
                )
            }
        }
    }
}

النقاط الرئيسية حول الرمز

  • يتم استخدام عنصر Box قابل للتجميع كعنصر الجذر.
    • داخل Box، يُرتِّب الرمز القابل للتجميع Row مؤشرات الصفحة أفقيًا.
  • يتم عرض مؤشر صفحة مخصّص كصف من الدوائر، حيث يمثّل كل Box مُقتطع إلى circle صفحة.
  • تكون دائرة الصفحة الحالية ملونة على شكل DarkGray، بينما تكون الدوائر الأخرى LightGray. تحدِّد المَعلمة currentPageIndex الدائرة التي يتم عرضها باللون الرمادي الداكن.

النتيجة

يعرض هذا الفيديو ملف التنقّل الأساسي للتقدم التلقائي من المقتطفات السابقة:

الشكل 1. أداة تنقّل تلقائي للصفحات مع تأخير لمدة ثانيتَين بين كل انتقال إلى صفحة أخرى