玩會兒Compose,原神主題列表

Jetpack Compose出來有一段時間了,一直都沒有去嘗試,這次有點想法去玩一玩這個聲明性界面工具,就以“原神”為主題寫個列表吧。

整體設計參考DisneyCompose

效果圖:

image.png
image.png

數據源

因為數據比較簡單,也就只包含圖片、姓名、描述等。所以在后臺數據存儲上選擇的是Bmob后端云,一個方便前端開發(fā)的后端服務平臺。

主要數據也是從原神各大網站搜集下來的,新建表結構并且將數據填充,我們簡單看一下Bmob的后臺。

image.png

數據準備好了,那就開始我們的Compose之旅。

首頁UI繪制

整體結構

從上面的項目效果圖來看,首頁總布局屬于是一個網格列表,平分兩格,列表中的每個Item上方帶有頭像,頭像下面是角色名稱以及角色其他信息。

image.png

網格布局

因為整體分成兩列,所以選擇的是網格布局,Compose提供了一個實現-LazyVerticalGrid。

fun LazyVerticalGrid(
    cells: GridCells,
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    content: LazyGridScope.() -> Unit
)

LazyVerticalGrid中有幾個重要參數先說明一下:

  • GridCells :主要控制如何將單元格構建為列,如GridCells.Fixed(2),表示兩列平分。
  • Modifier : 主要用來對列表進行額外的修飾。
  • PaddingValues :主要設置圍繞整個內容的padding。
  • LazyListState :用來控制或觀察列表狀態(tài)的狀態(tài)對象

首頁布局是平分兩列的網格布局,那相應的代碼如下:

LazyVerticalGrid(cells = GridCells.Fixed(2)) {}

單個Item

看過了外部框架,那現在來看每個Item的布局。每個Item為卡片式,外邊框為圓角,且?guī)в嘘幱?。內部上方是一張圖片Image,圖片下方是兩行文字Text。那Item具體該怎樣布局?

我們先來看看在Compose之前,在xml中是怎么寫?例如使用ConstraintLayout布局,頂部放一個ImageView,再來一個TextView layout_constraintTop_toBottomOf ImageView,最后在來個TextViewTopToBottomOf第一個TextView。

那使用Compose應該怎么寫?

其實在Compose里也存在著ConstraintLayout布局并且具體Api的調用思路與在xml中使用也是一致的。我們就來看看具體操作。

ConstraintLayout() {
Image()
Text()
Text()
}

一共兩個元素:Image,Text,分別代表著xml里的ImageViewTextView。

  • Image:
Image(
    painter = rememberCoilPainter(request = item.url),
    contentDescription = "",
    contentScale = ContentScale.Crop,
    modifier = Modifier
           .clickable(onClick = {
                  val objectId = item.objectId
                  navController.navigate("detail/$objectId")
                 })
           .padding(0.dp, 4.dp, 0.dp, 0.dp)
           .width(180.dp)
           .height(160.dp)
           .constrainAs(image) {
                 centerHorizontallyTo(parent)
                 top.linkTo(parent.top)
           })

Image加載的是網絡圖片,則使用painter加載圖片鏈接,contentScale與xml中的scaleType相似,modifier主要設置圖片的樣式,點擊事件、寬高等。里面有一個需要注意的點constrainAs(image)

constrainAs(image) {
                        centerHorizontallyTo(parent)
                        top.linkTo(parent.top)
                    }

這段代碼主要表示Image在父布局中的位置,例如相對父布局,相對其他子控件等,有點xml中layout_constraintTop_toBottomOf內味。下面Text也是相同的道理。

  • Text
Text(text = item.name,
                color = Color.Black,
                style = MaterialTheme.typography.h6,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .padding(0.dp, 4.dp, 0.dp, 0.dp)
                    .constrainAs(title) {
                        centerHorizontallyTo(parent)
                        top.linkTo(image.bottom)
                    }
            )

Text的設置主要包含Text內容、文字類型、大小、顏色等。在constrainAs(title)里有一句top.linkTo(image.bottom),這句代碼指的就是xml中,TextView layout_constraintTop_toBottomOf ImageView

在Image和Text中發(fā)現了一個點,constrainAs(?)中傳入了一個值,且設置相對位置時也是以此值為控件的代表。這是在進行相對位置的設定之前,利用createRefs創(chuàng)建多個引用,在ConstraintLayout中作為Modifier.constrainAs的一部分分配給布局。

val (image, title, content) = createRefs()

具體代碼:

ConstraintLayout() {
            val (image, title, content) = createRefs()
            //頭像
            Image(
                //圖片地址
                painter = rememberCoilPainter(request = item.url),
                contentDescription = "",
                //圖片縮放規(guī)則
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .clickable(onClick = {//點擊事件
                        val objectId = item.objectId
                        navController.navigate("detail/$objectId")
                    })
                    .padding(0.dp, 4.dp, 0.dp, 0.dp)
                    .width(180.dp)
                    .height(160.dp)
                    .constrainAs(image) {
                        centerHorizontallyTo(parent)  //水平居中
                        top.linkTo(parent.top)//位于父布局的頂部
                    })
            //文字
            Text(text = item.name,
                color = Color.Black,//顏色
                style = MaterialTheme.typography.h6,//字體格式
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .padding(0.dp, 4.dp, 0.dp, 0.dp)
                    .constrainAs(title) {
                        centerHorizontallyTo(parent)//水平居中
                        top.linkTo(image.bottom)//位于圖片的下方
                    }
            )
            Text(text = item.from,
                color = Color.Black,
                style = MaterialTheme.typography.body1,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .padding(4.dp)
                    .constrainAs(content) {
                        centerHorizontallyTo(parent)
                        top.linkTo(title.bottom)

                    })
        }
image.png

數據填充

UI已經畫好了,接下來就是數據展示的事情。還是以ViewModel-LiveData-Repository為整體請求方式。
因為數據都存儲到了Bmob后臺,就直接使用Bmob的方式查詢數據:

private val bmobQuery: BmobQuery<GcDataItem> = BmobQuery()

fun queryRoleData(successLiveData: MutableLiveData<List<GcDataItem>>) {
        bmobQuery.findObjects(object : FindListener<GcDataItem>() {
            override fun done(list: MutableList<GcDataItem>?, e: BmobException?) {
                if (e == null) {
                    successLiveData.value = list
                } 
            }

        })
    }

具體的請求方式可參考Bmob的完檔,這里就不在贅述。
ViewModel中還是拋出一個LiveData,而UI層相對之前有一些變化。

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HomePoster(navController: NavController, model: HomeViewModel = viewModel()) {
    model.queryGcData()
    val data: List<GcDataItem> by model.getDataLiveData().observeAsState(listOf())

    LazyVerticalGrid(cells = GridCells.Fixed(2)) {
        items(data) {
            ItemPoster(navController, item = it)
        }
    }

}

Compose提供了一個viewModel()方法來獲取ViewModel實例,至于怎么拿到數據,Compose提供了LiveData的一個擴展方法 observeAsState(listOf()) 。它的主要作用是用來觀察這個LiveData,并通過State表示它的值,每次有新值提交到LiveData時,返回的狀態(tài)將被更新,從而導致每個狀態(tài)的重新組合。

拿到List數據后,網格LazyVerticalGrid就開始使用items(data){}添加列表,

 LazyVerticalGrid(cells = GridCells.Fixed(2)) {
        items(data) {
            ItemPoster(navController, item = it)
        }
    }

而ItemPoster就是我們設置Item布局的地方,將每個Item的數據傳遞給ItemPoster,利用Image、Text等控件設置imageUrl、text內容等。

@Composable
fun ItemPoster(navController: NavController, item: GcDataItem) {
    Surface(
        modifier = Modifier
            .padding(4.dp),
        color = Color.White,
        elevation = 8.dp,
        shape = RoundedCornerShape(8.dp)
    ) {
        ConstraintLayout() {
            val (image, title, content) = createRefs()

            Image(
                //設置圖片Url-item.url
                painter = rememberCoilPainter(request = item.url),
                ...)
                
              Text(text = item.name
              ...)
              
              Text(text = item.from
              ...)
        }

    }

跳轉

樣例中還有一個從列表跳轉到詳情頁的功能,Compose提供了一個跳轉組件-navigation。這個navigation與之前管理Fragment的navigation思路也是一致的,利用NavHostController進行不同頁面的管理。我們先使用 rememberNavController()方法創(chuàng)建一個NavHostController實例。

val navController = rememberNavController()

接著將navController與NavHost相關聯,且設置導航圖的起始目的地startDestination

 NavHost(navController = navController, startDestination = "Home") {}

我們將起始目的地暫時先標記為“Home”。
那如何對頁面進行管理?這就需要在NavHost中使用composable添加頁面,例如該項目有兩個頁面,一個首頁列表頁,一個詳情頁。我們就可以這樣寫:

 NavHost(
            navController = navController, startDestination = "Home"
        ) {
            composable(
                route = "Home",
            ){
                HomePoster(navController)
            }

            composable("detail/{objectId}"){
                val objectId = it.arguments?.getString("objectId")
                DetailPoster(objectId){
                    navController.popBackStack()
                }
            }
        }

第一個composable則代表的是列表頁,并且將到達目的地的路線route設置為“Home”,其實類似于ARouter框架中在每個Activity上設置Path,做一個標識作用,后面做跳轉時也是依據該route進行跳轉。

第二個composable則代表的是詳情頁,同樣設置route="detail"

那如何從列表頁跳到詳情頁?只需要在點擊事件里使用navController.navigate("detail"),傳入想要跳轉的route即可。

攜帶參數跳轉

因為詳情頁需要根據所點擊列表Item的Id進行數據查詢,點擊時要將id傳到詳情頁,這就需要攜帶參數。
在Compose中,向route添加參數占位符,如"detail/{objectId}",從composable()函數提取 NavArguments。
如下修改詳情頁:

 composable("detail/{objectId}"){
                val objectId = it.arguments?.getString("objectId")
                DetailPoster(objectId){
                    navController.popBackStack()
                }
            }

跳轉時將objectId傳到route的占位符中即可。

clickable(onClick = {
          val objectId = item.objectId
          navController.navigate("detail/$objectId")})

當然,compose navigation還支持launchMode設置、深層鏈接等,具體可查看官方文檔。

一點感受

對于用習慣了xml編寫UI的我來說,首次上手Compose其實還是蠻不習慣,Compose打破了原有的格局,給了我們一個全新的視角去看待Android,學完后有種“哦,原來UI還可以這么干?。 钡母袊@。對于Android開發(fā)者來說,其實需要這些新的路線去突破自己的固有化思維。

Compose的風格其實和Flutter有點像,估計是出于同一個爸爸的原因。但是Compose沒有Flutter的無限套娃,對Android開發(fā)者來說還是比較友好的。如果想要學習Flutter,可以用Compose作為過渡。

以上便是本篇內容,感謝閱讀,如果對你有幫助,歡迎點贊收藏關注三連走一波??

項目地址:genshin-compose

歡迎關注公 z 號:9點大前端,每天9點推薦更多前端、Android、Flutter文章

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 簡介 Jetpack Compose 是 Google 官方 2019 年推出的UI框架,它可簡化并加快 Andr...
    TTTqiu閱讀 4,285評論 1 3
  • 邂逅FLutter 萬物皆是Widget 一般縮進2個空格 文字居中 Widget Center() Materi...
    JackLeeVip閱讀 3,518評論 0 4
  • Flutter 學習筆記-基礎篇 如果你要獲取與該筆記配套的源碼,請點擊這里[https://github.com...
    Stephen_Zhou閱讀 2,313評論 2 11
  • 表情是什么,我認為表情就是表現出來的情緒。表情可以傳達很多信息。高興了當然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,914評論 2 7
  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數的可能。 ...
    yichen大刀閱讀 8,176評論 0 4

友情鏈接更多精彩內容