如何使用Jetpack Compose创建返回页首功能

如何使用Android Jetpack Compose创建返回页首功能

通常在我们的应用中,我们有大型列表供用户滚动以查看一些内容,有时这些列表是固定的,例如一个包含100个元素的列表。

我已经开始开发一个简单的应用程序,该应用程序获取加密货币的列表并在主屏幕上向用户显示。刚开始时,应用程序仅获取100个元素并呈现给用户,当我使用该应用程序时,我注意到添加一个滚动到顶部的按钮将会非常有帮助,因为我有一个搜索栏,让用户搜索加载的任何加密货币。

应用程序演示

这是我最初的版本,没有滚动到顶部的按钮。

如果您查看此示例,您将看到用户需要手动向上滚动以找到搜索栏并搜索内容,我知道,您可能会想,将一个固定的搜索栏添加到顶部,这样您就始终可以进行搜索,这是正确的,您也可以这样做,但是您会发现,我已经为标题栏添加了一个固定的标头,并且在这种情况下,我仅仅使用了 100 条获取到的结果,您可能有一个分页列表,加载超过 100 个元素,而某个时刻您需要再次滚动到顶部。

为此,我们想要向用户展示的是,在这里,我还隐藏了底部导航,但让我们只看上去最高的按钮。

所以,在查看示例后,让我们来看一下代码

应用程序代码

首先,我们需要一个列表,我将编写一个简单的可组合项,它接受一个硬币列表,并将数据呈现给用户。我只关注回到顶部按钮,因此没有所有屏幕的代码,只有我们正在寻找的功能的代码。

@Composable
fun HomeScreen(
    modifier: Modifier,
    coinList: List<CoinData>
) {
    
    

    LazyColumn(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
    
    

        itemsIndexed(coinList) {
    
     index, item ->
            CoinRow(
                modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
                index = index + 1,
                coinData = item
            )
        }
    }
}

如果你看到上面的代码,它唯一的作用就是加载一个类型为 CoinData 的列表,我使用 itemsIndexed 是因为我需要索引来显示每个硬币旁边的数字,但你也可以使用更简单的 items(coinList)。

目前,我们正在将100个元素加载到 LazyColumn 中,现在我们需要一种方法来知道该列表的状态,因为我们需要知道用户是否已经从列表的第一个索引滚动了一些位置。当我们知道用户已经滚动到索引0之外时,我们将显示我们的"滚动到顶部"按钮。

但首先,让我们先创建"滚动到顶部"的可组合部分。

@Composable
fun GoToTop(goToTop: () -> Unit) {
    
    
    Box(modifier = Modifier.fillMaxSize()) {
    
    
        FloatingActionButton(
            modifier = Modifier
                .padding(16.dp)
                .size(50.dp)
                .align(Alignment.BottomEnd),
            onClick = goToTop,
            backgroundColor = White, contentColor = Black
        ) {
    
    
            Icon(
                painter = painterResource(id = R.drawable.ic_baseline_arrow_upward_24),
                contentDescription = "go to top"
            )
        }
    }
}

对于这个可组合部件,我们只创建一个浮动操作按钮(FloatingActionButton),并将操作传播到GoToTop,使其成为调用方组合部件的一部分。这被称为状态提升(state hoisting),我们将此可组合部件的状态托管到其他地方,而不是在同一可组合部件中,这使我们无需传递任何列表或其他内容给该可组合部件。

现在,让我们实现回到顶部功能的逻辑,正如之前所说,我们需要知道用户滚动列表的索引。为此,LazyColumn 已在其方法中提供了一个状态参数,我们可以在实现中看到。

@Composable
fun LazyColumn(
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(), // <----- Here 
    contentPadding: PaddingValues = PaddingValues(0.dp),
    reverseLayout: Boolean = false,
    verticalArrangement: Arrangement.Vertical =
        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
    userScrollEnabled: Boolean = true,
    content: LazyListScope.() -> Unit
) {
    
     ... }

所以,让我们将该状态对象添加到我们的LazyColumn可组合项中。

@Composable
fun HomeScreen(
    modifier: Modifier,
    coinList: List<CoinData>
) {
    
    
    val listState = rememberLazyListState()
    //Here we create a condition if the firstVisibleItemIndex is greater than 0
    val showButton by remember {
    
    
        derivedStateOf {
    
    
            listState.firstVisibleItemIndex > 0
        }
    }

    LazyColumn(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        state = listState
    ) {
    
    

        itemsIndexed(coinList) {
    
     index, item ->
            CoinRow(
                modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
                index = index + 1,
                coinData = item
            )
        }
    }

    AnimatedVisibility(visible = showButton, enter = fadeIn(), exit = fadeOut()) {
    
    
        GoToTop {
    
     }
    }
}

现在,我们可以访问listState并查看用户在滚动列表时移动的索引,记住,每当在LazyColumn上发生事件时,rememberLazyListState都会触发listState,而listState将被触发。

现在,有两种方法可以做到这一点,首先是按照以下步骤操作:

@Composable
fun HomeScreen(
    modifier: Modifier,
    coinList: List<CoinData>
) {
    
    
    val listState = rememberLazyListState()
    //Here we create a condition if the firstVisibleItemIndex is greater than 0
    val showButton by remember {
    
    
        derivedStateOf {
    
    
            listState.firstVisibleItemIndex > 0
        }
    }

    LazyColumn(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        state = listState
    ) {
    
    

        itemsIndexed(coinList) {
    
     index, item ->
            CoinRow(
                modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
                index = index + 1,
                coinData = item
            )
        }
    }

    AnimatedVisibility(visible = showButton, enter = fadeIn(), exit = fadeOut()) {
    
    
        GoToTop {
    
     }
    }
}

在这里,我们创建了一个使用derivedStateOf的新条件,derivedStateOf的作用是确保我们不会在重新组合时多次触发我们的可组合项。请记住,当我们使用remember监听更改时,每当在remember下发生更改时,它都将触发一次重新组合。现在想象一下,我们有100个项目,每次滚动一页,我们都会重新组合屏幕。

我们将会有100次重新组合仅仅是为了向下滚动!!现在,如果我们稍微回退一点,也会触发另一次重新组合。

我们想要做的只是触发一次按钮,而derivedStateOf提供了这种唯一的比较方式,即如果内容相同就不重新组合,只检查条件。为了更好地理解这一点,我建议阅读Ben Trengrove的精彩文章,并观看Android Developers YouTube频道的视频。

好的,让我们继续。

现在,我们需要做的只是在满足showButton条件时显示"回到顶部"按钮,为此,当发生此条件时,我们只会添加GoToTop按钮,并使用AnimatedVisibility为其提供漂亮的淡入淡出效果。

@Composable
fun HomeScreen(
    modifier: Modifier,
    coinList: List<CoinData>
) {
    
    
    val listState = rememberLazyListState()
    //Here we create a condition if the firstVisibleItemIndex is greater than 0
    val showButton by remember {
    
    
        derivedStateOf {
    
    
            listState.firstVisibleItemIndex > 0
        }
    }

    LazyColumn(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        state = listState
    ) {
    
    

        itemsIndexed(coinList) {
    
     index, item ->
            CoinRow(
                modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
                index = index + 1,
                coinData = item
            )
        }
    }

    AnimatedVisibility(visible = showButton, enter = fadeIn(), exit = fadeOut()) {
    
    
        GoToTop {
    
     }
    }
}

现在再看下效果

你是否注意到这个实现和我之前在帖子中展示的第一个 GIF 之间的区别?如果没有,请往上看看它的行为如何。

如果你注意到了,按钮会一直停留在索引 0,这是因为只有当用户回到顶部时,它才会消失!

在大多数情况下,这不是一个问题,因为我们一直在向用户展示“返回顶部”按钮,但我们可以做得更好,可以给它带来更好的用户体验。

对于下面的内容,我们希望在用户向上滚动一点时移除按钮,因为它朝着搜索的方向前进。

就像这样

所以,为了做到这一点,我们需要一种方法来知道用户何时向下滚动,为此,让我们创建一个帮助组合函数,它将是ListState的扩展函数。

@Composable
fun LazyListState.isScrollingUp(): Boolean {
    
    
    var previousIndex by remember(this) {
    
     mutableStateOf(firstVisibleItemIndex) }
    var previousScrollOffset by remember(this) {
    
     mutableStateOf(firstVisibleItemScrollOffset) }
    return remember(this) {
    
    
        derivedStateOf {
    
    
            if (previousIndex != firstVisibleItemIndex) {
    
    
                previousIndex > firstVisibleItemIndex
            } else {
    
    
                previousScrollOffset >= firstVisibleItemScrollOffset
            }.also {
    
    
                previousIndex = firstVisibleItemIndex
                previousScrollOffset = firstVisibleItemScrollOffset
            }
        }
    }.value
}

我知道,它说的是向上滚动,对吧?没问题,如果我们否定它,它就是用于向下滚动的

所以,这段代码还剩下什么?只需要用这个替换showButton

@Composable
fun HomeScreen(
    modifier: Modifier,
    coinList: List<CoinData>
) {
    
    
    val listState = rememberLazyListState()

    LazyColumn(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        state = listState
    ) {
    
    

        itemsIndexed(coinList) {
    
     index, item ->
            CoinRow(
                modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
                index = index + 1,
                coinData = item
            )
        }
    }

    AnimatedVisibility(visible = !listState.isScrollingUp(), enter = fadeIn(), exit = fadeOut()) {
    
    
        GoToTop {
    
     }
    }
}

现在,有了这个,我们将得到我们想要的行为,好的,现在让我们来到最重要的部分,当我们点击按钮时向上滚动!

为此,我们需要一个范围来执行列表的滚动,因为不能在列表更新状态时使用相同的线程进行操作,所以为此,我们将创建一个新的协程范围,并且仅在列表状态中执行scrollToItem方法,该方法将滚动我们的列表到所需的索引,本例中为0(列表的第一个),还有一个animatedScrollToItem方法,它会播放一个漂亮的动画,使列表滚动到顶部,而不是一次性地弹到顶部,但在我的情况下,这有时是高效的,有时不是,但可能是因为我的设备,有时会有点卡顿。

@Composable
fun HomeScreen(
    modifier: Modifier,
    coinList: List<CoinData>
) {
    
    
    val listState = rememberLazyListState()
    val scope = rememberCoroutineScope()

    LazyColumn(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        state = listState
    ) {
    
    

        itemsIndexed(coinList) {
    
     index, item ->
            CoinRow(
                modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
                index = index + 1,
                coinData = item
            )
        }
    }

    AnimatedVisibility(visible = !listState.isScrollingUp(), enter = fadeIn(), exit = fadeOut()) {
    
    
        GoToTop {
    
    
            scope.launch {
    
    
                listState.scrollToItem(0)
            }
        }
    }
}

下面是最终效果

完整代码如下:

@Composable
fun HomeScreen(
    modifier: Modifier,
    coinList: List<CoinData>
) {
    
    
    val listState = rememberLazyListState()
    val scope = rememberCoroutineScope()

    LazyColumn(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        state = listState
    ) {
    
    

        itemsIndexed(coinList) {
    
     index, item ->
            CoinRow(
                modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
                index = index + 1,
                coinData = item
            )
        }
    }

    AnimatedVisibility(visible = !listState.isScrollingUp(), enter = fadeIn(), exit = fadeOut()) {
    
    
        GoToTop {
    
    
            scope.launch {
    
    
                listState.scrollToItem(0)
            }
        }
    }
}

@Composable
fun LazyListState.isScrollingUp(): Boolean {
    
    
    var previousIndex by remember(this) {
    
     mutableStateOf(firstVisibleItemIndex) }
    var previousScrollOffset by remember(this) {
    
     mutableStateOf(firstVisibleItemScrollOffset) }
    return remember(this) {
    
    
        derivedStateOf {
    
    
            if (previousIndex != firstVisibleItemIndex) {
    
    
                previousIndex > firstVisibleItemIndex
            } else {
    
    
                previousScrollOffset >= firstVisibleItemScrollOffset
            }.also {
    
    
                previousIndex = firstVisibleItemIndex
                previousScrollOffset = firstVisibleItemScrollOffset
            }
        }
    }.value
}

希望本文对你实现列表回到顶部有所帮助!!!

猜你喜欢

转载自blog.csdn.net/u011897062/article/details/131452708