זימונית ב'כתיבה'

כדי להחליף בין התוכן אופקית או אנכית, אפשר להשתמש ברכיבי ה-Composable‏ 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 כדי לעקוב אחרי השינויים במשתנים האלה ולהגיב להם. לדוגמה, כדי לשלוח אירוע Analytics בכל שינוי בדף, אפשר לעשות את הפעולות הבאות:

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. רכיב החלפת דפים עם עיגול שמציין את הדף הנוכחי מתחת לתוכן

החלת אפקטים של גלילה בפריט על התוכן

תרחיש נפוץ לשימוש במיקום הגלילה הוא החלת אפקטים על פריטים של רכיב Pager. כדי לדעת כמה דפים מפרידים בין דף מסוים לבין הדף שנבחר, אפשר להשתמש בPagerState.currentPageOffsetFraction. לאחר מכן תוכלו להחיל על התוכן אפקטים של טרנספורמציה על סמך המרחק מהדף שנבחר.

איור 4. החלת שינויים בתוכן של Pager

לדוגמה, כדי לשנות את השקיפות של פריטים בהתאם למרחק שלהם מהמרכז, משנים את alpha באמצעות Modifier.graphicsLayer בפריט בתוך רכיב ה-pager:

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, הפריט ימוקם במרכז בצורה אופקית:

רכיב Pager עם מרווחים בהתחלה ובסוף, שבהם התוכן מוצג במרכז
איור 6. תצוגת דפים עם מרווח פנימי אופקי.

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

הגדרת הריווח הפנימי end מיישרת את הדפים לכיוון ההתחלה:

רכיב Pager עם ריווח בהתחלה ובסוף, שבו התוכן מיושר להתחלה
איור 7. רכיב Pager עם ריווח פנימי בסוף.

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

אפשר להגדיר את הערכים top ו-bottom כדי להשיג השפעות דומות על VerticalPager. הערך 32.dp משמש כאן רק כדוגמה. אתם יכולים להגדיר כל ערך לכל אחד מהמאפיינים של הריווח הפנימי.

התאמה אישית של התנהגות הגלילה

רכיבי ה-Composable HorizontalPager ו-VerticalPager שמוגדרים כברירת מחדל מציינים איך תנועות גלילה פועלות עם רכיב ה-Pager. עם זאת, אפשר להתאים אישית ולשנות את ברירות המחדל, כמו 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. מנגנון החלפת דפים אוטומטי עם השהיה של שתי שניות בין כל מעבר לדף הבא.

מקורות מידע נוספים