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步骤差不多,一般分为以下几个步骤:

  1. 获取父view的总宽度
  2. 测量每一个子view所占用的宽度
  3. 根据不同需求摆放子view的位置

在Compose使用Layout来测量和布置子view,如下:

fun ClassicRightLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
    
    }
}

参数解析:

  1. measurables 需要测量的子项列表
  2. 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()
      }
}

以上代码就是本章的全部内容了~

源码地址

源码戳~

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码

)">
< <上一篇
下一篇>>