Jetpack Compose 初體驗(上)

你是否受夠了 Android 中 UI 編寫的體驗——在 xml 文件中編寫複雜的層級結構和繁多的屬性,動態化的視圖邏輯又被分裂到 Activity 中?哦,這該死的友好度和割裂感!html

這兩年,Flutter 大行其道,不管是網上的討論度仍是實際的落地項目,風頭一時無兩。因此從這個角度來講,做爲 UI 框架的 Flutter,無疑是成功的。本着借鑑的思想(或許吧,誰知道呢),Android 在 Jetpack 項目中新增了一套全新的視圖開發套件——Compose。它有着和 Flutter 同樣好看的(姑且這麼認爲吧)外表,但究竟只是一個好看的花瓶仍是才貌雙全,這得咱們本身去尋找答案。前端

Compose 當前還處於測試版本,想要使用它,咱們須要首先下載 Android studio 的 canary 版本以提供支持。你能夠在這裏下載或者在你現有的 Android studio 中打開 File -> Settings -> Appearance & Behavior -> System Settings -> Updates 菜單,而後切換到 canary 渠道再點擊 Check Now 按鈕便可更新到最新的 canary 版本。android

1、佈局

1.可組合函數

使用 Compose 建立一個界面是簡單的,只需經過 @Composable 註解定義一個可組合函數,在函數中返回界面元素組件便可。web

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}
複製代碼

該函數能夠接收參數。函數內能夠是一個組件,也能夠是多個組件的組合。api

經過 setContent 方法塊能夠設置頁面內容,相似於以前的 setContentView() 方法。markdown

setContent {
    Text(text = "Hello Compose!")
}
複製代碼

相比於以前的界面書寫過程,Compose 更「神奇」的一個體如今於它能夠直接在 Android studio 中預覽咱們編寫的界面和組件,而無需讓程序運行在設備中。網絡

咱們只須要在可組合函數的基礎上再新增一個 @Preview 註解,可是須要注意的是,預覽函數不接受參數,因此比較好的作法是在可組合函數的基礎上編寫其對應的預覽函數。數據結構

@Preview
@Composable
fun DefaultPreview() {
    Greeting("Android")
}
複製代碼

預覽函數對你的應用在設備上的最終呈現不會產生影響,Android studio 提供了一個預覽窗口能夠實時看到預覽函數所呈現的效果。app

image-20210412225335487.png

2.佈局

咱們編寫的應用界面幾乎任什麼時候候都不會是簡簡單單的單一的控件,而是必定數量的獨立控件在空間上的一種組合。框架

首先,咱們就盲猜,若是我想豎直方向排列三個文字組件,確定不是像下面這樣隨便組合三個 Text 控件。它怎麼可能那麼聰明,能知道你是想橫着排仍是豎着排,想並排排仍是旋轉開。怎麼可能有人比蘇菲更懂你!

@Composable
fun VerticalText() {
    Text("Hello World!")
    Text("Hello Again World!")
    Text("How old are you, World!")
}
複製代碼

image-20210413003422129.png

那,就組合嘍。

@Composable
fun VerticalText() {
    Column {
        Text("Hello World!")
        Text("Hello Again World!")
        Text("How old are you, World!")
    }
}
複製代碼

給三個 Text 約定個豎框框,它們就能乖乖地排起隊。

image-20210413005809783.png

這裏,悄摸摸地說一句,這要是沒有偷瞄 Flutter 的考卷 向優秀的思想借鑑,我把三個 Text 佈局在我腦門上!

固然,只有這麼生硬的排列可不行,咱們還須要加點屬性,使得整個佈局更和諧點——例如,加點邊距。

咱們但願給 Column 加一個內邊距,那麼咱們就應該給 Column 添加一個屬性。Modifier 類用來給組件添加裝飾或者行爲,如背景、邊距、點擊事件等。

@Preview(showBackground = true)
@Composable
fun VerticalText() {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Text("Hello World!")
        Text("Hello Again World!")
        Text("How old are you, World!")
    }
}
複製代碼

image-20210413210728702.png

如今,爲了讓界面看起來不那麼單調,咱們給這個界面加上下面這一張圖片。

![](Compose 初體驗.assets/hello_world_new_black.png)

將這張圖片拷貝到 drawable 資源文件夾下面,而後經過下面的方式引用。

@Preview(showBackground = true)
@Composable
fun VerticalText() {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.hello_world_new_black),
            contentDescription = null
        )
        Text("Hello World!")
        Text("Hello Again World!")
        Text("How old are you, World!")
    }
}
複製代碼

Image 的其中一個構造函數支持如下參數,其中 painter 參數和 contentDescription 參數沒有默認值,爲必傳參數。

image-20210420220142848.png

這樣,圖片就被構造出來啦,看一下效果:

image-20210420220432840.png

那怎麼該對圖片進行一些約束呢?做爲一個頭圖,我不但願它這麼譁衆取寵,作圖片要低調一點。

在上面,咱們認識了 Modifier,那就尋求它的幫助,讓咱們的圖片小一些吧。

Image(
    painter = painterResource(id = R.drawable.hello_world_new_black),
    contentDescription = null,
    modifier = Modifier
    	.width(126.dp)
    	.height(62.dp),
    contentScale = ContentScale.Inside
)
複製代碼

藉助 Modifier 將圖片的高度和寬度分別進行限定。而後經過 contentScale 參數對圖片的縮放方式進行約束。ContentScale.Inside 保持圖片比例不變的狀況下儘量地充滿父控件的體積。

把上面的 Image 放入 preview 方法,看一下效果:

image-20210420221651702.png

如今頭圖就被咱們拿捏得死死的,可是它還不是很好看,沒脖子,加個脖子。

@Preview(showBackground = true)
@Composable
fun VerticalText() {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.hello_world_new_black),
            contentDescription = null,
            modifier = Modifier
                .width(126.dp)
                .height(62.dp),
            contentScale = ContentScale.Inside
        )

        Spacer(modifier = Modifier.height(16.dp))

        Text("Hello World!")
        Text("Hello Again World!")
        Text("How old are you, World!")
    }
}
複製代碼

image-20210420222219906.png

這樣是否是好看多了,嗯,是的。

3.Material Design

谷歌霸霸的產品固然是支持 Material Design 的,那咱就看看。

作頭圖不要鋒芒畢露,作圖處事要圓滑一點。給頭圖加個圓角是個不錯的想法。

在 Android 傳統的 UI 編寫中,圓角圖片一直沒有很簡單的解決方案,須要經過諸如自定義 ImageView 的方式來實現。可是,朋友們,當你使用 Compose 框架的時候,只須要一行代碼就能夠圓角圖片的顯示!家祭無忘告乃翁。

@Preview(showBackground = true)
@Composable
fun VerticalText() {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.hello_world_new_black),
            contentDescription = null,
            modifier = Modifier
                .width(126.dp)
                .height(62.dp)
                .clip(shape = RoundedCornerShape(4.dp)),
            contentScale = ContentScale.Inside
        )

        Spacer(modifier = Modifier.height(16.dp))

        Text("Hello World!")
        Text("Hello Again World!")
        Text("How old are you, World!")
    }
}
複製代碼

這裏仍是經過 Modifier 來實現需求,怎麼樣,如今的頭圖是否是圓滑可愛了不少。

image-20210420223334597.png

頭圖這麼求上進,文字也不能落後,一篇好的文章要主次分明,錯落有致。

聲明 Typography 對象,而後給 Text 添加 style 屬性,來控制文字的樣式。

@Preview(showBackground = true)
@Composable
fun VerticalText() {
    val typography = MaterialTheme.typography
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.hello_world_new_black),
            contentDescription = null,
            modifier = Modifier
                .width(126.dp)
                .height(62.dp)
                .clip(shape = RoundedCornerShape(4.dp)),
            contentScale = ContentScale.Inside
        )

        Spacer(modifier = Modifier.height(16.dp))

        Text("Hello World!", style = typography.h3)
        Text("Hello Again World!", style = typography.body1)
        Text("How old are you, World!", style = typography.body2)
    }
}
複製代碼

Typography 提供以下預設屬性,囊括標題、子標題、段落體、按鈕等。

image-20210420225524291.png

最終效果以下:

image-20210420225406851.png

怎麼樣,是否是主次開始變得分明瞭?結構變得清晰了?情節展開得順滑了?故事開始天然了?……

固然,其餘的諸如最大行數、字體、對齊方式等均可以被配置。

2、主題

基本佈局已經差很少啦,那麼咱們再來搞一些共性的東西,就像咱們黃種人都有同樣的膚色——散在土地裏的黃,有種頑強,很是東方……

之前的 View 系統其實也有關於 theme 的定義,那些被定義的 style,在官方定義的一系列 theme 的基礎上加以擴展,造成咱們 app 的主題。

Compose 框架提供了 Material Design 的實現,Material Design Theme 天然也被應用到 Compose 中,Material Design Theme 包括了對顏色、文本樣式和形狀等屬性的定義,我們自定義這些屬性後,包括 button、cards、switches 等控件都會相應的改變它們的默認樣式。

1.顏色

顏色在前端開發中真的是無處不在了,Color 能夠幫助咱們快速地構建顏色模型。

你能夠泡着吃:

val red = Color(0xffff0000)
複製代碼

能夠扭着吃:

val blue = Color(red = 0f, green = 0f, blue = 1f)
複製代碼

欸,你還能夠幹吃:

val black = Color.Black
複製代碼

只要你喜歡,你甚至能夠空翻360度加轉體一週半的時候吃:

// 我不會空翻,也不會轉體,期待你的表現,加油!
複製代碼

Compose 提供了 Colors數來建立成套的淺色或深色:

val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

private val DarkColorPalette = darkColors(
        primary = Purple200,
        primaryVariant = Purple700,
        secondary = Teal200,
        onPrimary = Color.Green
)

private val LightColorPalette = lightColors(
        primary = Purple500,
        primaryVariant Customize= Purple700,
        secondary = Teal200,
        onPrimary = Color.Green

        /* Other default colors to override background = Color.White, surface = Color.White, onPrimary = Color.White, onSecondary = Color.Black, onBackground = Color.Black, onSurface = Color.Black, */
)
複製代碼

而後,就能夠傳遞給 MaterialTheme 使用嘍:

@Composable
fun TestComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
            colors = colors,
            typography = Typography,
            shapes = Shapes,
            content = content
    )
}
複製代碼

怎麼樣,還自動適配深色模式。

並且,咱們也能夠隨時隨地獲取到主題色:

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)
複製代碼

表面顏色和內容顏色又是另外一個概念了,許多組件都接受一對顏色和「內容顏色」:

Surface(
    color: Color = MaterialTheme.colors.surface,
    contentColor: Color = contentColorFor(color),
    …

TopAppBar(
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    …
複製代碼

這樣一來,您不只能夠設置可組合項的顏色,並且還能爲包含在可組合項中的內容提供默認顏色。默認狀況下,許多可組合項都使用這種內容顏色。例如,Text 的顏色基於其父項的內容顏色,而 Icon :「俺也同樣」,它可使用該顏色來設置其色調。

contentColorFor() 方法能夠爲任何主題顏色檢索適當的「on」顏色。例如,若是您設置 primary 背景,就會將 onPrimary 設置內容顏色。若是您設置非主題背景顏色,還應指定合理的內容顏色。使用 LocalContentColor 可檢索與當前背景造成對比的當前內容顏色。

咱們以上面自定義的 Theme 來試驗,使用它做爲咱們的主題:

@Preview
@Composable
fun TestColor() {
    TestComposeTheme {
        Button(onClick = {}) {
            Text(
                "hello world"
            )
        }
    }
}Customize
複製代碼

效果:

image-20210429183615573.png

2.字體排版

字體排版主要經過 TypographyTextStyle 類來完成。Typography 構造函數能夠提供每種樣式的默認值,所以您能夠省略不但願自定義的任何樣式:

val Rubik = FontFamily(
    Font(R.font.rubik_regular),
    Font(R.font.rubik_medium, FontWeight.W500),
    Font(R.font.rubik_bold, FontWeight.Bold)
)

val MyTypography = Typography(
    h1 = TextStyle(
        fontFamily = Rubik,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = Rubik,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = MyTypography, /*...*/)
複製代碼

若是您但願自始至終使用同一字體,請指定 defaultFontFamily 參數,並省略全部 TextStyle 元素的 fontFamily

val typography = Typography(defaultFontFamily = Rubik)
MaterialTheme(typography = typography, /*...*/)
複製代碼

使用時,能夠從主題檢索 TextStyle,如如下示例所示:

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)
複製代碼

3.形狀

Compose 中能夠輕鬆地定義各類形狀,好比圓角或者操場跑道形狀,在傳統 View 系統中實現都比較麻煩。

咱們如今修改一下上面的 Button 的形狀來看看效果:

val Shapes = Shapes(
        small = CutCornerShape(
                topStart = 16.dp,
                topEnd = 0.dp,
                bottomStart = 16.dp,
                bottomEnd = 0.dp
        ),
        medium = RoundedCornerShape(percent = 50),
        large = RoundedCornerShape(0.dp)
)
複製代碼

image-20210429192726472.png

這裏有一點須要注意的是,默認狀況下,許多組件使用這些形狀。例如,Button、TextField 和 FloatingActionButton 默認爲 small,AlertDialog 默認爲 medium,而 ModalDrawerLayout 默認爲 large。如需查看完整的對應關係,請參閱形狀方案參考文檔。

3、列表

列表也是個常見的傢伙,Android View 系統中早期的 ListView 和後來的 RecyclerView, Flutter 裏的 ListView 等。

一個列表就是許多個元素排排站,整齊筆直。那一個縱向(或橫向)的佈局中動態地添加進許多的元素不就行了。

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}
複製代碼

來,你猜,RecyclerView 是否是這麼寫的。這裏有個最大的問題,假如你是個交際花,好友從這裏排到法國,列表多到滑一夜滑不到頭,那麼一次加載是否是要耗費巨大的資源,搞很差卡死了王思聰聯繫不上你那就太不給面了,很差。

RecyclerView 最大的一個優勢是它能夠懶加載列表項,一次只加載一個屏幕的條目(四捨五入就是我對)。Compose 中可沒有 RecyclerView,可是一樣有針對這一問題優化的組件,LazyColumnLazyRow 是垂直和水平方向的懶加載列表控件。咱們先來看一下效果:

@Preview
@Composable
fun TestList() {
    LazyColumn(modifier = Modifier.fillMaxWidth(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp)) {
        // Add a single item
        item {
            Text(text = "First item")
        }

        // Add 5 items
        items(10000) { index ->
            Text(text = "Item: $index")
        }

        // Add another single item
        item {
            Text(text = "Last item")
        }
    }
}
複製代碼

這裏加載了一萬個元素的列表,看看這絲滑的效果吧(建議就着德芙食用)。

lazy_column.gif

咱們還能夠像上面同樣,經過 contentPadding 設置內容邊距,verticalArrangement 則是能夠設置 item 間間距,以及均勻地排列元素以充滿父空間。

比較遺憾地是 LazyColumnLazyRow 暫時沒法設置例如添加元素時地動畫,期待後續的加入吧。

LazyColumn 能夠輕鬆地實現粘性標題,只需使用 stickyHeader() 函數便可:

// TODO: This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}
複製代碼

上面的代碼演示瞭如何經過 Map 數據結構實現粘性標題的數據展現。

既然有列表,那麼確定會有宮格列表,LazyVerticalGrid 則可以幫助咱們實現需求。更多用法查看相關 API(沒錯,我就是 LazyBoy,但個人尊嚴決定了我不會滾)。

在實際項目開發中,咱們常常會遇到將數據分頁展現的狀況,以減小數據請求壓力。藉助 Paging 3.0 庫 能夠來進行分頁,Paging 庫是 Jetpack 中重要的一項新特性,可幫助您一次加載和顯示多個小的數據塊。按需載入部分數據會減小網絡帶寬和系統資源的使用量。

如需顯示分頁內容列表,可使用 collectAsLazyPagingItems() 擴展函數,而後將返回的 LazyPagingItems 傳入 LazyColumn 中的 items()。與視圖中的 Paging 支持相似,您能夠經過檢查 item 是否爲 null,在加載數據時顯示佔位符:

import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(lazyPagingItems) { message ->
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}
複製代碼
相關文章
相關標籤/搜索