Jetpack (七)Android Compose 如何使用<5>

要启动新的Compose项目,请打开Android Studio Arctic Fox,然后选择启动新的Android Studio项目,如下所示:

如果没有出现以上屏幕,请转至 File > New > New Project. 我们老版本的创建项目模式

API level 21 是最低的要求

打开根目录 build.gradle

  • compose_version to the [latest version] 1.0.0-beta07:
  • Add the kotlin_version
buildscript {
    ext {
        compose_version = '1.0.0-beta07'
        kotlin_version = '1.4.32'
    }
    ...
}

会帮助你自动生成代码

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }
}

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

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greeting("Android")
    }
}

setContent中使用的应用程序主题取决于项目的命名方式。 该代码实验假定该项目名为BasicsCodelab。 如果从代码实验复制粘贴代码,请不要忘记使用ui / Theme.kt文件中可用的主题名称来更新BasicsCodelabTheme。

请注意,MainActivity.kt中的可组合函数是在MainActivity类之外的,它们被声明为顶级函数。 您在活动之外拥有的代码越多,您可以共享和重用的代码就越多。

首先,重构代码以使其更可重用,并创建一个新的@Composable MyApp函数,该函数包含特定于此Activity的Compose UI逻辑。

其次,将应用程序的背景色放置在可重用的Greeting Composable中是没有意义的。 该配置应应用于此屏幕上的每个用户界面,因此将Surface从Greeting移至新的MyApp函数:

class MainActivityDemo : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Composable
fun Greeting1(name: String) {
    Surface(color = Color.Yellow) {
        Text(text = "Hello $name!",modifier = Modifier.padding(24.dp))
    }
}

@Composable
fun MyApp(){
    MyDemoTheme {
        // A surface container using the 'background' color from the theme
        Surface(color = MaterialTheme.colors.background) {
            Greeting1("Android2")
        }
    }
}

@Preview
@Composable
fun DefaultPreview1() {
    MyApp()
}

使容器起作用

如果要创建一个包含应用程序所有常用配置的容器怎么办?

要创建通用容器,请创建一个Composable函数,该函数将返回Unit的Composable函数(此处称为content)作为参数。 您之所以返回Unit是因为,您可能已经注意到,可组合函数不返回UI组件,而是发出它们。 这就是为什么他们必须返回Unit的原因:

@Composable
fun MyApp(content: @Composable () -> Unit) {
    BasicsCodelabTheme {
        Surface(color = Color.Yellow) {
            content()
        }
    }
}

注意:使用Composable函数作为参数时,请注意@Composable()中的多余括号。 由于注释是在函数上应用的,因此需要它们!
fun MyApp(content: @Composable () -> Unit) { ... }

在函数内部,定义要容器提供的所有共享配置,然后调用传递的子代Composable。 在这种情况下,您要应用MaterialTheme和黄色,然后调用content()

class MainActivityDemo : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp{
                Greeting1(name = "Android3")
            }
        }
    }
}

@Composable
fun Greeting1(name: String) {
    Text(text = "Hello $name!",modifier = Modifier.padding(24.dp))
}

@Composable
fun MyApp(content: @Composable ()-> Unit){
     MyDemoTheme {
         Surface(color = Color.Yellow) {
             content()
         }
     }
}

@Preview("Text preview")
@Composable
fun DefaultPreview1() {
    MyApp{
        Greeting1("Android3")
    }
}

代码等效,但现在更加灵活。 使容器可组合的功能是一种良好的做法,可以提高可读性并鼓励重用代码。把Theme交给MyApp,内容传入

使用布局多次调用Composable函数

@Composable
fun Greeting1(name: String) {
    Text(text = "Hello $name!",modifier = Modifier.padding(24.dp))
}

@Composable
fun MyApp(content: @Composable ()-> Unit){
     MyDemoTheme {
         Surface(color = Color.Yellow) {
             content()
         }
     }
}


@Composable
fun MyScreenContent() {
    Column {
        Greeting1("Android")
        Divider(color = Color.Black)
        Greeting1("there")
    }
}

@Preview("Text preview")
@Composable
fun DefaultPreview1() {
    MyApp{
        MyScreenContent()
    }
}

注意:调用Composable函数时,它会将元素添加到Compose UI层次结构中。 您可以从代码的多个部分调用相同的函数(具有可能不同的参数)以添加新元素。 您可以认为这就像通过调用composable函数发出UI元素一样。

Compose 和 Kotlin

可以像Kotlin中的任何其他函数一样调用compose函数。 由于可以添加语句来影响UI的显示方式,因此构建UI非常强大。

例如 可以使用 for循环添加元素到 MyScreenContent Column:

@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
    }
}

Compose中的State 状态值

对状态更改做出反应是Compose的核心。 通过调用可组合函数,组合应用可将数据转换为UI。 如果您的数据发生更改,则可以使用新数据调用这些功能,从而创建更新的UI。 Compose提供了用于观察应用程序数据中的更改的工具,这些工具将自动调用您的功能-这称为重组recompose。 Compose还查看单个可组合物需要哪些数据,以便它仅需要重新组合其数据已更改的组件,并可以跳过不受影响的组件。

在幕后,Compose使用了自定义的Kotlin编译器插件,因此,当基础数据发生更改时,可重新调用可组合函数以更新UI层次结构。

例如,当您在MyScreenContent Composable函数中调用Greeting(“ Android”)时,正在对输入(“ Android123...”)进行硬编码,因此Greeting将被添加到UI树一次,即使更改, MyScreenContent的正文将重新组成。
PS: 就是实时更新

要向可组合对象添加内部状态,请使用mutableStateOf函数,该函数提供可组合的可变内存。 要使每个重组都有不同的状态,请使用remember记住可变状态。 并且,如果在屏幕上的不同位置有可组合对象的多个实例,则每个副本将获得其自己的状态版本。 可以将内部状态视为类中的私有变量


@Composable
fun Counter(){
    val count = remember{mutableStateOf(0)}
    Button(onClick = {count.value++}){
        Text("click ${count.value} times",
            modifier = Modifier.background(Color.Red))
    }
//    TextButton(onClick ={count.value++}) {
//        Text("click ${count.value} times",
//            modifier = Modifier.background(Color.White))
//    }

//    OutlinedButton(onClick ={count.value++}) {
//        Text("click ${count.value} times",
//            modifier = Modifier.background(Color.Green))
//
//    }
}

注意:Compose根据“Material Design Button”规范提供了不同类型的按钮,即Button,OutlinedButton和TextButton。 在您的情况下,使用一个具有文本的按钮作为按钮内容,以显示单击了多少次。

真实的业务原理......

在可组合函数中,应该公开对调用函数有用的状态,因为这是可以使用或控制的唯一方法,该过程称为状态提升。

状态提升是通过调用它的函数使内部状态可控的方法。 为此,您可以通过可控制的可组合函数的参数公开状态,并从可控制的可组合函数外部实例化该状态。

使状态可悬挂可避免重复状态和引入错误,有助于重用可组合对象,并使可组合对象实质上更易于测试。 对于可组合调用者不感兴趣的状态应该是内部的。

在某些情况下,使用者可能不关心某个状态(例如,在滚动条中,scrollerPosition状态处于暴露状态,而maxPosition则不在此状态)。 这个属于创造和控制该状态的人。

在该示例中,由于Counter的使用者可能对状态感兴趣,因此可以通过引入(count,updateCount)对作为Counter的参数,将状态完全推迟给调用者。 这样,Counter提升了其状态:

@Composable
fun MyScreenContent(names: List<String> = listOf("Android3", "there2")) {
    val countState = remember { mutableStateOf(0) }

    Column {
        for (name in names) {
            Greeting1(name = name)
            Divider(color = Color.Black)
        }
        Divider(color = Color.Black, thickness = 32.dp)
        Counter(count = countState.value,
            updateCount = { newCount -> countState.value = newCount })

    }
}

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button(onClick = { updateCount(count + 1) }) {
        Text("click $count times", modifier = Modifier.background(Color.Red))
    }
}

灵活的布局( Flexible layouts)

接触了Column 布局用于以垂直顺序放置项目。 同样,可以使用Row水平放置项目。
Column和Row将它们的项目一个接一个地放置。 如果要使某些项目具有弹性,以便它们以一定的比重占据屏幕,则可以使用weight修改器。

比如,把按钮放在底部,其他内容在屏幕顶部

  1. 用weight 将灵活的items包裹在另一column中。由于此column是灵活的,其余内容都不灵活,因此将占用尽可能多的空间。确定外部Column的大小后,它将能够使用所有剩余的高度。
  2. 将Counter保留在默认情况下不灵活的外部列中。
@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    val countState = remember { mutableStateOf(0) }


    Column(modifier = Modifier.fillMaxHeight()) {
        Column (modifier = Modifier.weight(1f)){
            for (name in names) {
                Greeting1(name = name)
                Divider(color = Color.Black)
            }
        }
        Counter(count = countState.value,
            updateCount = { newCount -> countState.value = newCount })
    }
}

您可以在外部Column上使用fillMaxHeight()修饰符,以使其占据尽可能多的屏幕(fillMaxSize()和fillMaxWidth()修饰符也可用)。

作为在Compose中利用Kotlin的另一个示例,您可以根据用户使用if ... else语句敲击Button的次数来更改Button的背景颜色:

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button(
        onClick = { updateCount(count + 1) },
        colors = ButtonDefaults.buttonColors(
            backgroundColor = if (count > 3) Color.Green else Color.White
        )
    ) {

        Text("click $count times")
    }
}

让列表更加真实。 到目前为止,已经在“列”中显示了两个项目,但它可以处理数千个项目吗? 在更改列表项之前,将“名称”列提取到专用的Composable:

@Composable
fun nameList(names:List<String>,modifier :Modifier = Modifier){
    Column(modifier = modifier) {
        for (name in names) {
            Greeting1(name = name)
            Divider(color = Color.Black)
        }

    }
}

更改MyScreenContent参数中的默认列表值以使用另一个列表构造函数,该构造函数允许设置列表大小并用其lambda中包含的值填充(此处$ it表示列表索引):

names: List<String> = List(1000) { "Hello Android #$it" }

无论是以交互方式呈现它还是将其部署在设备/仿真器上,您都将无法滚动浏览这数千行,因为默认情况下“列”是不可滚动的。

为了显示可滚动的列,我们使用LazyColumn。 LazyColumn仅渲染屏幕上的可见项,从而在渲染大列表时提高性能。 它等效于Android视图中的RecyclerView。

由于列表中包含成千上万的项目,因此渲染时会影响应用程序的流畅性,因此请使用LazyColumn仅在屏幕上渲染可见元素,而不是所有元素。

在其基本用法中,LazyColumn API在其作用域内提供了一个items元素,其中编写了单独的项目呈现逻辑:

@Composable
fun Greeting1(name: String) {
    Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))
}

@Composable
fun MyApp(content: @Composable () -> Unit) {
    MyDemoTheme {
        Surface(color = Color.Yellow) {
            content()
        }
    }
}

@Composable
fun nameList(names: List<String>, modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier) {
        items(count = names.size) { index ->
            Greeting1(name = names.get(index))
            Divider(color = Color.Black)
        }
    }
}

@Composable
fun MyScreenContent(names: List<String> = List(1000) { "Hello Android #$it" }) {
    val countState = remember { mutableStateOf(0) }
    Column(modifier = Modifier.fillMaxHeight()) {
        nameList(names = names, Modifier.weight(1f))
        Counter(count = countState.value,
            updateCount = { newCount -> countState.value = newCount })
    }

}

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button(
        onClick = { updateCount(count + 1) },
        colors = ButtonDefaults.buttonColors(
            backgroundColor = if (count > 3) Color.Green else Color.White
        )
    ) {

        Text("click $count times")
    }
}


@Preview("Text preview")
@Composable
fun DefaultPreview1() {
    Column {
        MyApp {
            MyScreenContent()
        }
    }
}

动画列表

现在,让列表更具交互性。
假设您要在单击列表项后更改其背景色。 您已经使用按钮完成了此操作,但是这次,从一种背景色到另一种背景色的过渡将是动态的,而不是瞬间的,如下:

为此,您将使用animateColorAsState API,但首先需要更新Greeting Composable以添加isSelected状态(将其记住为false初始化)和单击处理程序以切换该状态

@Composable
fun Greeting1(name: String) {
    // 记录选中状态
    var isSelected by remember { mutableStateOf(false) }

    val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)

    Text(text = "Hello $name!", modifier = Modifier
        .padding(24.dp)
        .background(color=backgroundColor)
        .clickable(onClick = {isSelected = !isSelected}))
}

注意:由于在问候语可组合物中悬挂了isSelected状态,因此NameList将不会跟踪其列表项是否被选中。 一旦项目滚动出屏幕,它们的状态将被设置为false。 此行为旨在作为此练习的目标,是保留一个简单列表。 为了跟踪列表中的选定项目,应将其isSelected状态提升到NameList级别。

App 主题

自定义App Theme 在其实现中使用MaterialTheme。 MaterialTheme是可组合的功能,反映了Material Design规范中的样式原则。 样式信息会向下层叠到其中的组件,这些组件可能会读取该信息以对其进行样式设置。 在原始的简单UI中,可以按以下方式使用:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyDemoTheme {
                Greeting(name = "Android")
            }
        }
    }
}

因为MyDemoTheme在内部包装了MaterialTheme,所以Greeting用主题中定义的属性设置样式。 您可以通过以下方式检索MaterialTheme的属性并使用它们来定义Text的样式:

@Composable
fun Greeting(name: String) {
    Text (
        text = "Hello $name!",
        modifier = Modifier.padding(24.dp),
        style = MaterialTheme.typography.h1
    )
}

上面的示例中可组合的Text设置了三个参数,要显示的字符串,modifiers和TextStyle。 您可以创建自己的TextStyle,也可以使用MaterialTheme.typography检索主题定义的样式。 此结构使您可以访问Material定义的文本样式,例如h1,body1或subtitle1。 在您的示例中,使用了在主题中定义的h1样式。

注意:您可以使用复制功能来修改预定义的样式。
例如,style = MaterialTheme.typography.body1.copy(color= Color.Yellow)

创建你自己的app 主题

主题是可组合函数,它接受其他子级可组合函数。 为了使其可重用,可以像在“声明式UI”部分中那样创建一个容器Composable函数:

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {

    // TODO 
}

MaterialTheme拥有颜色和版式的配置。 您只需在此时更改一些颜色即可实现所需的设计。

private val DarkColors = darkColors(
    primary = purple200,
    primaryVariant = purple700,
    secondary = teal200
)

private val LightColors = lightColors(
    primary = purple500,
    primaryVariant = purple700,
    secondary = teal200
)

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColors
    } else {
        LightColors
    }

    MaterialTheme(colors = colors) {
        content()
    }
}

您提供的自定义颜色将覆盖lightColors和darkColors方法中的自定义颜色,除非另行提供,否则它们将默认颜色为浅色和深色“材质”基线主题。 如您所见,这将传递给MaterialTheme的构造函数,该构造函数实现Material设计规范中的样式原则。

以相同的方式,您可以通过将它们传递给MaterialTheme函数来覆盖应用程序中使用的版式和形状。

我们用到的微件元素

  • Surface

  • Modifiers

  • Preview

  • @Composable
    @Composable批注仅对于发出UI或调用其他可组合函数的函数是必需的。 他们可以调用常规函数和其他可组合函数。 如果函数不满足这些要求,则不应使用@Composable对其进行注释。

  • Divider

  • remember

  • mutableStateOf

  • Text

  • Button

  • Column 布局

  • LazyColumn ----相当RecyclerView

  • MaterialTheme

注意点:

请注意,MainActivity.kt中的可组合函数是在MainActivity类之外的,它们被声明为顶级函数。 您在Activity之外拥有的代码越多,您可以共享和重用的代码就越多。

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容