Android|Android Compose实现底部按钮以及首页内容详细过程第1/2页
目录
- 前言
- Column、Row、ConstraintLayout布局先知
- Column纵向排列布局
- Row横向排列布局
- ConstraintLayout 约束布局
- Modifier的简单使用
- 底部导航栏的实现
- 首页内容的实现
- Banner的实现
- 首页ViewModel
前言
compose作为Android现在主推的UI框架,各种文章铺天盖地的席卷而来,作为一名Android开发人员也是很有必要的学习一下了,这里就使用wanandroid的开放api来编写一个compose版本的玩安卓客户端,全当是学习了,各位大佬轻喷~
先来看一下首页的效果图:
文章图片
从图片中可以看到首页的内容主要分为三部分,头部标题栏,banner,数据列表,底部导航栏;今天就实现这几个功能。
Column、Row、ConstraintLayout布局先知
在Compose布局中主要常用的就是这三个布局,分别代表纵向排列布局,横向排列布局,以及约束布局;先大概了解一下用法,以及布局包裹内部元素的排列方便在项目中更好的使用。
Column纵向排列布局
Column主要是将布局包裹内的元素由上至下垂直排列显示,类似于Recyclerview的item,简单来看一段代码:
@Preview@Composablefun ColumnItems(){Column {Text(text = "我是第一个Column元素",Modifier.background(Color.Gray))Text(text = "我是第二个Column元素",Modifier.background(Color.Green))Text(text = "我是第三个Column元素",Modifier.background(Color.LightGray))}}
可以看到在一个Column里面包裹了三个Text,那么来看一下效果:
文章图片
可以看到所有元素是由上至下进行排列的。
Row横向排列布局
简而言之就是将布局里面的元素一个一个的由左到右横向排列。
再来看一段简短的代码:
@Preview@Composablefun RowItems(){Row {Text(text = "我是第一个Row元素",Modifier.background(Color.Gray).height(100.dp))Text(text = "我是第二个Row元素",Modifier.background(Color.Green).height(100.dp))Text(text = "我是第三个Row元素",Modifier.background(Color.LightGray).height(100.dp))}}
在Row里面同样包裹了三个Text文本,再来看一下效果:
文章图片
可以看到Row里面的元素是由左到右横向进行排列的。
ConstraintLayout 约束布局
在compose里面同样可以使用约束布局,主要主用于一些Column或者Row或者Box布局无法直接实现的布局,在实现更大的布局以及有许多复杂对齐要求以及布局嵌套过深的场景下,ConstraintLayout 用起来更加顺手,在使用ConstraintLayout 之前需要先导入相关依赖包:
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"
这里额外提一句,在你创建项目的时候所有compose的相关依赖包都要和你项目当前的compose版本一致,或者都更新到最新版,如果compose的版本大于你现在导入的其他依赖库的版本,那么就会报错。
在使用ConstraintLayout需要注意以下几点:
- 声明元素 通过 createRefs() 或 createRef() 方法初始化声明的,并且每个子元素都会关联一个ConstraintLayout 中的 Composable 组件;
- 关联组件 Modifier.constrainAs(text)通过constrainAs关联组件
- 约束关系可以使用 linkTo 或其他约束方法实现;
- parent 是一个默认存在的引用,代表 ConstraintLayout 父布局本身,也是用于子元素的约束关联。
@Preview@Composablefun ConstraintLayoutDemo(){ConstraintLayout {//声明元素val (text,text2,text3) = createRefs()Text(text = "我是第一个元素",Modifier.height(50.dp).constrainAs(text){//将第一个元素固定到父布局的右边end.linkTo(parent.end)})Text(text = "老二",modifier = Modifier.background(Color.Green).constrainAs(text2){//将第二个元素定位到第一个元素的底部top.linkTo(text.bottom)//,然后于第一个元素居中centerTo(text)})Text(text = "老三",modifier = Modifier.constrainAs(text3){//将第三个元素定位到第二个元素的底部top.linkTo(text2.bottom)//将第三个元素定位在第二个元素的右边start.linkTo(text2.end)})}}
看一下效果:
文章图片
约束布局只要习惯linkTo的使用就能很好的使用该布局。
Modifier的简单使用
Modifier在compose里面可以设置元素的宽高,大小,背景色,边框,边距等属性;这里只介绍一些简单的用法。
先看一段代码:
modifier = Modifier//.fillMaxSize()//横向纵向 都铺满,设置了fillMaxSize就不需要设置fillMaxHeight和fillMaxWidth了//.fillMaxHeight()//fillMaxHeight纵向铺满.fillMaxWidth()//fillMaxWidth()横向铺满match.padding(8.dp)//外边距 vertical = 8.dp 上下有8dp的边距; horizontal = 8.dp 水平有8dp的边距.padding(8.dp)//内边距padding(8.dp)=.padding(8.dp,8.dp,8.dp,8.dp)左上右下都有8dp的边距//.width(100.dp)//宽100dp//.height(100.dp)//高100dp.size(100.dp)//宽高 100dp//.widthIn(min: Dp = Dp.Unspecified, max: Dp = Dp.Unspecified)//设置自身的最小和最大宽度(当子级元素超过自身时,子级元素超出部分依旧可见);.background(Color.Green)//背景颜色.border(1.dp, Color.Gray,shape = RoundedCornerShape(20.dp))//边框
- fillMaxSize 设置布局纵向横向都铺满
- fillMaxHeight 设置布局铺满纵向
- fillMaxWidth 设置布局铺满横向,这三个属性再使用了fillMaxSize 就没必要在设置下面两个了
- padding 设置边距,方向由左上右下设置,添加了vertical就是设置垂直的上下边距,horizontal设置了水平的左右边距。这里注意写了两个padding,第一个是外边距,第二个是内边距,外边距最好是放在Modifier的第一个元素。
- width 设置元素的宽
- height 设置元素的高
- size 设置元素大小,只有一个值时宽高都是一个值,.size(100.dp,200.dp)两个值前者是宽,后者是高
- widthIn 设置自身的最小和最大宽度(当子级元素超过自身时,子级元素超出部分依旧可见)
- background 设置元素的背景颜色
- border 设置边框,参数值:边框大小,边框颜色,shape
底部导航栏的实现
从图中可以可以出,底部导航栏主要包含四个tab,分别是首页、项目、分类以及我的,而每个tab又分别包含一张图片和一个文字。
文章图片
具体实现步骤:
1.编写每个tab的样式,这里要使用到Column进行布局,Column列的意思,就是Column里面的元素会一个顺着一个往下排的意思,所以我们需要在里面放一个图片Icon和一个文本Text。
Column(modifier.padding(vertical = 8.dp),//垂直(上下边距)8dphorizontalAlignment = Alignment.CenterHorizontally) {//对齐方式水平居中Icon(painter = painterResource(id = iconId),//图片资源contentDescription = tabName,//描述//图片大小//颜色modifier = Modifier.size(24.dp),tint = tint)//文本字体大小字体颜色Text(text = tabName,fontSize = 11.sp,color = tint)}
因为是四个按钮,并且有着选中和未选中的状态,所以我们需要封装成一个方法进行使用:
/** * 参数解析 * @DrawableRes iconId: Int * * iconId参数名称 * Int参数类型 * @DrawableRes 只能填入符合当前属性的值 * */@Composableprivate fun TabItem(@DrawableRes iconId: Int, //tab 图标资源tabName: String,//tab 名称tint: Color,//tab 颜色(选中或者未选中状态)modifier: Modifier = Modifier){Column(modifier.padding(vertical = 8.dp),horizontalAlignment = Alignment.CenterHorizontally) {Icon(painter = painterResource(id = iconId),contentDescription = tabName,modifier = Modifier.size(24.dp),tint = tint)Text(text = tabName,fontSize = 11.sp,color = tint)}}
2.使用Row放置四个TabItem,Row水平排列的意思。
@Composablefun BottomBar(modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit) {Row(modifier.fillMaxWidth().background(ComposeUIDemoTheme.colors.bottomBar).padding(4.dp, 0.dp).navigationBarsPadding(),content = content)}
@Composablefun BottomTabBar(selectedPosition: Int, currentChanged: (Int) -> Unit){ //使用Row将四个TabItem包裹起来,让它们水平排列BottomBar() {TabItem(iconId = if (selectedPosition == 0) R.drawable.home_selected else R.drawable.home_unselected,tabName = "首页",tint = if (selectedPosition == 0) ComposeUIDemoTheme.colors.iconCurrent else ComposeUIDemoTheme.colors.icon,Modifier.clickable {currentChanged(0)}.weight(1f))TabItem(iconId = if (selectedPosition == 1) R.drawable.project_selected else R.drawable.project_unselected,tabName = "项目",tint = if (selectedPosition == 1) ComposeUIDemoTheme.colors.iconCurrent else ComposeUIDemoTheme.colors.icon,Modifier.clickable {currentChanged(1)}.weight(1f))TabItem(iconId = if (selectedPosition == 2) R.drawable.classic_selected else R.drawable.classic_unselected,tabName = "分类",tint = if (selectedPosition == 2) ComposeUIDemoTheme.colors.iconCurrent else ComposeUIDemoTheme.colors.icon,Modifier.clickable {currentChanged(2)}.weight(1f))TabItem(iconId = if (selectedPosition == 3) R.drawable.mine_selected else R.drawable.mine_unselected,tabName = "我的",tint = if (selectedPosition == 3) ComposeUIDemoTheme.colors.iconCurrent else ComposeUIDemoTheme.colors.icon,Modifier.clickable {currentChanged(3)}.weight(1f))}}
TabItem填充解析:
- iconId tab图标资源,当选中的下标等于当前tab的下标时显示选中的资源,否则显示非选中资源
- tabName tab文本
- tint tab 颜色,同样分为选中和未选中
- Modifier 使用Modifier设置点击事件,以及权重
- currentChanged(0) tabitem的点击事件,返回当前item的下标
TabItem(iconId = if (selectedPosition == 0) R.drawable.home_selected elseR.drawable.home_unselected,tabName = "首页",tint = if (selectedPosition == 0) ComposeUIDemoTheme.colors.iconCurrent else ComposeUIDemoTheme.colors.icon,Modifier.clickable {currentChanged(0)}.weight(1f))
3.分别创建HomePage、ProjectPage、ClassicPage和MinePage四个页面,页面编写一些简单的代码铺满页面即可。
@Composablefun ClassicPage(viewModel: BottomTabBarViewModel = viewModel()){Column(Modifier.fillMaxWidth()) {DemoTopBar(title = "分类")Box(Modifier.background(ComposeUIDemoTheme.colors.background)//使用Modifier将页面铺满.fillMaxSize()) {Text(text = "分类")}}}
4.使用HorizontalPager进行页面滑动,并且与tabitem的点击事件进行绑定,达到页面滑动切换以及点击tabitem进行切换的效果。
HorizontalPager主要参数解析:
- count 总页面数
- state 当前选中的页面状态
implementation "com.google.accompanist:accompanist-pager:$accompanist_pager"//0.20.2
具体实现步骤如下:
先通过remember记录住当前选中的下标,这个主要作用与tabItem的切换
//记录页面状态val indexState = remember { mutableStateOf(0) }
然后通过rememberPagerState记录HorizontalPager的currentPager也就是当前页面下标
val pagerState = rememberPagerState()
使用HorizontalPager填充页面
HorizontalPager(count = 4,state = pagerState,modifier = Modifier.fillMaxSize().weight(1f)){ page: Int ->when(page){0 ->{HomePage()}1 ->{ProjectPage()}2 ->{ClassicPage()}3 ->{MinePage()}}}
使用LaunchedEffect进行页面切换
//页面切换LaunchedEffect(key1 = indexState.value, block = {pagerState.scrollToPage(indexState.value)})
最后绑定底部导航栏并绑定点击事件
//滑动绑定底部菜单栏/**selectedPosition = pagerState.currentPage将当前的currentPager赋值给tabitem的selectPosition对底部导航栏进行绑定indexState.value = https://www.it610.com/article/it将底部导航栏的点击回调下标赋值给indexState对pager进行绑定*/BottomTabBar(selectedPosition = pagerState.currentPage){indexState.value = it}
到这里就能实现一个底部导航栏以及四个页面的切换了。
首页内容的实现
Banner的实现
因为获取Banner数据要进行网络请求,至于网络封装就不贴代码了,这里直接从ViewModel开始展示,具体的网络代码可以移步到项目进行观看。
首页ViewModel
主要用于Banner和首页文章列表的网络请求:
class HomeViewModel : ViewModel() {private var _bannerList = MutableLiveData(listOf())val bannerList:MutableLiveData >= _bannerListfun getBannerList(){NetWork.service.getHomeBanner().enqueue(object : Callback
>>{override fun onResponse(call: Call >>,response: Response >>) {response.body()?.let {_bannerList.value = https://www.it610.com/article/it.data}}override fun onFailure(call: Call >>, t: Throwable) {}})}private var _articleData = MutableLiveData()val articleData:MutableLiveData = _articleDatafun getArticleData(){NetWork.service.getArticleList().enqueue(object : Callback {override fun onResponse(call: Call ,response: Response ) {response.body()?.let {articleData.value = https://www.it610.com/article/it.data}}override fun onFailure(call: Call , t: Throwable) {}})}}
在调用HomePage的时候将HomeViewModel传入进去,不推荐直接在compose里面直接调用,会重复调用:
val bVM = HomeViewModel()HomePage(bVM = bVM)
HomePage的创建:
fun HomePage(viewModel: BottomTabBarViewModel = viewModel(), bVM:HomeViewModel){}
数据调用进行请求,首先要创建变量通过observeAsState进行数据接收刷新
val bannerList by bVM.bannerList.observeAsState()
Compose的网络请求要放到LaunchedEffect去执行,才不会重复请求数据
val requestState = remember { mutableStateOf("") }LaunchedEffect(key1 = requestState.value, block = {bVM.getBannerList()})
绘制Banner的View,这里同样使用到HorizontalPager,并且还使用了coil进行网络加载,需要导入相关依赖包
implementation 'io.coil-kt:coil-compose:1.3.0'
BannerView的代码,实现大致和tabitem差不多,只是添加了一个轮播,就不做过多的极细,直接贴代码了
@ExperimentalCoilApi@ExperimentalPagerApi@Composablefun BannerView(bannerList: List,timeMillis:Long){Box(Modifier.fillMaxWidth().height(160.dp)) {val pagerState = rememberPagerState()var executeChangePage by remember { mutableStateOf(false) }var currentPageIndex = 0HorizontalPager(count = bannerList.size,state = pagerState,modifier = Modifier.pointerInput(pagerState.currentPage) {awaitPointerEventScope {while (true) {val event = awaitPointerEvent(PointerEventPass.Initial)val dragEvent = event.changes.firstOrNull()when {dragEvent!!.positionChangeConsumed() -> {return@awaitPointerEventScope}dragEvent.changedToDownIgnoreConsumed() -> {//记录下当前的页面索引值currentPageIndex = pagerState.currentPage}dragEvent.changedToUpIgnoreConsumed() -> {if (pagerState.targetPage == null) return@awaitPointerEventScopeif (currentPageIndex == pagerState.currentPage && pagerState.pageCount > 1) {executeChangePage = !executeChangePage}}}}}}.clickable {Log.e("bannerTAG","点击的banner item:${pagerState.currentPage}itemUrl:${bannerList[pagerState.currentPage].imagePath}")}.fillMaxSize()) { page ->Image(painter = rememberImagePainter(bannerList1 2 下一页 阅读全文
相关文章
-
文章图片
如何利用Android Studio将moudle变成jar示例详解
这篇文章主要给大家介绍了关于如何利用Android Studio将moudle变成jar的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来跟着小编一起学习学习吧。 2017-08-08 -
文章图片
Android仿打开微信红包动画效果实现代码
这篇文章主要介绍了Android仿打开微信红包动画效果实现代码,非常不错,具有参考借鉴价值,需要的朋友可以参考下 2017-12-12 -
文章图片
Android系统联系人全特效实现(上)分组导航和挤压动画(附源码)
本文将为大家讲解下Android系统联系人全特效实现之分组导航和挤压动画,具体实现及源代码如下,感兴趣的朋友可以参考下哈,希望对大家学习有所帮助 2013-06-06 -
文章图片
Android之沉浸式状态栏的实现方法、状态栏透明
本篇文章主要介绍了Android之沉浸式状态栏的实现方法、状态栏透明,具有一定的参考价值,有兴趣的可以了解一下。 2017-02-02 -
文章图片
Android自定义View实现音频播放圆形进度条
这篇文章主要为大家详细介绍了Android自定义View实现音频播放圆形进度条,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 2018-06-06 -
文章图片
Android变形(Transform)之Camera使用介绍
Camera主要实现3D的变形,有转动,旋转等,Camera的源码是由Native(本地代码)实现,提供的接口也比较简单,感兴趣的朋友可以参考下,或许对你学习有所帮助 2013-02-02 -
文章图片
Android仿百度地图小度语音助手的贝塞尔曲线动画
这篇文章主要为大家详细介绍了Android仿百度地图小度语音助手的贝塞尔曲线动画,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 2018-07-07 -
文章图片
Android Studio实现登录功能案例讲解
这篇文章主要介绍了Android Studio实现登录功能案例讲解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下 2021-08-08 -
文章图片
Android 中Fragment与Activity通讯的详解
这篇文章主要介绍了Android 中Fragment与Activity通讯的详解的相关资料,希望通过本文能帮助到大家,让大家理解掌握如何通信的,需要的朋友可以参考下 2017-10-10 -
文章图片
【Android|Android Compose实现底部按钮以及首页内容详细过程第1/2页】Android Studio 导入新工程项目图解
这篇文章主要介绍了Android Studio 导入新工程项目图解,需要的朋友可以参考下 2017-12-12
推荐阅读
- android第三方框架(五)ButterKnife
- 关于QueryWrapper|关于QueryWrapper,实现MybatisPlus多表关联查询方式
- MybatisPlus使用queryWrapper如何实现复杂查询
- python学习之|python学习之 实现QQ自动发送消息
- Android中的AES加密-下
- 带有Hilt的Android上的依赖注入
- 孩子不是实现父母欲望的工具——林哈夫
- opencv|opencv C++模板匹配的简单实现
- Node.js中readline模块实现终端输入
- java中如何实现重建二叉树