Compose:Compose 写一个简单界面

声明式 UI

目前 Android 传统的 UI 界面编写流程是这样的:先在 xml 编写静态界面,然后在 Java 或 Kotlin 代码写业务逻辑时需要更新界面则手动更新 UI。这种写法叫做命令式 UI:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="match_parent"
	android:layout_height="match_parent"

	<TextView
		android:id="@+id/nameTextView"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content" />
</LinearLayout>

// Java
Text nameTextView = findViewById(R.id.nameTextView);
nameTextView.setText(R.string.name);

// Kotlin
val nameTextView: TextView = findViewById(R.id.nameTextView);
nameTextView.setText(R.string.name);

那什么是声明式 UI?声明式 UI 就是不手动更新 UI,数据改变的时候界面就自动更新了

val name by remember { mutableStateOf("Hello") }
Text(name)

上面是 Compose 的 UI 写法,我们只需要声明一个变量 name 提供给 Composable 函数 Text(),当 name 的数值被修改时,Text() 会同步修改,不需要像传统的命令式 UI 手动还要编写更新 UI 的代码。

Compose 的环境配置

新项目配置 Compose 可以在创建项目的地方直接选择 Compose:

在这里插入图片描述

老项目配置 Compose 最简单的方式是可以在新建的 Compose 项目将 Compose 的环境配置复制过来;最新的环境配置也可以参考 Compose 官方文档

app module 的 build.gradle 配置

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk 33

    defaultConfig {
        applicationId "com.example.compose"
        minSdk 24
        targetSdk 33
        versionCode 1
        versionName "1.0"

        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.3.2'
        kotlinCompilerVersion '1.7.10'
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {
	implementation 'androidx.core:core-ktx:1.10.1'
	implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
	implementation 'androidx.activity:activity-compose:1.7.2'
	implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
    implementation platform('androidx.compose:compose-bom:2023.01.00')

    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.material3:material3'
    implementation 'androidx.compose.ui:ui-tooling-preview'
}

Compose 独立于 Android 平台

Compose 是独立于平台的,这里的平台指的是 Android,即 Compose 是不跟随 Android 系统更新的。例如 RecyclerView、ConstraintLayout、ViewModel、Jetpack 等目前都是独立于最新版 Android,有了新版本新改动可以直接发布给开发者使用,而不像系统控件只能等开发者升级到最新的系统版本时才同步。

Compose 做的更彻底,它是独立于 Android,与 Android 无关,但也并不是完全无关,最终底层的绘制还是用的原生的 Canvas 实现。只是上层暴露的接口(Text()、Image() 等)是和平台无关的,底层和 Android 有关。这样能更好的实现多平台支持。比如目前 Compose 已经支持了桌面版(Mac、Windows、Linux)和 Web。

为什么 Android Studio 对于界面的预览功能一直这么基础,稍微复杂一点就预览不了呢?因为 Android 的程序是基于 Android 系统的,而要在 Android Studio 这个不是 Android 系统的软件去显示 xml 文件,就得在 Android Studio 里面去模拟一个解析 xml 文件并且将界面显示出来的流程,这样就需要在 Android Studio 定制一个简化版的仅用于模拟显示的小的 Android 系统才能完美的预览,这就太复杂太麻烦了。

而 Compose 因为独立于 Android 平台,Android 团队就能够用一种相对较低成本完成 Compose 的完整预览系统,所以 Compose 的预览功能非常强大,可以切换到交互模式做手势预览、动画预览等等,有这套完整的预览系统很大程度上要归功于 Compose 是独立于平台。

文字、图片和 Accompanist

@Preview
@Composable
fun ShowText() {
    Text("vincent")
}

@Preview
@Composable
fun ShowImage() {
    Image(painterResource(R.drawable.ic_launcher_background), "Avatar")
}

使用 Compose 表示一个文字使用的 Text(),使用 Image() 表示的图片。

Compose 有两种方式表示的图片:ImageBitmap 和 ImageVector,分别代表的位图和矢量图。而使用 Image() 最常用的就是使用 painterResource() 指定一个资源 id 创建一个内部包含 ImageBitmap 或 ImageVector 的 Painter,Painter 类似于 Drawable

Compose 是没有提供处理显示网络图片,就像我们使用 Glide 那样,Compose 当前也有支持图片加载的三方库 coil(Coroutine Image Loader),coil 是目前受到 Android 官方推荐的图片加载库,它不是面向 View 系统的协程实现的图片加载库:

implementation 'io.coil-kt:coil-compose:2.3.0'
implementation 'io.coil-kt:coil:2.3.0'

Image(rememberAsyncImagePainter("url"), contentDescription = "Coli Image", Modifier.size(128.dp))

coil 其实一开始并没有支持 Compose,只是它是基于 Kotlin 编写的图片加载库。在早期为了能够让控件支持 Compose,Google 提供了 accompanist 的三方库,它是由多个库组成的,作用就是用于作为 Compose 的辅助库和孵化器,其中就有支持例如 Picasso、Glide、coil 对 Compose 的支持,还有提供 Pager() 平替传统 UI 的 ViewPager 的 Composable,只是随着时代的变化,Accompanist 中的库会随着 Compose 的完善逐渐的将这些支持移除或合并到 Compose 中。

在 Compose 里面的 API 都已经趋于稳定,而 Accompanist 作为辅助和孵化器它的 API 相对于 Compose 会不稳定一些,需要经过验证才会决定是否被 Compose 收录。

传统 Layout 的 Compose 平替

传统的 Layout 主要有 FrameLayout、RelativeLayout、LinearLayout、ConstraintLayout、RecyclerView、ScrollView、ViewPager,在 Compose 也能同样找到相应平替的 Composable。

这里只列出常用的 Composable,更多内容可以查阅官方文档:Compose 中的布局

平替 FrameLayout 和 RelativeLayout:Box

Box 可以实现 FrameLayout 叠加显示控件的效果;RelativeLayout 也能使用 Box 平替,只是需要加上 Modifier。

@Preview
@Composable
fun ShowBox() {
    Box {
        Image(painterResource(R.drawable.ic_launcher_background), "Avatar")
        Text("vincent")
    }
}

在 Compose 的 setContent {} 同样也是叠加的效果:

class MainActivity : ComponentActivity {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContent {
			Image(painterResource(R.drawable.ic_launcher_background), "Avatar")
			Text("vincent")
		}
	}
}

在这里插入图片描述

平替 LinearLayout:Column 和 Row

Column 和 Row 分别实现了 LinearLayout 的垂直排列和水平排列的效果,Column 和 Row 内部是共用的一套逻辑,按方向拆开是为了更好的给开发者方便使用。

@Preview
@Composable
fun ShowColumn() {
    Column {
        Text("vincent")
        Image(painterResource(R.drawable.ic_launcher_background), "Avatar")
    }
}

@Preview
@Composable
fun ShowRow() {
    Row {
        Text("vincent")
        Image(painterResource(R.drawable.ic_launcher_background), "Avatar")
    }
}

在这里插入图片描述
在这里插入图片描述

平替 RecyclerView:LazyColumn 和 LazyRow

LazyColumn 和 LazyRow 分别实现了垂直列表和水平列表的效果,而且相比传统 UI,在 Compose 不需要再写 Adapter 也不需要写 ViewHolder 更加简单,想怎么摆放直接写:

@Preview
@Composable
fun ShowLazyColumn() {
    val list =
        listOf("1", "2", "3", "4", "5", "6", "7", "8", "9")
    LazyColumn {
        items(list) { item ->
            Text(item)
        }
    }
}

@Preview
@Composable
fun ShowLazyRow() {
    val list =
        listOf("1", "2", "3", "4", "5", "6", "7", "8", "9")
    LazyRow {
        items(list) { item ->
            Text(item)
        }
    }
}

在这里插入图片描述

在这里插入图片描述

在 Compose 也可以设置单个列表项,再也不用 viewType 判断显示不同的类型而是直接添加,也可以用来添加 header/footer:

@Preview
@Composable
fun ShowLazyColumn() {
    val list =
        listOf("1", "2", "3", "4", "5", "6", "7", "8", "9")
    LazyColumn {
    	// 添加单个列表项
        item {
            Image(painterResource(R.drawable.ic_launcher_background), "Avatar")
        }

        items(list) { item ->
            Text(item)
        }
    }
}

在这里插入图片描述

平替 ScrollView:Modifier.verticalScroll() 和 Modifier.horizontalScroll()

在 Compose 并没有单独提供 Composable 平替 ScrollView,而是通过 Modifier.verticalScroll() 和 Modifier.horizontalScroll() 设置到 Composable 就能实现滚动效果:

@Preview
@Composable
fun ShowScrollView() {
	Column(Modifier.verticalScroll()) {	
	}
	Column(Modifier.horizontalScroll()) {
	}
}

平替 ViewPager:HorizontalPager 和 VerticalPager

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

在这里插入图片描述

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
VerticalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

在这里插入图片描述

更具体的使用方式可以查阅官方文档:Compose 中的分页器

Modifier

设置内边距和外边距

在 Compose 设置内边距和外边距并不是和传统 UI 那般分成了两个属性即 padding 和 margin,而是 统一由 Modifier.padding() 来完成设置内外边距。而区分内边距和外边距最主要的是在于背景色(即以背景作为对比参照物)

@Preview
@Composable
fun MarginAndPadding() {
	// 先设置背景,再设置padding,其实就是内边距,反之就是外边距
    Row(
        Modifier
            .background(Color.Red)
            .padding(8.dp)
    ) {
        Text(
            "vincent", Modifier
                .padding(8.dp)
                .background(Color.Green)
        )
    }
}

在这里插入图片描述

需要注意的是,Modifier 是对调用顺序敏感的

  • 先写 Modifier.background() 后写 Modifier.padding() 是内边距

  • 先写 Modifier.padding() 后写 Modifier.background() 是外边距

如果要同时先设置外边距然后再设置内边距,那就是 Modifier.padding().background().padding()。

因为 Modifier 对顺序敏感,所以函数的调用会是依次生效的,而不是先调用的被后调用的相同函数覆盖

设置背景形状

Modifier.background(color, Shape) 提供的 Shape 可以处理切割各种背景的形状,例如 RoundedCornerShape 圆角矩形;Modifier.clip(Shape) 也可以处理切割形状,例如 Modifier.clip(CircleShape) 可以切为圆形:

@Preview
@Composable
fun ShowShape() {
    Row(
        Modifier
            .background(Color.Red, RoundedCornerShape(8.dp))
            .padding(8.dp)
    ) {
        Image(
            painterResource(R.drawable.ic_launcher_background), "Avatar",
            Modifier.clip(CircleShape)
        )
    }
}

在这里插入图片描述

设置尺寸大小

尺寸调整 Modifier 提供了三个函数:

  • Modifier.width():设置宽度

  • Modifier.height():设置高度

  • Modifier.size() :统一调整宽高

类似于传统 UI 的 layout_width 和 layout_height。

如果你有留意从一开始讲解 Compose,我们都没有像传统 UI 必须设置 layout_width 和 layout_height,这是因为 在 Compose 宽高并不是必须的,默认会使用像传统 UI 的 wrap_content 进行测量,如果你想要设置 match_parent,就得自己写对应的 Modifier,同样得也提供了三个函数:

  • Modifier.fillMaxWidth():填充满宽度

  • Modifier.fillMaxHeight():填充满高度

  • Modifier.fillMaxSize():统一填充满宽高

@Preview
@Composable
fun ShowSize() {
    Image(painterResource(R.drawable.ic_launcher_background), "Avatar", Modifier.size(8.dp))
}

在这里插入图片描述

设置文字、颜色及 Modifier 和控件函数参数的使用场景

Text() 文字的设置是使用的函数参数 fontSize、color:

@Preview
@Composable
fun ShowFontSize() {
    Text("vincent", fontSize = 16.sp, color = Color.Red)
}

在这里插入图片描述

或许你会有疑问:在上面的节点讲解中,有关属性的设置都是放在 Modifier,为什么这里就变成了由控件函数参数设置了?要怎么去理解什么时候用 Modifier,什么时候用函数参数?

在 Compose 中区分使用 Modifier 和函数参数的原则是,像这类文字设置、文字颜色的参数它是文字独有的属性,那就由 Composable 函数参数设置;而通用的属性例如背景、点击事件等则由 Modifier 设置。通用参数使用 Modifier 设置,专用参数使用 Composable 函数参数设置

点击事件

按照通用参数使用 Modifier 设置的原则,点击事件也算是通用参数,所以是使用的 Modifier.clickable {} 设置点击监听。

但需要注意的是,因为 Modifier 是对调用顺序敏感的,所以 Modifier.clickable {} 也要注意顺序,否则点击范围就会不一样

@Preview
@Composable
fun ShowClickable() {
    Column {
        Text("vincent", Modifier
            .padding(8.dp)
            .clickable { }
            .background(Color.Red)
            .padding(8.dp))

        Text("vincent", Modifier
            .padding(8.dp)
            .background(Color.Red)
            .padding(8.dp)
            .clickable { })

        Button(onClick = { }) {
            Text("按钮")
        }
    }
}

在 Compose 的按钮和传统 UI 不同,第一点不同就是传统 UI 的 Button 是自带需要设置文字的,而在 Compose 的 Button 你是需要自己提供按钮的填充内容的;第二点不同是 Button 的点击事件不是用的 Modifier 而是 Composable 函数参数。

Compose 中的 Button 需要自己填充内容,相比传统 UI 而言其实自由度更高,因为按钮并不仅仅只有文字,当需要定制化时填充的内容可能还会包括图标等其他元素,而且在 Material Design 按钮并不只有 Button,还有 OutlineButton、TextButton 等,将按钮的内容拆开会有更好的能力支持。

Button 的点击事件为什么不用 Modifier.clickable {},其实也很简单,按钮的目的就是要被点击的,设置点击监听器作为 Composable 函数参数,就是为了方便。其实 onClick {} 内部实现也是用了 Modifier.clickable {}。

Compose 的分包

在 Compose 是有做了多层设计分包的,每一层提供的处理都是层层递进,从下往上依次为:

  • androidx.compose.compile:这一层是和编译有关的,所以我们也不需要添加依赖,从 UI 的角度它是不算在分层里面的,它是和编译配置相关

  • androidx.compose.runtime:它包含整个 compose 的最底层的概念模型,比如数据结构、状态转换机制 State、mutableStateOf()、remember {}

  • androidx.compose.ui:提供和 UI 相关的最基础的功能,比如测量、绘制、触摸反馈等

  • androidx.compose.animation:动画层,比如渐变、平移等

  • androidx.compose.foundation:基础层,它提供相对完整可用的 UI 体系,比如 Image()、Column()、Row() 等都在这一层

  • androidx.compose.material:Material Design 设计的控件包层,Button() 是在这一层

为什么 Compose 要设计这么多层?这其实是 Android 团队吸取了传统 View 系统的教训,因为传统 View 系统在不断的迭代开发中慢慢的出现了严重的扩展性问题,例如 ListView 和 RecyclerView,在 ListView 那套复用机制无法给 RecyclerView 使用。所以 Android 团队在设计 Compose 时从一开始就想好了分层设计,让后续的迭代不会出现这种问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值