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

للتنقّل بين المحتوى أفقيًا أو عموديًا، يمكنك استخدام الدالتَين المركّبتَين 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. تطبيق عمليات التحويل على محتوى "التنقّل بين الصفحات"

على سبيل المثال، لضبط عتامة العناصر بناءً على مدى بُعدها عن المركز، يمكنك تغيير 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 إلى محاذاة الصفحات نحو النهاية:

أداة تنقّل بين الصفحات مع مساحة متروكة في البداية تعرض المحتوى محاذيًا للنهاية
الشكل 5. أداة تنقّل بين الصفحات مع مساحة متروكة في البداية

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

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

أداة تنقّل بين الصفحات مع مساحة متروكة في البداية والنهاية تعرض المحتوى في الوسط
الشكل 6. أداة تنقّل بين الصفحات مع مساحة متروكة أفقية

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

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

أداة التنقّل بين الصفحات مع مساحة متروكة في البداية والنهاية تعرض المحتوى بمحاذاة البداية
الشكل 7. أداة تنقّل بين الصفحات مع مساحة متروكة في النهاية

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.

مسافة المحاذاة

By default, HorizontalPager and VerticalPager set the maximum number of pages that a fling gesture can scroll past to one page at a time. لتغيير ذلك، اضبط 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)
    }
}

إنشاء أداة تنقّل بين الصفحات تلقائيًا

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

مثال أساسي

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

@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 كعنصر جذر وتحتوي على Row لترتيب مؤشرات الصفحة أفقيًا.
  • يتم عرض مؤشر الصفحة المخصّص كصف من الدوائر، حيث تمثّل كل دائرة Box مُقتصّة إلى CircleShape صفحة.
  • تكون الدائرة التي تمثّل الصفحة الحالية ملوّنة باللون DarkGray، بينما تكون الدوائر الأخرى باللون LightGray. تحدّد المَعلمة currentPageIndex الدائرة التي تظهر باللون الرمادي الداكن.

النتيجة

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

الشكل 8 يتم الانتقال بين الصفحات تلقائيًا مع تأخير لمدة ثانيتين بين كل صفحة وأخرى.

مراجع إضافية