compose中HorizontalPager与BottomNavigation联动遇到的问题

Recomposition的概念以及原则

在讲HorizontalPager与BottomNavigation联动之前先需要区理解下一下Composable 的 Recomposition(官方文档译为"重组"),可以移步看这两篇文章。

Compose 的重组会影响性能吗?聊一聊 recomposition scope

手把手带你走一遍Compose重组流程

总结来说就是:Compose 在编译期分析出会受到某 state 变化影响的代码块,并记录其引用,当此 state 变化时,会根据引用找到这些代码块并标记为 Invalid 。在下一渲染帧到来之前 Compose 会触发 recomposition,并在重组过程中执行 invalid 代码块。

Invalid 代码块即编译器找出的下次重组范围。能够被标记为 Invalid 的代码必须是非 inline 且无返回值的 @Composalbe function/lambda,必须遵循 重组范围最小化 原则。

为何是 非 inline 且无返回值(返回 Unit)?

对于 inline 函数,由于在编译期会在调用处中展开,因此无法在下次重组时找到合适的调用入口,只能共享调用方的重组范围。

而对于有返回值的函数,由于返回值的变化会影响调用方,因此无法单独重组,而必须连同调用方一同参与重组,因此它不能作为入口被标记为 invalid

范围最小化原则

只有会受到 state 变化影响的代码块才会参与到重组,不依赖 state 的代码不参与重组。

HorizontalPager与BottomNavigation联动

还没有玩过compose的可以简单的把HorizontalPager理解成平常用的viewpager,BottomNavigationl理解成底部导航栏。一般一个activity多个fragment模式的都会用到这两个东西。

主要代码如下:compose版本为compose_version = '1.0.2',介绍这个compose的版本是因为在后续我升级到最新的版本,HorizontalPager的实现方式发生了变化,前面能用的,升级后不能用了。

class MainActivity : ComponentActivity() {

    @OptIn(ExperimentalPagerApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        /**
         * 1、setContent方法里 通过创建ComposeView,并当作R.layout.xxx 设置到原setContentView方法中,来初始化内容视图。
         * 2、@Composable 函数最后生成LayoutNode,一些列@Composable 最后构建成了一个以root为根的LayoutNode树
         * 3、Modifier 绑定到 LayoutNode 中,每一个Modifier的扩展方法首先会通过 then 方法生成CombinedModifier的链,
         *   即每一个CombinedModifier 中 都包含上一个扩展方法返回的 Modifier,然后在 LayoutNode 中转换为LayoutNodeWrapper链,
         *   然后测量过程中递归遍历所有LayoutNodeWrapper链中所有LayoutNodeWrapper进行各个属性测量,最后回到LayoutNodeWrapper链中
         *   最后一个节点 InnerPlaceable(LayoutNode成员属性中初始化) 中开始测量当前LayoutNode子node,然后记录测量结果。
         * 4、测量流程 从我们的 AndroidComposeView 的 onMeasure 方法中开始,通过AndroidComposeView中的root来递归测量所有的LayoutNode,
         *   每一个LayoutNode又通过 LayoutNodeWrapper链测量。
         * 5、绘制draw入口 也在 AndroidComposeView 里,在 dispatchDraw 方法里调用了 root.draw(this)
         */
        setContent {
            //observeAsState 该函数的作用就是将ViewModel提供的LiveData数据转换为Compose需要的State数据。
            val viewModel:MainViewModel  = viewModel()
            val selectIndex by viewModel.getSelectedIndex().observeAsState(initial = 0)
            ComposePracticeTheme {
                Column {
                    val pageState = rememberPagerState(
                        pageCount = 5,
                        initialPage = selectIndex,
                        initialOffscreenLimit = 4
                    )
                    HorizontalPager(
                        state = pageState,
                        dragEnabled = false,
                        modifier = Modifier.weight(1f)

                    ) { pager ->
                        when(pager){
                            0 -> NewsPage()
                            1 -> MoviePage()
                            2 -> PicturePage()
                            3 -> MusicPage()
                            4 -> WeatherPage()
                        }

                    }
                    BottomNavigationAlwaysShowLabelComponent(pagerState = pageState)
                }
            }
        }
    }
}

/**
 * Composable的生命周期
 *
 * onActive :当Composable首次进入组件树的时候
 * onCommit :当UI随着recomposition发生更新的时候
 * onDispose : 当Composable从组件树移除的时候
 */

@OptIn(ExperimentalPagerApi::class)
@Composable
fun BottomNavigationAlwaysShowLabelComponent(pagerState: PagerState){
    val viewModel:MainViewModel = viewModel()
    val selectIndex by viewModel.getSelectedIndex().observeAsState(0)

    /**
     * 作用:用来创建并获取某个跨越函数栈存在的数据
     *  rememberCoroutineScope创建CoroutineScope,CoroutineScope不会随函数的执行反复创建
     *  Scope与Composable生命周期一致,随着onDispose而cancel,避免泄露、
     *  Dispatcher通常为AndroidUiDispatcher.Main
     */
    val scope = rememberCoroutineScope()

    BottomNavigation(backgroundColor = Color.White) {
        listItem.forEachIndexed { index, label ->
            BottomNavigationItem(
                icon = {
                    when(index){
                        0 -> BottomIcon(image = Icons.Filled.Home, selectIndex = selectIndex, index = index)
                        1 -> BottomIcon(image = Icons.Filled.List, selectIndex = selectIndex, index = index)
                        2 -> BottomIcon(image = Icons.Filled.Favorite, selectIndex = selectIndex, index = index)
                        3 -> BottomIcon(image = Icons.Filled.ThumbUp, selectIndex = selectIndex, index = index)
                        4 -> BottomIcon(image = Icons.Filled.Place, selectIndex = selectIndex, index = index)
                    }
                },
                label = {
                    Text(
                        text = label,
                        color = if (selectIndex == index) MaterialTheme.colors.primary else Color.Gray
                    )
                },
                selected = selectIndex == index ,
                onClick = {
                    viewModel.saveSelectIndex(index)
                    scope.launch {
                        pagerState.scrollToPage(index)
                    }
                })
        }
    }
}

用上面的代码没有问题,但是升级之后,HorizontalPager的实现方式改变了,所以初始化方式也改变了,在compose_version = '1.1.0-beta02'中,HorizontalPager的使用方式为:

HorizontalPager(
                        state = pageState,
                        modifier = Modifier
                            .weight(1f)
                            /*.scrollable(
                                state = rememberScrollState(),
                                enabled = false,
                                orientation = Orientation.Horizontal
                            )*/,
                        count = listItem.size
                    ) { page ->
                        when(page){
                            0 -> {
                                NewsPage()
                            }
                            1 -> {
                                MoviePage()
                            }
                            2 -> {
                                PicturePage()
                            }
                            3 -> {
                                MusicPage()
                            }
                            4 -> {
                                WeatherPage()
                            }
                        }

                    }

然后运行发现,滑动HorizontalPager的时候发现,BottomNavigation不跟随联动,然后我返回到前面的版本做的方式,原来是有个方式可以禁止HorizontalPager滑动的参数,直接将那个参数置为false,这样HorizontalPager滑不动,那自然也没这个联动的效果,自然也就没问题。

那现在的问题是HorizontalPager没有这个dragEnabled这个参数来控制,那我们也就没办法禁止HorizontalPager滑动,那现在的问题有两种解决办法,1是回退到最开始的版本,但是有个问题,以后一直不升级嘛?那就选择第二个问题,就是硬着头皮试着去改。

于是先去看原来的版本dragEnabled这个参数是怎么控制HorizontalPager滑动的。

HorizontalPager里面调用的是Pager,然后搜索找到最终使用dragEnabled的使用处为

val scrollable = Modifier.scrollable(
        orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
        flingBehavior = flingBehavior,
        reverseDirection = reverseDirection,
        state = state,
        interactionSource = state.internalInteractionSource,
        enabled = dragEnabled,
    )

然后调用Layout方法,

Layout(
        modifier = modifier
            .then(semantics)
            .then(scrollable)
            // Add a NestedScrollConnection which consumes all post fling/scrolls
            .nestedScroll(connection = ConsumeFlingNestedScrollConnection)
            .clipScrollableContainer(isVertical),
        ......    
    )

上面modifier具体的操作的原理可以看
Compose | 一文理解神奇的Modifier 一文。从上面可以看到,其实是将dragEnabled传入到Modifier,那这换不简单,好像说一句,就这?

于是进行操作,修改如下,

HorizontalPager(
                        state = pageState,
                        modifier = Modifier
                            .weight(1f)
                            .scrollable(
                                state = rememberScrollState(),
                                enabled = false,
                                orientation = Orientation.Horizontal
                            ),
                        count = listItem.size
                    ) 

然后一运行,打脸来的太快,和原来的效果一模一样,依旧无法联动。

看来得转换思路啊,我们看到在点击BottomNavigationItem得时候,HorizontalPager是可以跟着动的,但是滑动HorizontalPager,BottomNavigationItem却不跟着改变,可以看到点击事件做了下面的操作。

onClick = {
                    //viewModel.saveSelectIndex(pagerState.currentPage)
                    scope.launch {
                        pagerState.scrollToPage(index)
                    }
                }

是不是可以理解pagerState控制着HorizontalPager,然后联想到本来开头讲的重绘,当pagerState发生改变的时候,HorizontalPager是使用了pageState,然后发生了重绘,但是我们的BottomNavigationItem没有使用pageState,所以也就没有发生改变。有了这个思路,尝试的对原来的代码进行改变如下;

BottomNavigation(backgroundColor = Color.White) {
        listItem.forEachIndexed { index, label ->
            BottomNavigationItem(
                icon = {
                    when(index){
                        0 -> BottomIcon(image = Icons.Filled.Home, selectIndex = pagerState.currentPage, index = index)
                        1 -> BottomIcon(image = Icons.Filled.List, selectIndex = pagerState.currentPage, index = index)
                        2 -> BottomIcon(image = Icons.Filled.Favorite, selectIndex = pagerState.currentPage, index = index)
                        3 -> BottomIcon(image = Icons.Filled.ThumbUp, selectIndex = pagerState.currentPage, index = index)
                        4 -> BottomIcon(image = Icons.Filled.Place, selectIndex = pagerState.currentPage, index = index)
                    }
                },
                label = {
                    Text(
                        text = label,
                        color = if (index == pagerState.currentPage) MaterialTheme.colors.primary else Color.Gray
                    )
                },
                selected = index == pagerState.currentPage ,
                onClick = {
                    //viewModel.saveSelectIndex(pagerState.currentPage)
                    scope.launch {
                        pagerState.scrollToPage(index)
                    }
                })
        }
    }

最终运行实现了我们想要的效果。

参考资料

Compose 的重组会影响性能吗?聊一聊 recomposition scope

手把手带你走一遍Compose重组流程

Compose | 一文理解神奇的Modifier

你可能感兴趣的:(compose中HorizontalPager与BottomNavigation联动遇到的问题)