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

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

أداة تقسيم الصفحات مع مساحة بادئة تعرض المحتوى محاذيًا للنهاية
الشكل 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.

مسافة الالتقاط

بشكلٍ تلقائي، يضبط كلّ من 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)
    }
}

إنشاء أداة عرض صفحات متقدّمة تلقائيًا

يوضّح هذا القسم كيفية إنشاء أداة عرض صفحات تقدّم تلقائيًا مع مؤشرات الصفحات في 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 أداة تنقل تلقائي بين الصفحات مع تأخير لمدة ثانيتين بين كل انتقال إلى صفحة أخرى

مراجع إضافية