Search code examples
androidkotlinpaginationandroid-jetpack-composehorizontal-scrolling

Text pagination in Jetpack Compose


PROBLEM STATEMENT

From the backend, there is a large text observed which is basically a story. Showing that text inside Text. And I'm not putting that Text inside any LazyColumn since I want the HorizontalPager behaviour. So user can swipe from right-to-left to see the remaning content.

To solve this problem, I have written the solution like that

Initially,making the default value of page count to 1

 var pageCount by remember { mutableIntStateOf(1) }
 val pagerState = rememberPagerState(pageCount = { pageCount })

Creating the HorizontalPager like that

val linesPerPage = remember { mutableStateListOf<Int>() }
val remainingText = remember { mutableStateOf("") }

    HorizontalPager(state = pagerState) {
        Column(
                modifier = modifier
                    .fillMaxSize()
                    .padding(bottom = innerPadding.calculateBottomPadding())
            ) {
                showLog("PAGE COUNT is $pageCount")
                if (pagerState.currentPage == 0) {
                    Spacer(modifier = modifier.padding(top = innerPadding.calculateTopPadding()))
                    TitleView(title = data.title, modifier = modifier)
                    AuthorNameTextView(name = data.author.name, modifier = modifier)
                }
                StoryContentView(
                    story = dummyStory,
                    textSize = textSize,
                    textStyle = selectedTextStyle,
                    onTextLayout = { result ->
                        val lastVisibleLineIndex =
                            result.getLineForVerticalPosition(result.layoutInput.constraints.maxHeight.toFloat())
                        val lastVisibleLineStart = result.getLineStart(lastVisibleLineIndex)
                        val lastVisibleLineEnd = result.getLineEnd(lastVisibleLineIndex)
                        val lastVisibleLineText =
                            dummyStory.substring(lastVisibleLineStart, lastVisibleLineEnd)
                        showLog("REMAINING TEXT: $lastVisibleLineText")
                        if (result.didOverflowHeight) {
                            // Check if this is a new page or the current page is overflowing
                            if (linesPerPage.isEmpty() || linesPerPage.last() != lastVisibleLineIndex) {
                                // New page or current page is overflowing
                                if (!linesPerPage.contains(lastVisibleLineIndex)) {
                                    pageCount++
                                    linesPerPage.add(lastVisibleLineIndex)
                                    remainingText.value = lastVisibleLineText
                                }
                            }
                        }
                    }
                )
            }
        }

In this StoryContentView is nothing but simple Text of Jetpack compose.

Here, requirement is when result.didOverflowHeight returns true in that case I will go to add one more page. Now when I land to page2 then I want to show the remaning text there and if the content is again overflowed in the page2 then it append the pageCount and then it went to page3 and so on, unless I reached the completion of content.

In this code, I'm able to get the REMANING ALL TEXT but how to show in the other page, I'm struggling there.

For this I have taken dummyStory as example which is val dummyStory = """ On my way to building the dream so bright, I chase the stars through the endless night. With each step I take, with each stride, I'll conquer the world, I won't let dreams hide.

The path may be winding, the journey long, 
But I'll keep marching, staying strong. Through trials and tests, I'll find my way, To the land of dreams where I'll someday stay.

With bricks of ambition, and mortar of hope, 
I'll build my dream, I'll never elope. 
I'll shape it with passion, and carve it with care, 
A vision so grand, beyond compare.

On my way to building the dream in my heart, 
I'll face every challenge, I'll do my part. With unwavering faith and a determined soul, 
I'll reach my destination, I'll achieve my goal.

So, watch me as I rise, like a radiant sunbeam, 
On my way to building the dream.

As I walk this path, lined with stars aglow,
I feel the universe whisper, a comforting flow.
Each star a story, each twinkle a sign,
Guiding me forward, igniting a shine.

Through valleys of doubt and mountains of fear,
I'll press on, knowing my purpose is near.
With courage as my compass, and hope as my guide,
I'll navigate challenges, with grace as my stride.

Every setback a lesson, every obstacle a test,
I'll embrace them all, for they help me manifest.
The dream that's within, the vision so clear,
I'll nurture it daily, banishing every fear.

With patience as my ally, and persistence my friend,
I'll endure the hardships, until the very end.
For dreams are not fleeting, nor illusions in air,
They're the seeds of our future, the essence we bear.

So, as I continue on this journey of mine,
I'll cherish each moment, each mountain I climb.
For the dream that I seek, the dream that I chase,
Is not just a destination, but a journey of grace.

On my way to building the dream so bright,
I'll paint the sky with my colors, in the darkest of night.
For dreams are the fuel, the fire in our soul,
Guiding us forward, making us whole.

""".trimIndent()


Solution

  • You can keep a list of String as your state, adding the remaining text at each step, then looking up the list with the current page index to find out the content for the current page. Something like the following:

    var storyPages by remember { mutableStateOf(listOf(story)) }
    val pageCount by remember { derivedStateOf { storyPages.count() } }
    val pagerState = rememberPagerState(pageCount = { pageCount })
    
    HorizontalPager(state = pagerState) {
        Column(
            modifier = modifier
                .fillMaxSize()
                .padding(bottom = innerPadding.calculateBottomPadding())
        ) {
            val currentPageIndex = it
            if (currentPageIndex == 0) {
                Spacer(modifier = modifier.padding(top = innerPadding.calculateTopPadding()))
                TitleView(title = data.title, modifier = modifier)
                AuthorNameTextView(name = data.author.name, modifier = modifier)
            }
    
            val pageContent = storyPages[currentPageIndex]
    
            StoryContentView(
                story = pageContent,
                textSize = textSize,
                textStyle = selectedTextStyle,
                onTextLayout = { result ->
                    val lastVisibleLineIndex =
                        result.getLineForVerticalPosition(result.layoutInput.constraints.maxHeight.toFloat())
                    val lastVisibleLineStart = result.getLineStart(lastVisibleLineIndex)
                    val lastVisibleLineText = pageContent.substring(lastVisibleLineStart, pageContent.lastIndex)
                    val hasNextPage = storyPages.count() > currentPageIndex + 1
                    if (result.didOverflowHeight && !hasNextPage) {
                        // Add remaining text to be the new story page
                        val newList = storyPages.toMutableList()
                        newList.add(lastVisibleLineText)
                        storyPages = newList
                    }
                }
            )
        }
    }
    

    Summary of what I've changed from yours:

    • Instead of pagerState.currentPage, just use it that is available within HorizontalPager's scope. (i.e. val currentPageIndex = it)
    • Instead of just the first line of overflown text, get all of remaining text to be set as content for the next page
    • When text is laid out, find out what didn't fit and set it to be the contents of next page.
      • Only add the next page if result.didOverflowHeight and there's no next page (see hasNextPage)
      • Otherwise you already reached the final page.