Jetpack Compose -> 声明式UI & Modifier

前言

本章主要介绍下 Compose 的声明式 UI 以及初级写法;

什么是声明式UI

传统UI

传统 UI 方式来声明UI

<androidx.appcompat.widget.LinearLayoutCompat    
    android:layout_width="match_parent"    
    android:layout_height="match_parent"    
    android:orientation="vertical">    
    <androidx.appcompat.widget.AppCompatImageView        
        android:layout_width="match_parent"        
        android:scaleType="centerCrop"        
        android:layout_height="300dp" />
</androidx.appcompat.widget.LinearLayoutCompat>

是通过 xml 来进行显示的,显示文字的方式是使用 TextView,它内部显示文字的方式有两种,一种是在 xml 中直接设置,通过下面这种方式设置

android:text="@string/app_name"

这种方式是通过初始值在 xml 中进行预设置的;

还有一种是在代码中直接调用 setText 进行设置

TextView textView = findViewById(R.id.text);
textView.setText("xxxx");

代码中是通过 setText 后续对值进行手动更新的;

这种需要手动对界面更新的方式 就不是声明式,属于传统式

声明式UI

指的是:在写界面的时候,只需要对界面进行一次性的声明,而不用在各种条件对界面元素进行跟条件有关的更新;

声明式 UI 自动更新界面,不需要手动更新,而传统式需要手动更新界面;

例如我们写一个 Compose 的 Text

Text(    
    text = "Hello $name!",    
    modifier = modifier
)

我们只需要把 name 作为参数传给这个 Text ,这个文字控件就会显示 name 的值;而声明式 UI 它不仅会用 name 来初始化显示 name 的值,当 name 的值发生变化的时候,Text 所显示的内容也会跟着发生改变,比如 name 的初始值是 Mars,界面第一时间显示的就是 Mars,过了一会这个 name 的值变成了 老A 了,那么界面就会跟着变成 老A,而不需要再次调用 setText 进行更新;这就是声明式 UI;并不是说界面是用声明式写出来的,而是说只需要声明就够了,不需要任何的手动更新;

传统式再次调用 setText 才能更新,这个调用不能省略,否则不会发生更新;

Compose 怎么写?

一种是直接创建一个 Compose 项目,一种是项目中增加配置,让它支持 Compose;

我的 Android Stuido 版本,可以直接创建 Compose 项目,并且这个版本的 AS 已经默认创建的就是 Compose 项目了;

点击 Next 创建完成;

完成之后,我们在 MainActivity 中可以看到一个大概的构建逻辑:

区别与传统的方式,我们通过一个 setContent 函数开启了我们的 Compose 之旅;

文字 Text 的声明

setContent {    
    Text("老A")
}

运行我们的程序,然后就可以在界面上看到 老A

Text 函数还有很多其他的用法,大家根据 API 直接去探索使用即可

图片 Image 的声明

传统 ImageView 显示图片的方式有两种,一种是位图,也就是 Bitmap;一种是矢量图,也就是 VectorDrawable;

但是在 Compose 中位图提供了一个新的 API,ImageBitmap;矢量图也提供了一个新的 API,ImageVector;

到这里的时候,可能很多人就会有疑问了,为什么要区别开来?

因为 Compose 的初衷是独立于 Android 平台,这个 [独立于平台] 说的就是不依赖于最新的 Android 系统才能使用;

例如我们熟悉的 RecyclerView,ViewPager2,AppCompat,协调者布局,ViewModel 等等一系列 Jetpack 组件,任何一个库出了新版本就能直接发布,开发者就可以直接使用,不需要等升级到最新的 Android 系统版本之后才能使用;

Compose 不仅独立与最新版本的 Android ,还独立于 Android 平台;Compose 提供的所有 API 全都不带有 Android 的痕迹;

虽然 Compose 不带有 Android 痕迹了,但是它底层还是使用的 Android 的 drawText()、drawTextRun()(可以看下这个链接,针对这两个 api 的介绍)、Canvas 来进行的绘制;

更精确的说:是上层暴露给开发者的 API 是独立于 Android 平台的;

声明一个图片 使用 Image,如果想使用 drawable 文件夹下的图片,使用 painterResource 来传递一个资源 id;

Image(
    painterResource(    
        id = R.drawable.ic_launcher_foreground),    
        contentDescription = resources.getString(R.string.app_name)
)

painterResource 这个函数内部会创建一个 ImageBitmap 或者 ImageVector 对象,取决于传递进去的时候位图还是矢量图,不过这就跟外部调用的没有关系了;painter 跟 Android 中的 drawable 比较类似,我们也可以不使用 painterResource 直接传递一个 ImageBitmap、ImageVector 也是可以的;

运行效果如上;

网络图片的加载,就需要依赖第三方库了,这里推荐一个图片库 Coil (Android 官方推荐 Coroutine Image Loader)这个库是面向 kotlin 和协程的,同时它还不是面向 View 系统的,

使用的话,直接引入依赖即可

implementation("io.coil-kt:coil-compose:2.5.0")

然后还是使用 Image 函数

Image(    
    rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),    
    contentDescription = resources.getString(R.string.app_name),    
    modifier = Modifier.size(150.dp)
)

只不过 painterResource 替换成了 rememberAsyncImagePainter,就可以传递一个网络图片url,并进行加载;

设置图片尺寸,使用 Modifier 关键字;

别忘了添加网络权限

<uses-permission android:name="android.permission.INTERNET"/>

运行效果如下:

早期的 Coil 并没有支持 Jetpack Compose,如果你使用的 Coil 版本比较低,可以使用另外一个库 Accompanist 

Image 的底层也是最终调用的 Android 的原生 API drawBitmap,如果是纯色,就是 Canvas 的 drawColor 等 API;

图标 Icon 的声明

在 Compose 中,图标的声明使用 Icon 函数;

Column {
    Icon(    
        imageVector = Icons.Filled.Favorite, contentDescription = ""
    )
}

运行效果如下:

Icon 的用法基本上和 Image 上一样;

按钮 Button 的声明

在Compose当中,Button 和 Text 之间并没有什么关系。它们是两个独立的控件,并且通常它们还需要配合在一起使用才行;

先来看下不配合 Text 的效果

Button(onClick = {}) {    }

运行效果如下:

按钮是没有文案的,需要给 Button 设置一个文案

Button(onClick = {}) { 
    Text(text = "我是老A")
}

运行效果如下:

点击事件的话,就直接在 onClick = { }  中处理即可

Button(onClick = {    
    Toast.makeText(LocalContext.current as Context, "", Toast.LENGTH_LONG).show()
}) {    
    Text(text = "我是老A")
}

输入框 TextFiled 的声明

在 Compose 中,使用 TextFiled 代替了 EditText

var text by remember { mutableStateOf("") }
TextField(
    value = text, 
    onValueChange = {       
        text = it                                
    }, 
    label = {Text(text = "用户名")})

运行效果如下:

可以看到,实现了 EditeText 的效果,还是 Material 风格的;

TextField 函数还有很多其他的用法,大家根据 API 直接去探索使用即可;

使用 Layout 的 Compose 平替

Android 中常用的布局有 FrameLayout、 LinearLayout、RelativeLayout、ConstraintLayout、ScrollView、RecyclerView、ViewPager 等等

Compose 中的平替控件

FrameLayout ->  Box()

Box {
    Text(
       text = "老A"
       color = Color(0XFF886600) 
    )
    Image(
        rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),        contentDescription = resources.getString(R.string.app_name),        modifier = Modifier.size(150.dp)
    )
}

运行效果如下:

可以看到,和上面截图排列一样,也就是说,不使用布局控件,默认就是 FrameLayout排列;

LinearLayout -> Column()、Row() 表示纵向和横向;

Column {
    Text(
       text = "老A"
       color = Color(0XFF886600) 
    )
    Image(
        rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),        contentDescription = resources.getString(R.string.app_name),        modifier = Modifier.size(150.dp)
    )
}

运行效果如下:

Row {
    Text(
       text = "老A"
       color = Color.Blue
    )
    Image(
        rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),        contentDescription = resources.getString(R.string.app_name),        modifier = Modifier.size(150.dp)
    )
}

运行效果如下:

RelativeLayout -> Box()

RelativeLayout 也是使用 Box 平替,但是通过 Modifier 来修改相对位置;

Box(modifier = Modifier.border(2.dp, Color.Black).size(200.dp)){    
    Text(        
        text= "老A",        
        color = Color.Blue    
    )    
    Image(        
        rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),        
        contentDescription = resources.getString(R.string.app_name),        
        modifier = Modifier.size(150.dp).align(Alignment.CenterEnd)    
    )
}

运行效果如下:

ConstraintLayout ->  ConstraintLayout for Compose

因为约束布局非常好用,所以官方为我们迁移到了 Compose 平台,我们可以直接使用这个控件;这里并不是套壳支持,而是把它们的逻辑移植到了 Compose 里面;

implementation("androidx.constraintlayout:constraintlayout-compose:+")
ConstraintLayout(modifier = Modifier    
    .border(2.dp, Color.Black)    
    .size(400.dp)    
    .padding(10.dp)) {    
    val (portraitImageRef, usernameTextRef, desTextRef) =  remember { createRefs() }    
    Image(        
        rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),        
        contentDescription = "",        
        modifier = Modifier.size(150.dp).constrainAs(portraitImageRef){            
            top.linkTo(parent.top)            
            bottom.linkTo(parent.bottom)            
            start.linkTo(parent.start)        
        })    
    Text(        
        text = "我是老A",        
        fontSize = 16.sp,        
        maxLines = 1,        
        textAlign = TextAlign.Left,        
        modifier = Modifier.constrainAs(usernameTextRef) {            
            top.linkTo(portraitImageRef.top)            
            start.linkTo(portraitImageRef.end, 10.dp)        
        })    
    Text(        
        text = "这是我的个人描述",        
        fontSize = 12.sp,        
        maxLines = 1,        
        color = Color.Red,        
        fontWeight = FontWeight.Light,        
        modifier = Modifier.constrainAs(desTextRef) {                
            top.linkTo(usernameTextRef.bottom, 5.dp)            
            start.linkTo(portraitImageRef.end, 10.dp)        
        })
}

运行效果如下:

使用 RecyclerView(竖向) 的 Compose 平替 -> LazyColumn

val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul")
LazyColumn { // 没有 adapter,没有 ViewHolder    
    items(names.size) {        
        Text(text = names[it])    
    }
}

运行效果如下:

items 是遍历,item 是设置单个列表项,而且可以使用 item 来添加header、footer,也可以设置不同的 itemType,也就是不同的 ViewHolder;

val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul")
LazyColumn {    
    item {        
        Text(text = "header")    
    }    
    items(names.size) {        
        Text(text = names[it])    
    }    
    item {        
        Text(text = "footer")  
    }
}

运行效果如下:

设置不同的 itemType,

val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul")
LazyColumn {    
    item {        
        Text(text = "header")    
    }    
    items(names.size) {        
        Text(text = names[it])    
    }    
    item {        
        Image(            
            rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),            
            contentDescription = resources.getString(R.string.app_name),            
            modifier = Modifier.size(150.dp)        
        )    
    }    
    items(names.size) {        
        Text(text = names[it])    
    }    
    item {        
        Image(            
            rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),            
            contentDescription = resources.getString(R.string.app_name),            
            modifier = Modifier.size(150.dp)        
        )    
    }
}

运行效果如下:

可以看到,直接就可以滑动了,也就是实现了 Rechyclerview 的效果;

使用 RecyclerView(横向) 的 Compose 平替 -> LazyRow

val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul")
LazyRow {    
    item {        
        Text(text = "header")    
    }    
    items(names.size) {        
        Text(text = names[it])    
    }    
    item {        
        Image(            
            rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),            
            contentDescription = resources.getString(R.string.app_name),            
            modifier = Modifier.size(150.dp)        
        )    
    }    
    items(names.size) {        
        Text(text = names[it])    
    }    
    item {        
        Image(            
            rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),            
            contentDescription = resources.getString(R.string.app_name),            
            modifier = Modifier.size(150.dp)        
        )    
    }
}

运行效果如下,横向滑动的列表:

使用 ScrollView(竖向) 的 Compose 平替 -> Modifier.verticalScroll

Android 中还有一个滑动组件:ScrollView,它的内部不是动态的,它内部的内容都是预先编排好的,而不是让它自己算的,它内部的布局也是在第一时间就加载好的,而不是滑动到什么地方才加载,如果 ScrollView 加载一个很长很长的布局,有可能会内存溢出的;

而在 Compose 中的平替是没有 ScrollView 的概念的,你只需要给你需要滑动的组件加上 Modifier.verticalScroll() 函数就可以实现竖向滑动;

Column(Modifier.requiredSize(200.dp)    
        .background(Color.Blue)    
        .verticalScroll(rememberScrollState())) {    
        repeat(20) {        
            Text(            
                text= "老A",            
                color = Color.Red        
            )    
        }
}

运行效果如下:

使用 ScrollView(横向) 的 Compose 平替 -> Modifier.horizontalScroll

Row(Modifier.requiredSize(200.dp)    
        .background(Color.Blue)    
        .horizontalScroll(rememberScrollState())) {            repeat(20) {        
            Text(            
                text= "老A",            
                color = Color.Red        
            )    
        }
}

运行效果如下:

使用 ViewPager(竖向) 的 Compose 平替 -> VerticalPager

VerticalPager(10) { page ->     
    Text(        
        text = "Page: $page",        
        modifier = Modifier.fillMaxWidth()    
    )
}

运行效果如下:

使用 ViewPager(横向) 的 Compose 平替 -> HorizontalPager

HorizontalPager(10) { page ->     
    Text(        
        text = "Page: $page",        
        modifier = Modifier.fillMaxWidth()    
    )
}

这里就不运行显示了,大家可以自己运行看下效果

Modifier

padding & margin

现在我们知道怎么去写布局了,其中线性布局是我们开法中用的最多的布局了,但是我们一般并不只是在那放一个线性布局就OK了,而是我们需要做很多很多的细节设置,比如我们需要去设置一些边距,margin、padding 等等,而在 Compose 中需要使用另外一种方式来设置边距,它就是 Modifier;

Modifier 是 Compose 中的一个很重要的角色,UI 的很多设置都是通过 Modifier 来完成的,通过官方文档,我们可以知道,Modifier 有如下四个作用:

借助修饰符(Modifier),您可以修饰或扩充可组合项。您可以使用修饰符(Modifier)来执行以下操作:

  • 更改可组合项的大小、布局、行为和外观;
  • 添加信息,如无障碍标签;
  • 处理用户输入;
  • 添加高级互动,如使元素可点击、可滚动、可拖动或可缩放;

比如,我们想给一个 Row 设置一个 10dp 的 padding,我们可以这么实现

Row(Modifier.padding(8.dp)) {
    
}

但是 Compose 中的 Modifier 是没有 margin 的概念的,我们在 Andriod 中使用 padding 和 margin 来设置内边距和外边距,通常是因为背景色的原因,margin 是外边距,它是不包含在背景之内的,而 padding 它是内边距;

我们在 Compose 中设置背景色的时候,也是通过 Modifier 来执行的,Modifier.background(),但是 Modifier 对调用顺序是有先后要求的,我们可以看下下面的这个例子,以及我们如果和通过 padding 来实现 margin 的效果:

Row(    
    Modifier        
        .background(Color.Red)        
        .padding(10.dp)) {    
    Image(        
        rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),        
        contentDescription = resources.getString(R.string.app_name),        
        modifier = Modifier.size(150.dp)    
    )        
    
    Text(
        text = "我是老A", 
        Modifier.background(Color.Green).padding(10.dp)
    )    

    Text(
        text = "老A是我", 
        Modifier.padding(10.dp).background(Color.Green)
    )
}

运行效果:

可以看到 我是老A 是把背景色包含进来了,而 老A是我 是不包含的,也是 Modifier  的一个特点,设置有先后

这样,我们就只需要 padding 这一个函数就可以了;

background

我们看到,background 其实是有两个参数的,一个设置背景色,一个是 Shape,也就是形状,也就是说,我们可以给 background 设置一个形状,我们来看下效果

Row(    
    Modifier        
        .background(Color.Red, RoundedCornerShape(10.dp)) // 给背景设置一个圆角        
        .padding(10.dp)) {    
    Image(        
        rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),        
        contentDescription = resources.getString(R.string.app_name),        
        modifier = Modifier.size(150.dp)    
    )        
    
    Text(
        text = "我是老A", 
        Modifier.background(Color.Green).padding(10.dp)
    )    

    Text(
        text = "老A是我", 
        Modifier.padding(10.dp).background(Color.Green)
    )
}

运行效果如下:

我们的 CardView,可以通过 Modifier 来实现;

clip(shape: Shape)

Modifier 还提供了一个切割函数 clip,可以切各种东西,例如可以切图片,我们看下面这个例子

Row(    
    Modifier        
        .background(Color.Red, RoundedCornerShape(10.dp)) // 给背景设置一个圆角        
        .padding(10.dp)) {    
    Image(        
        rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),        
        contentDescription = resources.getString(R.string.app_name), 
        // 将图片切成圆的       
        modifier = Modifier.clip(CircleShape).size(150.dp)    
    )        
    
    Text(
        text = "我是老A", 
        Modifier.background(Color.Green).padding(10.dp)
    )    

    Text(
        text = "老A是我", 
        Modifier.padding(10.dp).background(Color.Green)
    )
}

运行效果如下:

因为图片的左右是透明的,所以看不出来效果;已经把图片切割成圆的了;

layout_width & layout_height 的 Compose 平替 size

layout_width 可以用 width 来替换,layout_height 可以用 height 来替换,如果宽高一样的话,可以直接使用 size

Row(    
    Modifier        
        .background(Color.Red, RoundedCornerShape(10.dp))        
        .width(400.dp) // 设置宽度       
        .height(800.dp) // 设置高度       
        .padding(10.dp)) {    
    Image(        
        rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),        
        contentDescription = resources.getString(R.string.app_name),        
        modifier = Modifier.clip(CircleShape).size(150.dp)    
    )        
    Text(text = "我是老A", Modifier.background(Color.Green).padding(10.dp))    
    Text(text = "老A是我", Modifier.padding(10.dp).background(Color.Green))
}

在 Android 中,我们需要强制给每个控件声明 width 和 height,但是在 Compose 中,这个并不是强制的了,因为每个组件都有一个默认规则,默认规则就是,如果你不写,那么它就相当于是传统 View 的 wrap_content 的了;

如果想使用类似 match_parent 的,则使用 Modifier.fillMaxWidth/ fillMaxHeight/ fillMaxSize 函数;

给 Text 设置颜色和大小

如果我们想给 Text 设置颜色和大小,可能第一时间我们就想到了使用 Modifier,但是,可能没有你想的那么简单,Modifier 并没有提供相关API,我们可以往上看 Text 的基础使用,发现其实 Text 的 API 提供了相关设置;

我们在声明 Text 的时候,直接使用 color 和 fontSize 来设置颜色和大小,而不用通过 Modifier,到这里的时候,可能大家就比较迷糊了,为啥还不统一呢,是不是 Jetpack Compose 的作者脑子没想好呢?然而并不是,因为 Compose 对于 Android 团队来说,是一次重要的革命,Android 团队其实在 Compose 上下了很大的功夫的;判断是 Modifier 还是函数参数,其实很简单:

通用的设置方式,使用 Modifier;专项的设置,使用函数参数

给控件设置点击监听

Android 中其实任何控件都可以设置监听的,这其实可以看做通用的设置方式,也就是说可以通过 Modifier 来给任意的控件设置一个监听

Row(    
    Modifier        
        .background(Color.Red, RoundedCornerShape(10.dp))        
        .width(400.dp)        
        .height(800.dp)        
        .padding(10.dp) 
        // 给 Row 设置一个监听       
        .clickable {            
            Toast.makeText(context, "我是 Row,我被点击了", Toast.LENGTH_LONG).show()        
        }) {    
            Image(        
                rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"),        
                contentDescription = resources.getString(R.string.app_name),        
                modifier = Modifier.clip(CircleShape).size(150.dp)    
            )        
            Text(text = "我是老A", Modifier.background(Color.Green).padding(10.dp))    
            Text(text = "老A是我", Modifier.padding(10.dp).background(Color.Green))
}

运行效果如下:

可以看到,点击事件被执行了;

好了,Compose 的初探,就先到这里吧~~

下一节预告

状态订阅和自动更新;

  • 28
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值