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

  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()
      

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

源码地址

源码戳~

以上是关于Android compose wanandroid app之分类页面的实现的主要内容,如果未能解决你的问题,请参考以下文章

Android compose crane

Android compose crane

Android笔记--Compose基础

Android Kotlin Jetpack Compose 使用

Android Jetpack Compose学习—— Jetpack compose基础布局

Android Jetpack Compose学习—— Jetpack compose基础布局