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

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

مسافة الانطباق

تلقائيًا، يضبط كلّ من 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. مُرحّل يتقدّم تلقائيًا مع تأخير لمدة ثانيتَين بين كل تقدّم للصفحة

مراجع إضافية