Android compose wanandroid app之分类页面的实现
Posted theyangchoi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android compose wanandroid app之分类页面的实现相关的知识,希望对你有一定的参考价值。
实现分类页面
前言
之前实现了底部导航栏以及滑动切换,这里根据官方推荐的底部导航栏的使用方式重新实现了底部导航栏,并实现分类页面,通过API获取导航数据,实现左边菜单栏,右边内容显示的效果,效果图如下:
Scaffold简单使用
使用Scaffold可以实现Compose的基槽位布局,比如topBar顶部菜单栏,bottomBar底部导航栏,floatingActionButtonPosition悬浮按钮等等;这里就不做过多的介绍了,详情可以查阅Scaffold的属性进行设置,这里主要看bottomBar的实现。
先看一下bottomBar在Scaffold的表现形式:
bottomBar: @Composable () -> Unit = ,
从参数类型可以看出来,我们需要在里面放置一个被@Composable标记的函数,那么就先创建一个函数,并使用@Composable注解:
@Composable
fun BottomTab()
//实现逻辑
然后使用Scaffold,参数实现一个bottomBar就可以了:
Scaffold(
bottomBar =
BottomTab()
)
//逻辑实现
接下来就是使用BottomNavigation和NavHost实现底部导航的操作了。
BottomNavigation和NavHost实现底部导航
官方推荐使用BottomNavigation实现导航栏,先来看一下BottomNavigation的属性,根据自己的需求设置即可:
@Composable
fun BottomNavigation(
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = BottomNavigationDefaults.Elevation,
content: @Composable RowScope.() -> Unit
)
在正式使用之前我们还需要设置一些变量,比如底部菜单的文字,选中和未选中的图片资源:
private val tabs = arrayOf("首页","项目","分类","我的")
private val defImg = arrayOf(R.drawable.home_unselected,R.drawable.project_unselected,R.drawable.classic_unselected,R.drawable.mine_unselected)
private val selectImg = arrayOf(R.drawable.home_selected,R.drawable.project_selected,R.drawable.classic_selected,R.drawable.mine_selected)
然后将BottomTab的方法补齐,如下:
@Composable
fun BottomTab(navController:NavController,viewModel: BottomTabBarViewModel,labels:Array<String>,selectImages:Array<Int>,defImages:Array<Int>)
BottomNavigation(backgroundColor = Color.White, elevation = 6.dp,modifier = Modifier.navigationBarsPadding()//要设置这个属性,不然你会发现你的底部导航栏不见了
)
for (i in labels.indices)
BottomNavigationItem(selected = viewModel.bottomBarIndex == i, onClick =
viewModel.bottomBarIndex = i
navController.navigate(labels[i])
, icon =
Image(
painter = painterResource(id = if (viewModel.bottomBarIndex == i) selectImages[i] else defImages[i]),
contentDescription = labels[i],
modifier = Modifier.size(25.dp)
)
, label =
Text(text = labels[i], color = if (viewModel.bottomBarIndex == i) Color(114,160,240) else Color.Gray)
)
参数分析:
1.navController 导航控制器,主要用于设置NavHost的路由,代码里面表现为navController.navigate(labels[i])
2.labels 文字资源集合
3.selectImages 选中图片集合
4.defImages 未选中图片集合
5.modifier = Modifier.navigationBarsPadding 应用与内容底部边缘的导航栏高度相匹配的附加空间,以及与相应开始边缘和结束边缘上的导航栏宽度相匹配的附加空间。简单来说就是不设置该属性你的导航栏会被挤出屏幕外。
BottomNavigationItem
底部导航栏的item,和Recyclerview的item差不多,这里通过for循环去添加,一共四个item
for (i in labels.indices)
BottomNavigationIte()
参数解析:
1.selected: Boolean, 是否选中,代码表现为:selected = viewModel.bottomBarIndex == i
2.onClick: () -> Unit 点击事件,点击后要保存选中的下标,并且通知NavHost切换路由,代码表现为:
onClick =
viewModel.bottomBarIndex = i//保存选中下标
navController.navigate(labels[i])//切换路由
3.icon: @Composable () -> Unit 图片资源
4.label: @Composable (() -> Unit)? = null, 文字资源
NavHost切换路由
使用NavHost切换路由,先来看一下NavHost的属性:
@Composable
public fun NavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
route: String? = null,
builder: NavGraphBuilder.() -> Unit
)
1.navController 导航控制器
2.startDestination 设置目的地
3.builder: NavGraphBuilder.() -> Unit 实现逻辑
看代码实现:
val navController = rememberNavController()
Scaffold(
bottomBar =
BottomTab(navController = navController,viewModel = viewModel, labels = tabs, selectImages = selectImg, defImages = defImg)
)
NavHost(navController = navController, startDestination = tabs[viewModel.bottomBarIndex])
//startDestination 的值等于 tabs[0]的值切换到HomePage
composable(tabs[0])
HomePage(bVM = bVM)
//startDestination 的值等于 tabs[1]的值切换到ProjectPage
composable(tabs[1])
ProjectPage()
composable(tabs[2])
ClassicPage(cVM = cVM)
composable(tabs[3])
MinePage()
通过以上代码就可以实现官方推荐的导航使用方法了。
分类页面的实现
前面说的都是导航栏的使用,属于前面的内容了;进入今天的主题,分类页面的实现;从效果图可以看出分类页面主要分为两部分,左边的菜单栏和右边的内容显示栏,点击左边的菜单,右边显示对应的内容。
获取数据
在实现功能之前肯定要先获取数据,那么创建ClassicViewModel进行数据获取:
class ClassicViewModel : ViewModel()
private var _naviList = MutableLiveData(listOf<DataEntity>())
val naviList:MutableLiveData<List<DataEntity>> = _naviList
fun getNaviList()
NetWork.service.getNaviJson().enqueue(object : Callback<NaviEntity>
override fun onResponse(call: Call<NaviEntity>, response: Response<NaviEntity>)
response.body()?.let
_naviList.value = it.data
override fun onFailure(call: Call<NaviEntity>, t: Throwable)
)
val selectIndex: MutableLiveData<Int> = MutableLiveData(0)
init
getNaviList()
在ClassicPage页面获取到数据,并且获取选中的下标:
val naviList by cVM.naviList.observeAsState()
val selectIndex by cVM.selectIndex.observeAsState(0)
左边布局的实现
因为左边布局比较简单,就一个列表然后设置选中和未选中的样式,这里就不做过多的赘述了,直接贴代码:
@Composable
private fun ClassicLeftList(naviList: List<DataEntity>,selectIndex: Int,clickCallBack:((Int)->Unit))
LazyColumn
itemsIndexed(naviList) index: Int, item: DataEntity ->
Box(modifier = Modifier
.width(120.dp)
.background(if (index == selectIndex) Color(150,180,233) else ComposeUIDemoTheme.colors.listItem)
.height(48.dp)
.clickable
clickCallBack.invoke(index)
)
ClassicLeftItem(title = naviList[index].name,index = index, selectIndex = selectIndex)
@Composable
private fun ClassicLeftItem(title:String,index:Int,selectIndex:Int)
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.height(48.dp))
Text(text = title,
modifier = Modifier.fillMaxWidth(),
fontSize = 12.sp,
color = if (index == selectIndex) Color(248,249,249) else ComposeUIDemoTheme.colors.icon,
textAlign = TextAlign.Center)
右边布局的实现
从图中可以看到,右边不是列表,而是一个流式布局,但是要内容总有超出屏幕显示区域的时候,所以这里先设置一下右边布局的基本属性:
@Composable
private fun ClassicRightList(dataList:List<Article>)
//verticalScroll(rememberScrollState()设置内容可以上下滑动
Column(modifier = Modifier
.padding(16.dp,0.dp,0.dp,0.dp)
.fillMaxSize()
.background(color = Color.White)
.verticalScroll(rememberScrollState()))
ClassicRightLayout
for (index in dataList.indices)
Child(text = dataList[index].title)
@Composable
private fun Child(modifier: Modifier = Modifier, text: String)
Card(
modifier = modifier,
border = BorderStroke(color = Color.Black, width = Dp.Hairline),
shape = RoundedCornerShape(8.dp)
)
Row(
modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically
)
Spacer(Modifier.width(4.dp))
Text(text = text)
填充ClassicPage内容
通过Row来填充页面内容
@Composable
fun ClassicPage(cVM:ClassicViewModel)
val naviList by cVM.naviList.observeAsState()
val selectIndex by cVM.selectIndex.observeAsState(0)
Column(Modifier.fillMaxWidth())
DemoTopBar(title = "分类")
Row(modifier = Modifier.fillMaxSize())
if (naviList != null && naviList?.size !== 0)
ClassicLeftList(naviList = naviList!!,selectIndex)
cVM.selectIndex.value = it
Box(modifier = Modifier
.fillMaxHeight()
.width(10.dp)
.background(color = Color(234,233,234)))
ClassicRightList(dataList = naviList!![selectIndex].articles)
Compose自定义布局实现流式布局
Compose的自定义view和android传统的自定义view步骤差不多,一般分为以下几个步骤:
- 获取父view的总宽度
- 测量每一个子view所占用的宽度
- 根据不同需求摆放子view的位置
在Compose使用Layout来测量和布置子view,如下:
fun ClassicRightLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
)
Layout(
modifier = modifier,
content = content
) measurables, constraints ->
参数解析:
- measurables 需要测量的子项列表
- constraints 父布局的约束条件
遍历所有子项,测量宽高
//获取父控件最大宽度
val parentWidth = constraints.maxWidth
//当前行宽(超出屏幕要换行)
var lineWidth = 0
//当前行高
var lineHeight = 0
//总高度(每换行一次记录一次)
var totalHeight = 0
//所有可放置的内容
val placeableList = mutableListOf<MutableList<Placeable>>()
//每行的最高高度
val mLineHeight = mutableListOf<Int>()
//每行放置的内容
var lineViews = mutableListOf<Placeable>()
/**
* 需要测量的子项 测量子View,获取FlowLayout的宽高
* 遍历子项测量宽高
* */
measurables.mapIndexed i, measurable ->
// 测量子view
val placeable = measurable.measure(constraints)
// 设置子view宽高
val childWidth = placeable.width
val childHeight = placeable.height
//如果当前行宽度超出父Layout则换行
if (lineWidth + childWidth > parentWidth)
mLineHeight.add(lineHeight)//添加行高
placeableList.add(lineViews)//将当前子布局放到所有的内容集合里面去
//将当前行的子view清空,然后换行添加新的view
lineViews = mutableListOf()
lineViews.add(placeable)
//记录总高度
totalHeight += lineHeight
//重置行高与行宽
lineWidth = childWidth
lineHeight = childHeight
totalHeight += 10.dp.toPx().toInt()
else
//记录每行宽度
lineWidth += childWidth + if (i == 0) 0 else 10.dp.toPx().toInt()
//记录每行最大高度
lineHeight = maxOf(lineHeight, childHeight)
//将当前子view添加到当前行内容里面去
lineViews.add(placeable)
定位子项
layout(parentWidth, totalHeight)
//从左上角开始定位 top 0 left 0
var topOffset = 0
var leftOffset = 0
//循环定位
for (i in placeableList.indices)
lineViews = placeableList[i]
lineHeight = mLineHeight[i]
for (j in lineViews.indices)
val child = lineViews[j]
val childWidth = child.width
val childHeight = child.height
// 根据Gravity获取子项y坐标
val childTop = topOffset + (lineHeight - childHeight) / 2
child.placeRelative(leftOffset, childTop)
// 更新子项x坐标
leftOffset += childWidth + 10.dp.toPx().toInt()
//重置子项x坐标
leftOffset = 0
//子项y坐标更新
topOffset += lineHeight + 10.dp.toPx().toInt()
以上代码就是本章的全部内容了~
源码地址
以上是关于Android compose wanandroid app之分类页面的实现的主要内容,如果未能解决你的问题,请参考以下文章
Android Kotlin Jetpack Compose 使用