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