玩會(huì)兒Compose臼予,原神主題列表

Jetpack Compose出來(lái)有一段時(shí)間了,一直都沒(méi)有去嘗試啃沪,這次有點(diǎn)想法去玩一玩這個(gè)聲明性界面工具粘拾,就以“原神”為主題寫(xiě)個(gè)列表吧。

整體設(shè)計(jì)參考DisneyCompose

效果圖:

image.png
image.png

數(shù)據(jù)源

因?yàn)閿?shù)據(jù)比較簡(jiǎn)單创千,也就只包含圖片缰雇、姓名入偷、描述等。所以在后臺(tái)數(shù)據(jù)存儲(chǔ)上選擇的是Bmob后端云械哟,一個(gè)方便前端開(kāi)發(fā)的后端服務(wù)平臺(tái)疏之。

主要數(shù)據(jù)也是從原神各大網(wǎng)站搜集下來(lái)的,新建表結(jié)構(gòu)并且將數(shù)據(jù)填充暇咆,我們簡(jiǎn)單看一下Bmob的后臺(tái)锋爪。

image.png

數(shù)據(jù)準(zhǔn)備好了,那就開(kāi)始我們的Compose之旅爸业。

首頁(yè)UI繪制

整體結(jié)構(gòu)

從上面的項(xiàng)目效果圖來(lái)看几缭,首頁(yè)總布局屬于是一個(gè)網(wǎng)格列表,平分兩格沃呢,列表中的每個(gè)Item上方帶有頭像年栓,頭像下面是角色名稱(chēng)以及角色其他信息。

image.png

網(wǎng)格布局

因?yàn)檎w分成兩列薄霜,所以選擇的是網(wǎng)格布局某抓,Compose提供了一個(gè)實(shí)現(xiàn)-LazyVerticalGrid

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

LazyVerticalGrid中有幾個(gè)重要參數(shù)先說(shuō)明一下:

  • GridCells :主要控制如何將單元格構(gòu)建為列惰瓜,如GridCells.Fixed(2)否副,表示兩列平分。
  • Modifier : 主要用來(lái)對(duì)列表進(jìn)行額外的修飾崎坊。
  • PaddingValues :主要設(shè)置圍繞整個(gè)內(nèi)容的padding备禀。
  • LazyListState :用來(lái)控制或觀(guān)察列表狀態(tài)的狀態(tài)對(duì)象

首頁(yè)布局是平分兩列的網(wǎng)格布局,那相應(yīng)的代碼如下:

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

單個(gè)Item

看過(guò)了外部框架奈揍,那現(xiàn)在來(lái)看每個(gè)Item的布局曲尸。每個(gè)Item為卡片式,外邊框?yàn)閳A角男翰,且?guī)в嘘幱傲砘肌?nèi)部上方是一張圖片Image,圖片下方是兩行文字Text蛾绎。那Item具體該怎樣布局昆箕?

我們先來(lái)看看在Compose之前,在xml中是怎么寫(xiě)租冠?例如使用ConstraintLayout布局鹏倘,頂部放一個(gè)ImageView,再來(lái)一個(gè)TextView layout_constraintTop_toBottomOf ImageView顽爹,最后在來(lái)個(gè)TextViewTopToBottomOf第一個(gè)TextView纤泵。

那使用Compose應(yīng)該怎么寫(xiě)?

其實(shí)在Compose里也存在著ConstraintLayout布局并且具體Api的調(diào)用思路與在xml中使用也是一致的话原。我們就來(lái)看看具體操作夕吻。

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

一共兩個(gè)元素: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加載的是網(wǎng)絡(luò)圖片归园,則使用painter加載圖片鏈接,contentScale與xml中的scaleType相似稚矿,modifier主要設(shè)置圖片的樣式庸诱,點(diǎn)擊事件、寬高等晤揣。里面有一個(gè)需要注意的點(diǎn)constrainAs(image)

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

這段代碼主要表示Image在父布局中的位置桥爽,例如相對(duì)父布局,相對(duì)其他子控件等昧识,有點(diǎn)xml中layout_constraintTop_toBottomOf內(nèi)味钠四。下面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的設(shè)置主要包含Text內(nèi)容跪楞、文字類(lèi)型缀去、大小、顏色等甸祭。在constrainAs(title)里有一句top.linkTo(image.bottom)缕碎,這句代碼指的就是xml中,TextView layout_constraintTop_toBottomOf ImageView池户。

在Image和Text中發(fā)現(xiàn)了一個(gè)點(diǎn)咏雌,constrainAs(?)中傳入了一個(gè)值校焦,且設(shè)置相對(duì)位置時(shí)也是以此值為控件的代表赊抖。這是在進(jìn)行相對(duì)位置的設(shè)定之前,利用createRefs創(chuàng)建多個(gè)引用斟湃,在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 = {//點(diǎn)擊事件
                        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

數(shù)據(jù)填充

UI已經(jīng)畫(huà)好了檐薯,接下來(lái)就是數(shù)據(jù)展示的事情凝赛。還是以ViewModel-LiveData-Repository為整體請(qǐng)求方式。
因?yàn)閿?shù)據(jù)都存儲(chǔ)到了Bmob后臺(tái)坛缕,就直接使用Bmob的方式查詢(xún)數(shù)據(jù):

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
                } 
            }

        })
    }

具體的請(qǐng)求方式可參考Bmob的完檔墓猎,這里就不在贅述。
ViewModel中還是拋出一個(gè)LiveData赚楚,而UI層相對(duì)之前有一些變化毙沾。

@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提供了一個(gè)viewModel()方法來(lái)獲取ViewModel實(shí)例,至于怎么拿到數(shù)據(jù)宠页,Compose提供了LiveData的一個(gè)擴(kuò)展方法 observeAsState(listOf()) 左胞。它的主要作用是用來(lái)觀(guān)察這個(gè)LiveData寇仓,并通過(guò)State表示它的值,每次有新值提交到LiveData時(shí)烤宙,返回的狀態(tài)將被更新遍烦,從而導(dǎo)致每個(gè)狀態(tài)的重新組合。

拿到List數(shù)據(jù)后躺枕,網(wǎng)格LazyVerticalGrid就開(kāi)始使用items(data){}添加列表服猪,

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

而ItemPoster就是我們?cè)O(shè)置Item布局的地方,將每個(gè)Item的數(shù)據(jù)傳遞給ItemPoster拐云,利用Image罢猪、Text等控件設(shè)置imageUrl、text內(nèi)容等叉瘩。

@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(
                //設(shè)置圖片Url-item.url
                painter = rememberCoilPainter(request = item.url),
                ...)
                
              Text(text = item.name
              ...)
              
              Text(text = item.from
              ...)
        }

    }

跳轉(zhuǎn)

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

val navController = rememberNavController()

接著將navController與NavHost相關(guān)聯(lián)捅暴,且設(shè)置導(dǎo)航圖的起始目的地startDestination恬砂。

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

我們將起始目的地暫時(shí)先標(biāo)記為“Home”。
那如何對(duì)頁(yè)面進(jìn)行管理蓬痒?這就需要在NavHost中使用composable添加頁(yè)面泻骤,例如該項(xiàng)目有兩個(gè)頁(yè)面,一個(gè)首頁(yè)列表頁(yè)梧奢,一個(gè)詳情頁(yè)狱掂。我們就可以這樣寫(xiě):

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

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

第一個(gè)composable則代表的是列表頁(yè),并且將到達(dá)目的地的路線(xiàn)route設(shè)置為“Home”亲轨,其實(shí)類(lèi)似于ARouter框架中在每個(gè)Activity上設(shè)置Path趋惨,做一個(gè)標(biāo)識(shí)作用,后面做跳轉(zhuǎn)時(shí)也是依據(jù)該route進(jìn)行跳轉(zhuǎn)惦蚊。

第二個(gè)composable則代表的是詳情頁(yè)器虾,同樣設(shè)置route="detail"

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

攜帶參數(shù)跳轉(zhuǎn)

因?yàn)樵斍轫?yè)需要根據(jù)所點(diǎn)擊列表Item的Id進(jìn)行數(shù)據(jù)查詢(xún)莉掂,點(diǎn)擊時(shí)要將id傳到詳情頁(yè)葛圃,這就需要攜帶參數(shù)。
在Compose中,向route添加參數(shù)占位符库正,如"detail/{objectId}"曲楚,從composable()函數(shù)提取 NavArguments
如下修改詳情頁(yè):

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

跳轉(zhuǎn)時(shí)將objectId傳到route的占位符中即可褥符。

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

當(dāng)然洞渤,compose navigation還支持launchMode設(shè)置、深層鏈接等属瓣,具體可查看官方文檔载迄。

一點(diǎn)感受

對(duì)于用習(xí)慣了xml編寫(xiě)UI的我來(lái)說(shuō),首次上手Compose其實(shí)還是蠻不習(xí)慣抡蛙,Compose打破了原有的格局护昧,給了我們一個(gè)全新的視角去看待Android,學(xué)完后有種“哦粗截,原來(lái)UI還可以這么干M锇摇!”的感嘆熊昌。對(duì)于Android開(kāi)發(fā)者來(lái)說(shuō)绽榛,其實(shí)需要這些新的路線(xiàn)去突破自己的固有化思維。

Compose的風(fēng)格其實(shí)和Flutter有點(diǎn)像婿屹,估計(jì)是出于同一個(gè)爸爸的原因灭美。但是Compose沒(méi)有Flutter的無(wú)限套娃,對(duì)Android開(kāi)發(fā)者來(lái)說(shuō)還是比較友好的昂利。如果想要學(xué)習(xí)Flutter届腐,可以用Compose作為過(guò)渡。

以上便是本篇內(nèi)容蜂奸,感謝閱讀犁苏,如果對(duì)你有幫助,歡迎點(diǎn)贊收藏關(guān)注三連走一波??

項(xiàng)目地址:genshin-compose

歡迎關(guān)注公 z 號(hào):9點(diǎn)大前端扩所,每天9點(diǎn)推薦更多前端围详、Android、Flutter文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末祖屏,一起剝皮案震驚了整個(gè)濱河市助赞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赐劣,老刑警劉巖嫉拐,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異魁兼,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)咐汞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)盖呼,“玉大人,你說(shuō)我怎么就攤上這事化撕〖肝睿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵植阴,是天一觀(guān)的道長(zhǎng)蟹瘾。 經(jīng)常有香客問(wèn)我,道長(zhǎng)掠手,這世上最難降的妖魔是什么憾朴? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮喷鸽,結(jié)果婚禮上众雷,老公的妹妹穿的比我還像新娘。我一直安慰自己做祝,他們只是感情好砾省,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著混槐,像睡著了一般编兄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上声登,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天翻诉,我揣著相機(jī)與錄音,去河邊找鬼捌刮。 笑死碰煌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绅作。 我是一名探鬼主播芦圾,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼俄认!你這毒婦竟也來(lái)了个少?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤眯杏,失蹤者是張志新(化名)和其女友劉穎夜焦,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體岂贩,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡茫经,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卸伞。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡抹镊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出荤傲,到底是詐尸還是另有隱情垮耳,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布遂黍,位于F島的核電站终佛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏雾家。R本人自食惡果不足惜铃彰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望榜贴。 院中可真熱鬧豌研,春花似錦、人聲如沸唬党。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)驶拱。三九已至霜浴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蓝纲,已是汗流浹背阴孟。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留税迷,地道東北人永丝。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像箭养,于是被迫代替她去往敵國(guó)和親慕嚷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 簡(jiǎn)介 Jetpack Compose 是 Google 官方 2019 年推出的UI框架毕泌,它可簡(jiǎn)化并加快 Andr...
    TTTqiu閱讀 3,808評(píng)論 1 3
  • 邂逅FLutter 萬(wàn)物皆是Widget 一般縮進(jìn)2個(gè)空格 文字居中 Widget Center() Materi...
    JackLeeVip閱讀 3,170評(píng)論 0 4
  • Flutter 學(xué)習(xí)筆記-基礎(chǔ)篇 如果你要獲取與該筆記配套的源碼喝检,請(qǐng)點(diǎn)擊這里[https://github.com...
    蝸牛學(xué)開(kāi)車(chē)閱讀 2,004評(píng)論 2 11
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來(lái)的情緒撼泛。表情可以傳達(dá)很多信息挠说。高興了當(dāng)然就笑了,難過(guò)就哭了愿题。兩者是相互影響密不可...
    Persistenc_6aea閱讀 125,013評(píng)論 2 7
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險(xiǎn)厭惡者损俭,不喜歡去冒險(xiǎn)蛙奖,但是人生放棄了冒險(xiǎn),也就放棄了無(wú)數(shù)的可能撩炊。 ...
    yichen大刀閱讀 6,049評(píng)論 0 4