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

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