Android Compose實(shí)現(xiàn)底部按鈕以及首頁內(nèi)容詳細(xì)過程第1/2頁
前言
compose作為Android現(xiàn)在主推的UI框架,各種文章鋪天蓋地的席卷而來,作為一名Android開發(fā)人員也是很有必要的學(xué)習(xí)一下了,這里就使用wanandroid的開放api來編寫一個(gè)compose版本的玩安卓客戶端,全當(dāng)是學(xué)習(xí)了,各位大佬輕噴~
先來看一下首頁的效果圖:
從圖片中可以看到首頁的內(nèi)容主要分為三部分,頭部標(biāo)題欄,banner,數(shù)據(jù)列表,底部導(dǎo)航欄;今天就實(shí)現(xiàn)這幾個(gè)功能。
Column、Row、ConstraintLayout布局先知
在Compose布局中主要常用的就是這三個(gè)布局,分別代表縱向排列布局,橫向排列布局,以及約束布局;先大概了解一下用法,以及布局包裹內(nèi)部元素的排列方便在項(xiàng)目中更好的使用。
Column縱向排列布局
Column主要是將布局包裹內(nèi)的元素由上至下垂直排列顯示,類似于Recyclerview的item,簡(jiǎn)單來看一段代碼:
@Preview @Composable fun ColumnItems(){ Column { Text(text = "我是第一個(gè)Column元素",Modifier.background(Color.Gray)) Text(text = "我是第二個(gè)Column元素",Modifier.background(Color.Green)) Text(text = "我是第三個(gè)Column元素",Modifier.background(Color.LightGray)) } }
可以看到在一個(gè)Column里面包裹了三個(gè)Text,那么來看一下效果:
可以看到所有元素是由上至下進(jìn)行排列的。
Row橫向排列布局
簡(jiǎn)而言之就是將布局里面的元素一個(gè)一個(gè)的由左到右橫向排列。
再來看一段簡(jiǎn)短的代碼:
@Preview @Composable fun RowItems(){ Row { Text(text = "我是第一個(gè)Row元素",Modifier.background(Color.Gray).height(100.dp)) Text(text = "我是第二個(gè)Row元素",Modifier.background(Color.Green).height(100.dp)) Text(text = "我是第三個(gè)Row元素",Modifier.background(Color.LightGray).height(100.dp)) } }
在Row里面同樣包裹了三個(gè)Text文本,再來看一下效果:
可以看到Row里面的元素是由左到右橫向進(jìn)行排列的。
ConstraintLayout 約束布局
在compose里面同樣可以使用約束布局,主要主用于一些Column或者Row或者Box布局無法直接實(shí)現(xiàn)的布局,在實(shí)現(xiàn)更大的布局以及有許多復(fù)雜對(duì)齊要求以及布局嵌套過深的場(chǎng)景下,ConstraintLayout 用起來更加順手,在使用ConstraintLayout 之前需要先導(dǎo)入相關(guān)依賴包:
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"
這里額外提一句,在你創(chuàng)建項(xiàng)目的時(shí)候所有compose的相關(guān)依賴包都要和你項(xiàng)目當(dāng)前的compose版本一致,或者都更新到最新版,如果compose的版本大于你現(xiàn)在導(dǎo)入的其他依賴庫的版本,那么就會(huì)報(bào)錯(cuò)。
在使用ConstraintLayout需要注意以下幾點(diǎn):
- 聲明元素 通過 createRefs() 或 createRef() 方法初始化聲明的,并且每個(gè)子元素都會(huì)關(guān)聯(lián)一個(gè)ConstraintLayout 中的 Composable 組件;
- 關(guān)聯(lián)組件 Modifier.constrainAs(text)通過constrainAs關(guān)聯(lián)組件
- 約束關(guān)系可以使用 linkTo 或其他約束方法實(shí)現(xiàn);
- parent 是一個(gè)默認(rèn)存在的引用,代表 ConstraintLayout 父布局本身,也是用于子元素的約束關(guān)聯(lián)。
來看一段代碼:
@Preview @Composable fun ConstraintLayoutDemo(){ ConstraintLayout { //聲明元素 val (text,text2,text3) = createRefs() Text(text = "我是第一個(gè)元素",Modifier.height(50.dp).constrainAs(text){ //將第一個(gè)元素固定到父布局的右邊 end.linkTo(parent.end) }) Text(text = "老二",modifier = Modifier.background(Color.Green).constrainAs(text2){ //將第二個(gè)元素定位到第一個(gè)元素的底部 top.linkTo(text.bottom) //,然后于第一個(gè)元素居中 centerTo(text) }) Text(text = "老三",modifier = Modifier.constrainAs(text3){ //將第三個(gè)元素定位到第二個(gè)元素的底部 top.linkTo(text2.bottom) //將第三個(gè)元素定位在第二個(gè)元素的右邊 start.linkTo(text2.end) }) } }
看一下效果:
約束布局只要習(xí)慣linkTo的使用就能很好的使用該布局。
Modifier的簡(jiǎn)單使用
Modifier在compose里面可以設(shè)置元素的寬高,大小,背景色,邊框,邊距等屬性;這里只介紹一些簡(jiǎn)單的用法。
先看一段代碼:
modifier = Modifier // .fillMaxSize()//橫向 縱向 都鋪滿,設(shè)置了fillMaxSize就不需要設(shè)置fillMaxHeight和fillMaxWidth了 // .fillMaxHeight()//fillMaxHeight縱向鋪滿 .fillMaxWidth()//fillMaxWidth()橫向鋪滿 match .padding(8.dp)//外邊距 vertical = 8.dp 上下有8dp的邊距; horizontal = 8.dp 水平有8dp的邊距 .padding(8.dp)//內(nèi)邊距 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)//設(shè)置自身的最小和最大寬度(當(dāng)子級(jí)元素超過自身時(shí),子級(jí)元素超出部分依舊可見); .background(Color.Green)//背景顏色 .border(1.dp, Color.Gray,shape = RoundedCornerShape(20.dp))//邊框
- fillMaxSize 設(shè)置布局縱向橫向都鋪滿
- fillMaxHeight 設(shè)置布局鋪滿縱向
- fillMaxWidth 設(shè)置布局鋪滿橫向,這三個(gè)屬性再使用了fillMaxSize 就沒必要在設(shè)置下面兩個(gè)了
- padding 設(shè)置邊距,方向由左上右下設(shè)置,添加了vertical就是設(shè)置垂直的上下邊距,horizontal設(shè)置了水平的左右邊距。這里注意寫了兩個(gè)padding,第一個(gè)是外邊距,第二個(gè)是內(nèi)邊距,外邊距最好是放在Modifier的第一個(gè)元素。
- width 設(shè)置元素的寬
- height 設(shè)置元素的高
- size 設(shè)置元素大小,只有一個(gè)值時(shí)寬高都是一個(gè)值,.size(100.dp,200.dp)兩個(gè)值前者是寬,后者是高
- widthIn 設(shè)置自身的最小和最大寬度(當(dāng)子級(jí)元素超過自身時(shí),子級(jí)元素超出部分依舊可見)
- background 設(shè)置元素的背景顏色
- border 設(shè)置邊框,參數(shù)值:邊框大小,邊框顏色,shape
更多Modifier的設(shè)置可以查看源碼或者官方文檔。
底部導(dǎo)航欄的實(shí)現(xiàn)
從圖中可以可以出,底部導(dǎo)航欄主要包含四個(gè)tab,分別是首頁、項(xiàng)目、分類以及我的,而每個(gè)tab又分別包含一張圖片和一個(gè)文字。
具體實(shí)現(xiàn)步驟:
1.編寫每個(gè)tab的樣式,這里要使用到Column進(jìn)行布局,Column列的意思,就是Column里面的元素會(huì)一個(gè)順著一個(gè)往下排的意思,所以我們需要在里面放一個(gè)圖片Icon和一個(gè)文本Text。
Column( modifier.padding(vertical = 8.dp),//垂直(上下邊距)8dp horizontalAlignment = Alignment.CenterHorizontally) {//對(duì)齊方式水平居中 Icon(painter = painterResource(id = iconId),//圖片資源 contentDescription = tabName,//描述 //圖片大小 //顏色 modifier = Modifier.size(24.dp),tint = tint) // 文本 字體大小 字體顏色 Text(text = tabName,fontSize = 11.sp,color = tint) }
因?yàn)槭撬膫€(gè)按鈕,并且有著選中和未選中的狀態(tài),所以我們需要封裝成一個(gè)方法進(jìn)行使用:
/** * 參數(shù)解析 * @DrawableRes iconId: Int * * iconId 參數(shù)名稱 * Int 參數(shù)類型 * @DrawableRes 只能填入符合當(dāng)前屬性的值 * */ @Composable private fun TabItem(@DrawableRes iconId: Int, //tab 圖標(biāo)資源 tabName: String,//tab 名稱 tint: Color,//tab 顏色(選中或者未選中狀態(tài)) 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放置四個(gè)TabItem,Row水平排列的意思。
@Composable fun BottomBar(modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit) { Row( modifier .fillMaxWidth() .background(ComposeUIDemoTheme.colors.bottomBar) .padding(4.dp, 0.dp) .navigationBarsPadding(), content = content ) }
@Composable fun BottomTabBar(selectedPosition: Int, currentChanged: (Int) -> Unit){ //使用Row將四個(gè)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 = "項(xiàng)目", 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圖標(biāo)資源,當(dāng)選中的下標(biāo)等于當(dāng)前tab的下標(biāo)時(shí)顯示選中的資源,否則顯示非選中資源
- tabName tab文本
- tint tab 顏色,同樣分為選中和未選中
- Modifier 使用Modifier設(shè)置點(diǎn)擊事件,以及權(quán)重
- currentChanged(0) tabitem的點(diǎn)擊事件,返回當(dāng)前item的下標(biāo)
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.分別創(chuàng)建HomePage、ProjectPage、ClassicPage和MinePage四個(gè)頁面,頁面編寫一些簡(jiǎn)單的代碼鋪滿頁面即可。
@Composable fun ClassicPage(viewModel: BottomTabBarViewModel = viewModel()){ Column(Modifier.fillMaxWidth()) { DemoTopBar(title = "分類") Box( Modifier .background(ComposeUIDemoTheme.colors.background) //使用Modifier將頁面鋪滿 .fillMaxSize() ) { Text(text = "分類") } } }
4.使用HorizontalPager進(jìn)行頁面滑動(dòng),并且與tabitem的點(diǎn)擊事件進(jìn)行綁定,達(dá)到頁面滑動(dòng)切換以及點(diǎn)擊tabitem進(jìn)行切換的效果。
HorizontalPager主要參數(shù)解析:
- count 總頁面數(shù)
- state 當(dāng)前選中的頁面狀態(tài)
使用HorizontalPager需要導(dǎo)入以下資源:
implementation "com.google.accompanist:accompanist-pager:$accompanist_pager"http://0.20.2
具體實(shí)現(xiàn)步驟如下:
先通過remember記錄住當(dāng)前選中的下標(biāo),這個(gè)主要作用與tabItem的切換
//記錄頁面狀態(tài) val indexState = remember { mutableStateOf(0) }
然后通過rememberPagerState記錄HorizontalPager的currentPager也就是當(dāng)前頁面下標(biāo)
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進(jìn)行頁面切換
//頁面切換 LaunchedEffect(key1 = indexState.value, block = { pagerState.scrollToPage(indexState.value) })
最后綁定底部導(dǎo)航欄并綁定點(diǎn)擊事件
//滑動(dòng)綁定底部菜單欄 /** selectedPosition = pagerState.currentPage 將當(dāng)前的currentPager賦值給tabitem的selectPosition對(duì)底部導(dǎo)航欄進(jìn)行綁定 indexState.value = it 將底部導(dǎo)航欄的點(diǎn)擊回調(diào)下標(biāo)賦值給indexState對(duì)pager進(jìn)行綁定 */ BottomTabBar(selectedPosition = pagerState.currentPage){ indexState.value = it }
到這里就能實(shí)現(xiàn)一個(gè)底部導(dǎo)航欄以及四個(gè)頁面的切換了。
首頁內(nèi)容的實(shí)現(xiàn)
Banner的實(shí)現(xiàn)
因?yàn)楂@取Banner數(shù)據(jù)要進(jìn)行網(wǎng)絡(luò)請(qǐng)求,至于網(wǎng)絡(luò)封裝就不貼代碼了,這里直接從ViewModel開始展示,具體的網(wǎng)絡(luò)代碼可以移步到項(xiàng)目進(jìn)行觀看。
首頁ViewModel
主要用于Banner和首頁文章列表的網(wǎng)絡(luò)請(qǐng)求:
class HomeViewModel : ViewModel() { private var _bannerList = MutableLiveData(listOf<BannerEntity>()) val bannerList:MutableLiveData<List<BannerEntity>> = _bannerList fun getBannerList(){ NetWork.service.getHomeBanner().enqueue(object : Callback<BaseResult<List<BannerEntity>>>{ override fun onResponse(call: Call<BaseResult<List<BannerEntity>>>,response: Response<BaseResult<List<BannerEntity>>>) { response.body()?.let { _bannerList.value = it.data } } override fun onFailure(call: Call<BaseResult<List<BannerEntity>>>, t: Throwable) { } }) } private var _articleData = MutableLiveData<ArticleEntityPage>() val articleData:MutableLiveData<ArticleEntityPage> = _articleData fun getArticleData(){ NetWork.service.getArticleList().enqueue(object : Callback<BaseResult<ArticleEntityPage>>{ override fun onResponse(call: Call<BaseResult<ArticleEntityPage>>,response: Response<BaseResult<ArticleEntityPage>>) { response.body()?.let { articleData.value = it.data } } override fun onFailure(call: Call<BaseResult<ArticleEntityPage>>, t: Throwable) { } }) } }
在調(diào)用HomePage的時(shí)候?qū)omeViewModel傳入進(jìn)去,不推薦直接在compose里面直接調(diào)用,會(huì)重復(fù)調(diào)用:
val bVM = HomeViewModel() HomePage(bVM = bVM)
HomePage的創(chuàng)建:
fun HomePage(viewModel: BottomTabBarViewModel = viewModel(), bVM:HomeViewModel){ }
數(shù)據(jù)調(diào)用進(jìn)行請(qǐng)求,首先要?jiǎng)?chuàng)建變量通過observeAsState進(jìn)行數(shù)據(jù)接收刷新
val bannerList by bVM.bannerList.observeAsState()
Compose的網(wǎng)絡(luò)請(qǐng)求要放到LaunchedEffect去執(zhí)行,才不會(huì)重復(fù)請(qǐng)求數(shù)據(jù)
val requestState = remember { mutableStateOf("") } LaunchedEffect(key1 = requestState.value, block = { bVM.getBannerList() })
繪制Banner的View,這里同樣使用到HorizontalPager,并且還使用了coil進(jìn)行網(wǎng)絡(luò)加載,需要導(dǎo)入相關(guān)依賴包
implementation 'io.coil-kt:coil-compose:1.3.0'
BannerView的代碼,實(shí)現(xiàn)大致和tabitem差不多,只是添加了一個(gè)輪播,就不做過多的極細(xì),直接貼代碼了
@ExperimentalCoilApi @ExperimentalPagerApi @Composable fun BannerView(bannerList: List<BannerEntity>,timeMillis:Long){ Box( Modifier .fillMaxWidth() .height(160.dp)) { val pagerState = rememberPagerState() var executeChangePage by remember { mutableStateOf(false) } var currentPageIndex = 0 HorizontalPager(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() -> { //記錄下當(dāng)前的頁面索引值 currentPageIndex = pagerState.currentPage } dragEvent.changedToUpIgnoreConsumed() -> { if (pagerState.targetPage == null) return@awaitPointerEventScope if (currentPageIndex == pagerState.currentPage && pagerState.pageCount > 1) { executeChangePage = !executeChangePage } } } } } } .clickable { Log.e( "bannerTAG", "點(diǎn)擊的banner item:${pagerState.currentPage} itemUrl:${bannerList[pagerState.currentPage].imagePath}" ) } .fillMaxSize()) { page -> Image( painter = rememberImagePainter(bannerList
相關(guān)文章
android閃關(guān)燈的開啟和關(guān)閉方法代碼實(shí)例
這篇文章主要介紹了android閃關(guān)燈的開啟和關(guān)閉方法代碼實(shí)例,本文直接給出代碼和配置實(shí)例,需要的朋友可以參考下2015-05-05Android實(shí)現(xiàn)整理PackageManager獲取所有安裝程序信息
這篇文章主要介紹了Android實(shí)現(xiàn)整理PackageManager獲取所有安裝程序信息的方法,實(shí)例分析了Android使用PackageManager獲取安裝程序信息的具體步驟與相關(guān)技巧,需要的朋友可以參考下2016-01-01Android程序開發(fā)之手機(jī)APP創(chuàng)建桌面快捷方式
這篇文章主要介紹了Android程序開發(fā)之手機(jī)APP創(chuàng)建桌面快捷方式 的相關(guān)資料,需要的朋友可以參考下2016-04-04Android 靜默安裝和智能安裝的實(shí)現(xiàn)方法
靜默安裝就是無聲無息的在后臺(tái)安裝apk,沒有任何界面提示。智能安裝就是有安裝界面,但全部是自動(dòng)的,不需要用戶去點(diǎn)擊。下面腳本之家小編給大家介紹下Android 靜默安裝和智能安裝的實(shí)現(xiàn)方法,感興趣的朋友一起看看吧2018-01-01android 實(shí)現(xiàn)類似微信緩存和即時(shí)更新好友頭像示例
本篇文章主要介紹了android 實(shí)現(xiàn)類似微信緩存和即時(shí)更新好友頭像示例,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01用Android Location獲取當(dāng)前地理位置的方法
本篇文章小編為大家介紹,用Android Location獲取當(dāng)前地理位置的方法。需要的朋友參考下2013-04-04