compose--初入compose、资源获取、标准控件与布局

compose正式发布已经一年多了,越来越多的开发人员选择使用它,声明式UI也是未来的一个主流趋势,本人也是一年前学习后,并没有真正的使用,所以本着边学习,边分享的心态,准备写个compose系列的文章
首先compose目前只支持kotlin,基于google对移动端的鸿图,未来应该也不会支持其他语言,和传统安卓的xml布局不同,compose是通过kotlin定义一个一个组件,由于是通过代码定义的组件,每个组件都可以很方便的重用,这点在UI开发时确实便利了不少。至于声明式UI和命令式UI的区别,相信你会在后续实际使用时有很大的感触

一、认识compose

通过官方文档我们可以了解到compose的编程思想。官方地址:https://developer.android.google.cn/jetpack/compose/mental-model

我这边也是根据官方文档,对重要的部分和自己的想法进行融合,来介绍什么是compose。这部分内容都是概念性的,但是贯穿整个compose的学习,应该进行着重深入理解

1. 重组

1.1 安卓传统UI

先来说在安卓传统UI,大致的流程就是xml中我们定义了一系列的布局(组件)和控件后,由ActivityonCreate()触发xml解析,生成View树DecorView,并ActivityhandleResumeActivity()ViewRootImpl绑定,通过Binder通信,交由由WindowManagerService创建surface进行渲染,最终呈现在手机屏幕

当然了,我们只需要关注在onCreate()中设置xml即可,由于布局是一次性加载的,即生成View树的过程是同步进行的

1.2 compose UI

对与compose而言,每个可组合函数(组件)的调用可能发生在与调用方不同的线程上,即每个组件添加至View树的过程,都是通过协程进行的,上树的过程未必按代码调用的顺序执行

1.3 什么是重组?

在compose中,每个可组合函数调用直至渲染完成,称之为重组
通过异步上树虽然带来了性能的提升,但是管理方面变得困难,所以compose规定,每个可组合函数都是独立运行的存在,可组合函数内部应该仅处理的UI操作,重组的发生的时机并不由我们控制,而是由compose内部自动管理,后续我们可以使用状态来通知compose进行重组

二、创建compose项目

推荐使用最新的android studio,低版本并不支持compose,也可以查看官方文档-快速入门:https://developer.android.google.cn/jetpack/compose/setup

1.创建项目

我这边尝鲜使用MD3风格的项目,实际开发中google也推荐:UI设计从MD2转变为MD3

2.BOM

对于compose的版本管理,官方推荐使用BOM,导入BOM后的好处是:导入compose其他库组,都将使用BOM中定义的版本,后续更新,我们只需要更新BOM的版本即可。下面是官方给出的BOM:compose版本对应关系:

库组 版本 (2022.10.00) 版本 (2022.11.00)
androidx.compose.animation:animation 1.3.0 1.3.1
androidx.compose.animation:animation-core 1.3.0 1.3.1
androidx.compose.animation:animation-graphics 1.3.0 1.3.1
androidx.compose.foundation:foundation 1.3.0 1.3.1
androidx.compose.foundation:foundation-layout 1.3.0 1.3.1
androidx.compose.material:material 1.3.0 1.3.1
androidx.compose.material:material-icons-core 1.3.0 1.3.1
androidx.compose.material:material-icons-extended 1.3.0 1.3.1
androidx.compose.material:material-ripple 1.3.0 1.3.1
androidx.compose.material3:material3 1.0.0 1.0.1
androidx.compose.material3:material3-window-size-class 1.0.0 1.0.1
androidx.compose.runtime:runtime 1.3.0 1.3.1
androidx.compose.runtime:runtime-livedata 1.3.0 1.3.1
androidx.compose.runtime:runtime-rxjava2 1.3.0 1.3.1
androidx.compose.runtime:runtime-rxjava3 1.3.0 1.3.1
androidx.compose.runtime:runtime-saveable 1.3.0 1.3.1
androidx.compose.ui:ui 1.3.0 1.3.1
androidx.compose.ui:ui-geometry 1.3.0 1.3.1
androidx.compose.ui:ui-graphics 1.3.0 1.3.1
androidx.compose.ui:ui-test 1.3.0 1.3.1
androidx.compose.ui:ui-test-junit4 1.3.0 1.3.1
androidx.compose.ui:ui-test-manifest 1.3.0 1.3.1
androidx.compose.ui:ui-text 1.3.0 1.3.1
androidx.compose.ui:ui-text-google-fonts 1.3.0 1.3.1
androidx.compose.ui:ui-tooling 1.3.0 1.3.1
androidx.compose.ui:ui-tooling-data 1.3.0 1.3.1
androidx.compose.ui:ui-tooling-preview 1.3.0 1.3.1
androidx.compose.ui:ui-unit 1.3.0 1.3.1
androidx.compose.ui:ui-util 1.3.0 1.3.1
androidx.compose.ui:ui-viewbinding 1.3.0 1.3.1

工程中导入:

dependencies {
    def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
    implementation composeBom

...
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
    implementation 'androidx.activity:activity-compose'
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.ui:ui-tooling-preview"
    implementation 'androidx.compose.material3:material3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.3.1"
    debugImplementation "androidx.compose.ui:ui-tooling"
    debugImplementation "androidx.compose.ui:ui-test-manifest"
}

3.kotlin-compose compiler版本对应

BOM中不包含Compose编译器库,所以我们需要手动对应下kotlin版本与compose compiler版本,下面是两者的兼容关系,官网也可以查询到最新的对应关系:
https://developer.android.google.cn/jetpack/androidx/releases/compose-kotlin

Compose Compiler 版本 兼容的 Kotlin 版本
1.4.0-alpha01 1.7.20
1.3.2 1.7.20
1.3.1 1.7.10
1.3.0 1.7.10
1.3.0-rc02 1.7.10
1.3.0-rc01 1.7.10
1.3.0-beta01 1.7.10
1.2.0 1.7.0
1.2.0-rc02 1.6.21
1.2.0-rc01 1.6.21
1.2.0-beta03 1.6.21
1.2.0-beta02 1.6.21
1.2.0-beta01 1.6.21
1.2.0-alpha08 1.6.20
1.2.0-alpha07 1.6.10
1.2.0-alpha06 1.6.10
1.2.0-alpha05 1.6.10
1.2.0-alpha04 1.6.10
1.2.0-alpha03 1.6.10
1.2.0-alpha02 1.6.10
1.2.0-alpha01 1.6.10
1.1.1 1.6.10
1.1.0 1.6.10
1.1.0-rc03 1.6.10
1.1.0-rc02 1.6.10
1.1.0-rc01 1.6.0
1.1.0-beta04 1.6.0
1.1.0-beta03 1.5.31
1.1.0-beta02 1.5.31
1.1.0-beta01 1.5.31
1.1.0-alpha06 1.5.31
1.1.0-alpha05 1.5.31
1.0.5 1.5.31
1.0.4 1.5.31
1.1.0-alpha04 1.5.30
1.1.0-alpha03 1.5.30
1.0.3 1.5.30
1.1.0-alpha02 1.5.21
1.1.0-alpha01 1.5.21
1.0.2 1.5.21
1.0.1 1.5.21
1.0.0 1.5.10
1.0.0-rc02 1.5.10
1.0.0-rc01 1.5.10

我这边使用的是1.3.1,对应kotlin版本是1.7.10,工程中build.gradle

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.3.1"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

主工程中build.gradle:

plugins {
    id 'com.android.application' version '7.3.1' apply false
    id 'com.android.library' version '7.3.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
}

4.预览compose函数与启动

4.1 预览compose函数

引入了ui-tooling-preview库组后,我们可以使用@Preview注解可组合函数,并实现预览组件

4.2 启动

启动到模拟器的效果:

三、资源获取

xml中,我们常常会使用资源id获取到资源文件,比如:color、drawable、string等,在compose中,通过以下函数获取,这些函数都位于androidx.compose.ui.res包下:

当然我们并不需要使用里面全部的类,掌握下面列出的即可:

资源获取方式 描述
stringResource 获取对应id的string资源,并支持传入多个参数,来实现字符串格式化
colorResource 获取对应id的color资源
painterResource 获取对应id的图片资源,可以是一个vector,也可以是drawable
dimensionResource 获取对应id的dimen资源,由于compose推荐使用md主题设置dimen,用的也不多

四、标准控件

compose本身内置了一些组件,官方说法所有组件都是可组合函数,这边仅仅是便于传统开发理解,分成控件和布局来介绍,这些内置可组合函数分散在各个不同的库组内,如:androidx.compose.foundationandroidx.compose.foundation.layoutandroidx.compose.material3
其中控件大多位于md包下,他们都具有MD风格,也是官方推荐使用的组件:

1.Text

Text用于呈现一段文字,是使用最多的组件,官方也详细的介绍了该组件:https://developer.android.google.cn/jetpack/compose/text

1.1 基本使用

所有compose函数都要由@Composable注解,并且每个可组合函数都是可以重用的组件:

@Composable
@Preview
fun MyText() {
    Text(text = "hello world!")
}

预览效果:

1.2 使用资源获取文本

通过stringResource(id)获取String,可以达到同样的效果

@Composable
@Preview
fun MyText() {
    Text(text = stringResource(id = R.string.hello))
}
1.3 AnnotatedString

传统UI的TextView,可以通过Span来改变文本的内嵌样式,比如个别字颜色设置、设置背景颜色等效果
compose中可以使用AnnotatedString来达到这种效果,通过buildAnnotatedString()构建一个AnnotatedStringAnnotatedString可以包含多个 SpanStyle(点击跳转API)ParagraphStyle(点击跳转API)

  • SpanStyle:设置文本的内嵌样式
  • ParagraphStyle:设置文本的行高,对齐方式,文字方向和文字缩进样式

例子:

@Composable
@Preview
fun MyText() {
    Text(
        text = buildAnnotatedString {
            withStyle(
                style = ParagraphStyle(
                    lineHeight = 30.sp,//行高
                    textAlign = TextAlign.Left,//左对齐
                    textIndent = TextIndent(firstLine = 10.sp)//缩进
                )
            ) {
                withStyle(
                    style = SpanStyle(
                        fontSize = 20.sp,
                        color = Color.Red,//设置颜色为红色
                        fontWeight = FontWeight.Medium//加粗
                    )
                ) {
                    append("hi\n")
                }
            }

            withStyle(
                style = ParagraphStyle(
                    lineHeight = 60.sp,
                )
            ) {
                withStyle(
                    style = SpanStyle(
                        color = Color.Red,
                        shadow = Shadow(//设置阴影
                            color = Color.Blue,//阴影颜色
                            blurRadius = 3f,//虚化
                            offset = Offset(5f, 20f)//x,y轴的偏移
                        )
                    )
                ) {
                    append("你好\n")
                }
            }
        }
    )
}

预览效果:

1.4 其他参数

其他参数可以通过源码查看:

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,//修饰符
    color: Color = Color.Unspecified,//颜色
    fontSize: TextUnit = TextUnit.Unspecified,//字体
    fontStyle: FontStyle? = null,//字体样式,正常或斜体
    fontWeight: FontWeight? = null,//字体粗细
    fontFamily: FontFamily? = null,//字体
    letterSpacing: TextUnit = TextUnit.Unspecified,//字间距
    textDecoration: TextDecoration? = null,//字体装饰,删除线、下划线等
    textAlign: TextAlign? = null,//内容对齐方式,居中、左对齐、右对齐等
    lineHeight: TextUnit = TextUnit.Unspecified,//行高
    overflow: TextOverflow = TextOverflow.Clip,//内容超出处理方式,截断、使用...等
    softWrap: Boolean = true,//是否自动换行
    maxLines: Int = Int.MAX_VALUE,//最大行数
    onTextLayout: (TextLayoutResult) -> Unit = {},//文本变化导致重组的回调
    style: TextStyle = LocalTextStyle.current//更丰富的字体样式,包含上面大多数设置,以及SpanStyle和ParagraphStyle
) {
...
}

其中Modifier后续会详细介绍,举例使用里面的几个参数设置,如使用TextStyle去除首行的顶部行间距:

<string name="hello">hello!\nworld</string>
@Composable
@Preview
fun MyText() {
    Text(
        text = stringResource(id = R.string.hello),
        fontWeight = FontWeight.Medium,
        overflow = TextOverflow.Clip,
        //将当前的style和另一个合并,以另一个设置的属性为优先
        style = LocalTextStyle.current.merge(
            TextStyle(
                lineHeight = 2.5.em,
                platformStyle = PlatformTextStyle(
                    includeFontPadding = false//配合trim
                ),
                lineHeightStyle = LineHeightStyle(
                    alignment = LineHeightStyle.Alignment.Center,
                    // trim生效需要includeFontPadding = false
                    // trim是指将行间距尽可能的去除
                    // FirstLineTop:将第一行顶部的行间距去除
                    trim = LineHeightStyle.Trim.FirstLineTop
                )
            )
        )
    )
}

预览效果:

2.Image

Image用于展现图片

2.1 基本使用

必传入参为图片资源对象painter和内容描述contentDescriptioncontentDescription主要是为了残疾人使用的,国外对于残疾人使用也非常的重视,此外使用python自动化测试也可以通过contentDescription找到该组件:

@Composable
@Preview
fun MyImage() {
    Image(
        painter = painterResource(id = R.drawable.ic_launcher_foreground),//指定图片资源
        contentDescription = "my image" //描述,残疾人以及自动化测试使用
    )
}

预览效果:

2.2 其他参数

相较于TextImage的参数少很多:

@Composable
fun Image(
    painter: Painter,
    contentDescription: String?,
    modifier: Modifier = Modifier,//修饰符
    alignment: Alignment = Alignment.Center,//图片对齐方式
    contentScale: ContentScale = ContentScale.Fit,//图片的拉伸方式
    alpha: Float = DefaultAlpha,//图片透明度
    colorFilter: ColorFilter? = null//通过ColorFilter对颜色矩阵进行变换
) {
    
}

参数还是比较简单的,ContentScale的几种方式可以通过官网认识:ContentScale介绍(点击跳转),其中ColorFilter和传统UI自定义控件时,使用的高级渲染效果相同,ColorFilter分别拥有三个伴生方法,对应不同的渲染方式:

使用tint例子,使用SrcIn模式合成一个红色:

@Composable
@Preview
fun MyImage() {
    Image(
        painter = painterResource(id = R.drawable.ic_launcher_foreground),
        contentDescription = "my image",
        colorFilter = ColorFilter.tint(
            color = Color.Red,
            blendMode = BlendMode.SrcIn
        )
    )
}

预览效果:

使用colorMatrix例子,颜色增强:

@Composable
@Preview
fun MyImage() {
    Row {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "my image1",
            colorFilter = ColorFilter.colorMatrix(
                ColorMatrix().apply {
                    setToScale(1.2f, 1.2f, 1.2f, 1f)//颜色增强
                }
            )
        )

        Spacer(modifier = Modifier.width(10.dp))

        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "my image2",
        )
    }
}

预览效果,左边为颜色增强后:

使用lighting例子,添加红色向量:

@Composable
@Preview
fun MyImage() {
    Row {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "my image1",
            // 红色向量添加255,红色加绿色 = 黄色
            colorFilter = ColorFilter.lighting(
                Color(red = 0xff, green = 0xff, blue = 0xff),
                Color(red = 0xff, green = 0, blue = 0)
            )
        )

        Spacer(modifier = Modifier.width(10.dp))

        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "my image2",
        )
    }
}

预览效果,左边为添加红色向量后:

2.3 Icon

同样用于显示图标,Icon功能比Image少,只支持tint,并且该tint为一个Color对象,不支持模式,只支持染色:

@Composable
@Preview
fun MyImage() {
    Icon(
        painter = painterResource(id = R.drawable.ic_launcher_foreground),
        contentDescription = "icon",
        tint = Color.Blue // 将图标染成蓝色
    )
}

预览效果:

3.TextField

TextField就是输入框,并且需要用到state,关于state后续会详细介绍

3.1 基本使用

TextField必须传入的两个参数,一个是value,一个是onValueChange ,结合之前的重组概念来理解,每次重组都会重新调用可组合函数,所以输入框内容value必须是一个全局对象,在compose中,可以使用remember函数来使得一个变量成为全局变量,从而不受重组时代码调用导致重新初始化操作的影响
此外,只有state的改变才能通知compose进行重组,所以value又必须是一个state对象,并在onValueChange中对state进行改变,才能够进行组件的刷新

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyTextField() {
    var text by remember { mutableStateOf("") }// 定义state对象:text ,并设为全局
    TextField(
        value = text,//text 与TextField进行绑定
        onValueChange = { text = it },//当输入框值发生变换时,改变text值,从而引起状态的刷新,进而重组
        label = { Text("hint") }//提示
    )
}

效果:

3.2 TextFieldValue

value的参数类型除了支持String外,还支持TextFieldValueTextFieldValue具有更好的自定义性,如使用AnnotatedString使文本具有样式、TextRange指定光标位置:

@Immutable
class TextFieldValue constructor(
    val annotatedString: AnnotatedString,//带样式的字符串
    selection: TextRange = TextRange.Zero,//
    composition: TextRange? = null
) {
...
}

例子:

@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Preview
@Composable
fun TextFieldValuePreview(
) {
    val textFieldValueState = remember {
        mutableStateOf(
            TextFieldValue(
                annotatedString = buildAnnotatedString {
                    append("hi")

                    withStyle(
                        style = SpanStyle(
                            color = Color.Red,
                            //设置阴影
                            shadow = Shadow(
                                color = Color.Blue,//阴影颜色
                                blurRadius = 3f,//虚化
                            )
                        )
                    ) {
                        append("你好\n")
                    }
                },
                selection = TextRange(2)// 光标默认显示在第二个字符位置
            )
        )
    }

    val showKeyboard = remember { mutableStateOf(true) }
    val focusRequester = remember { FocusRequester() }
    val keyboard = LocalSoftwareKeyboardController.current

    // 显示键盘
    LaunchedEffect(focusRequester) {
        if (showKeyboard.value) {
            focusRequester.requestFocus()
            delay(100)
            keyboard?.show()
        }
    }

    TextField(
        modifier = Modifier.focusRequester(focusRequester),
        value = textFieldValueState.value,
        onValueChange = {
        }
    )
}

效果:

3.3 其他参数
@ExperimentalMaterial3Api
@Composable
fun TextField(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,// 是否可用
    readOnly: Boolean = false,// 是否只读
    textStyle: TextStyle = LocalTextStyle.current,// 和Text一样支持的TextStyle
    label: @Composable (() -> Unit)? = null,//提示,有内容时自动缩小并上移
    placeholder: @Composable (() -> Unit)? = null,//提示,有内容时自动消失
    leadingIcon: @Composable (() -> Unit)? = null,//文本前的图标
    trailingIcon: @Composable (() -> Unit)? = null,//文本尾的图标
    supportingText: @Composable (() -> Unit)? = null,//文本下方的文本
    isError: Boolean = false,//是否错误,错误会将label、下划线、下方文本、文本尾的图标的图标染红
    visualTransformation: VisualTransformation = VisualTransformation.None,//输入内容的视觉类型,如密码显示*
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,//键盘类型和imeAction
    keyboardActions: KeyboardActions = KeyboardActions.Default,//imeAction触发时的回调
    singleLine: Boolean = false,//是否单行
    maxLines: Int = Int.MAX_VALUE,//最大行数
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },//传入状态,从而监听用户触摸操作,如点击、拖拽
    shape: Shape = TextFieldDefaults.filledShape,//设置背景形状
    colors: TextFieldColors = TextFieldDefaults.textFieldColors()// 颜色集,通过设置相应的颜色,可以改变如错误发生时的颜色
) {
...
}

例子:

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyTextField() {
    var text by remember { mutableStateOf("") }
    TextField(
        value = text,
        onValueChange = { text = it },
        placeholder = { Text("haha") },
        leadingIcon = {//设置文本前图片
            Icon(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "leadingIcon"
            )
        },
        trailingIcon = {//设置文本后图片
            Icon(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "leadingIcon"
            )
        },
        supportingText = {//设置文本下的文本
            Text("supportingText")
        },
        isError = true,// 设置发生错误
        visualTransformation = PasswordVisualTransformation(),//视觉为密码
        shape = RoundedCornerShape(10.dp),//背景为圆角
        colors = TextFieldDefaults.textFieldColors(//错误时,下划线显示黄色
            errorIndicatorColor = Color.Yellow
        )
    )
}

效果:

3.4 OutlinedTextField

OutlinedTextField是含有一个边框的输入框,其他用法和TextField相同

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyTextField() {
    var text by remember { mutableStateOf("") }

    OutlinedTextField(
        modifier = Modifier.padding(start = 10.dp, top = 10.dp),
        value = text,
        onValueChange = { text = it },
        keyboardOptions = KeyboardOptions(
            keyboardType = KeyboardType.Number,
            imeAction = ImeAction.Search
        ),
        keyboardActions = KeyboardActions { 
            
        }
    )
}

效果:

4. Button

Button需要传入一个点击事件onClicklambda表达式,和一个content内容组件的lambda表达式,border边框支持Shader(点击跳转详情),其他参数说明如下:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,// 是否可用
    shape: Shape = ButtonDefaults.shape,// 背景形状
    colors: ButtonColors = ButtonDefaults.buttonColors(),//颜色集,背景、内容的可用和非可用颜色
    elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),//阴影,默认、按下、不可用等状态下的阴影
    border: BorderStroke? = null,//边框,支持Shader
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,// 内容组件的padding
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },//触摸事件的状态改变
    content: @Composable RowScope.() -> Unit//按钮的内容组件
) {

}
4.1 基本使用

Buttoncontent是一个RowScope的作用域,也就是以行来摆放组件

例子:

@Preview
@Composable
fun MyButton() {
    Button(
        onClick = { /*TODO*/ },
        colors = ButtonDefaults.buttonColors(
            containerColor = Color.Cyan,
            contentColor = Color.Red
        ),
        elevation = ButtonDefaults.buttonElevation(defaultElevation = 3.dp),
        border = BorderStroke(
            1.dp,
            Brush.linearGradient(
                0f to Color.Transparent,
                1f to Color.DarkGray
            )
        ),
        contentPadding = PaddingValues(
            start = 10.dp,
            top = 5.dp
        ),
        content = {
            Text("点我")
            Text("点我")
        }
    )
}

预览效果:

4.2 IconButton

IconButtoncontent需要传入一个Icon组件,其他用法和Button相同:

@Composable
fun MyIconButton() {
    IconButton(
        onClick = { /*TODO*/ },
        colors = IconButtonDefaults.iconButtonColors(contentColor = Color.Green),
        content = {
            Icon(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "icon"
            )
        }
    )
}

预览效果:

4.3 IconToggleButton

IconToggleButton具有选中和未选中状态,checked入参需要配合state对象使用,onCheckedChange用于选中状态切换的处理,其他用法和Button相同:

@Preview
@Composable
fun MyIconToggleButton() {
    var checked by remember { mutableStateOf(false) }

    IconToggleButton(
        checked = checked,
        onCheckedChange = {
            checked = it
        },
        modifier = Modifier
            .width(100.dp)
            .height(100.dp),
        colors = IconButtonDefaults.iconToggleButtonColors(
            contentColor = Color.Green,//选中为绿色
            checkedContentColor = Color.Red//非选中为红色
        ),
        content = {
            Icon(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "icon"
            )
        }
    )
}

效果:

4.4 Switch

Switch为开关样式的IconToggleButton组件,thumbContent参数支持指定开关按钮的Icon,其他用法与IconToggleButton相同:

@Preview
@Composable
fun MySwitch() {
    var checked by remember { mutableStateOf(false) }

    Switch(
        checked = checked,
        onCheckedChange = { checked = it },
        thumbContent = {
            Icon(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "icon"
            )
        }
    )
}

效果:

4.5 RadioButton

RadioButton为单选框

@Preview
@Composable
fun MyRadioButton() {
    var selected by remember { mutableStateOf(false) }

    RadioButton(
        selected = selected,
        onClick = { selected = !selected }
    )
}

效果:

4.6 Checkbox

Checkbox为复选框

@Preview
@Composable
fun MyCheckbox() {
    var selected by remember { mutableStateOf(false) }

    Checkbox(
        checked = selected,
        onCheckedChange = { selected = it }
    )
}

效果:

4.7 ExtendedFloatingActionButton

ExtendedFloatingActionButton为悬浮按钮,控制expanded参数可以展开和缩小,此外还支持shape设置背景形状、elevation设置阴影:

@Composable
fun ExtendedFloatingActionButton(
    text: @Composable () -> Unit,// 文字
    icon: @Composable () -> Unit,// 图标
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    expanded: Boolean = true,// 是否展开
    shape: Shape = FloatingActionButtonDefaults.extendedFabShape,//背景形状
    containerColor: Color = FloatingActionButtonDefaults.containerColor,//容器颜色
    contentColor: Color = contentColorFor(containerColor),//内容组件颜色
    elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),//阴影
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
...
}

例子:

@Preview
@Composable
fun MyExtendedFloatingActionButton() {
    var expanded by remember { mutableStateOf(false) }
    
    ExtendedFloatingActionButton(
        text = { Text(text = "点我") },
        icon = { /*TODO*/ },
        onClick = { expanded = !expanded },
        expanded = expanded,
        shape = RoundedCornerShape(30.dp)
    )
}

效果:

5.Spacer

Spacer表示间距,用来代表一片隔离区域,隔离组件与组件

@Preview
@Composable
fun MySpacer() {
    Row {
        Text("hi")
        Spacer(modifier = Modifier.width(20.dp))
        Text("hi")
    }
}

预览效果:

6.Divider

Divider可以用来表示一条分割线,默认是一条横向的,所以通过Modifier来改变

@Preview
@Composable
fun MyDivider() {
    Row() {
        Text(
            "hi",
            modifier = Modifier.weight(1f)
        )
        Divider(
            color = Color.Blue,
            modifier = Modifier
                .fillMaxHeight()//充满整个组件
                .width(1.dp)//宽度为1dp
        )
        Text(
            "hi",
            modifier = Modifier.weight(1f)
        )
    }
}

预览效果:

6.1 IntrinsicSize

从上面的预览效果可以知道,将Divider设置为最大高度后,MyDivider组件充满了整个屏幕,如果想到达到Divider的高度不计入MyDivider的高度,并随着MyDivider的高度进行填充,就需要用到IntrinsicSize
IntrinsicSize表示允许父组件优先查询下子组件的高度,所以设置给父组件,这边给Row设置Modifier

@Preview
@Composable
fun MyDivider2() {
    Row(modifier = Modifier.height(IntrinsicSize.Min)) {//高度设置为IntrinsicSize
        Text(
            "hi",
            modifier = Modifier.weight(1f)
        )
        Divider(
            color = Color.Red,
            modifier = Modifier
                .fillMaxHeight()//充满整个组件
                .width(1.dp)//宽度为1dp
        )
        Text(
            "hi",
            modifier = Modifier.weight(1f)
        )
    }
}

预览效果:

五、标准布局

compose中的布局也不多,最基础的为ColumnRowBox,官方给出的定义如下图:

1.Row

上面我们使用过一个Row,它的作用域是RowScope,同横向LinearLayout

@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,// 内容组件水平排列方式
    verticalAlignment: Alignment.Vertical = Alignment.Top,//内容组件垂直对齐方式
    content: @Composable RowScope.() -> Unit// 内容组件
) {
    
}
1.1 Arrangement

关于Arrangement的几种方式,官方给出的图示:

1.2 基本使用
@Preview
@Composable
fun MyRow() {
    Row(
        modifier = Modifier.width(100.dp),
        horizontalArrangement = Arrangement.End,//内容组件往右对齐
        verticalAlignment = Alignment.CenterVertically//内容组件垂直居中
    ) {
        Text("hi")
        Text("你好\n 张三")
    }
}

预览效果:

2.Column

Column就是竖直方向摆放组件的布局,用法上和Row相同,同竖向LinearLayout

@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,//内容组件垂直对齐方式
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,//内容组件水平对齐方式
    content: @Composable ColumnScope.() -> Unit//内容组件
) {
    
}
2.1 基本使用
@Preview
@Composable
fun MyColumn() {
    Column(
        modifier = Modifier.height(100.dp),
        horizontalAlignment = Alignment.CenterHorizontally,//内容组件水平居中
        verticalArrangement = Arrangement.SpaceBetween//内容组件垂直分布到两侧
    ) {
        Text("hi")
        Text("你好\n 张三")
    }
}

预览效果:

3.Box

Box类似FrameLayout,可以堆叠摆放子组件

@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,//内容组件的对齐方式
    propagateMinConstraints: Boolean = false,//是否指定内容组件使用该组件的最小约束(最小宽高)
    content: @Composable BoxScope.() -> Unit
) {

}
3.1 基本使用

下面两个Image的宽高设定为40dp,由于Box设置了最小约束为50dp和70dp,所以Image变大了:

@Preview
@Composable
fun MyBox() {
    Box(
        modifier = Modifier
            .sizeIn(50.dp, 70.dp),//设置内容组件的最小宽度和高度为50dp、70dp,配合propagateMinConstraint=true使用
        propagateMinConstraints = true,//使内容组件最小宽度和高度生效
        contentAlignment = Alignment.BottomEnd
    ) {
        // propagateMinConstraints,内部需要一个组件撑大整体的大小
        Box(Modifier.size(50.dp,150.dp).background(Color.Cyan))
        Image(
            modifier = Modifier.size(40.dp, 40.dp),
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = null,
            contentScale = ContentScale.FillHeight // 图片高度拉伸
        )
        Image(
            modifier = Modifier.size(40.dp, 40.dp),
            painter = painterResource(id = R.drawable.ic_launcher_foreground),
            contentDescription = null,
            contentScale = ContentScale.FillHeight // 图片高度拉伸
        )
    }
}

预览效果:

4.Scaffold

Scaffold预设了很多槽位(存放子组件)和功能,Scaffold的学习可以通过官网:Scaffold官方示例(有些参数只有MD2版本才有)

4.1 topBar

槽位topBar就是给顶部子组件准备的,如:TopAppBar

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {//标题
                    Text(
                        modifier = Modifier.padding(start = 10.dp),
                        text = "topBar"
                    )
                },
                navigationIcon = {//导航图标
                    Icon(Icons.Default.ArrowBack, contentDescription = null)
                },
                actions = {//右侧按钮
                    IconButton(onClick = { /*TODO*/ }) {
                        Icon(Icons.Filled.Favorite, contentDescription = null)
                    }
                    IconButton(onClick = { /*TODO*/ }) {
                        Icon(Icons.Filled.Search, contentDescription = null)
                    }
                },
                colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
            )
        }
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

预览效果:

4.2 bottomBar

topBar对应,bottomBar是用来存放底部子组件的槽位,如:BottomAppBarBottomNavigation

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold() {
    Scaffold(
        topBar = {
           ...
        },
        bottomBar = {
            BottomAppBar(
                containerColor = MaterialTheme.colorScheme.primaryContainer,
                tonalElevation = 2.dp,
            ) {
                IconButton(onClick = { /*TODO*/ }) {
                    Icon(Icons.Filled.Home, contentDescription = null)
                }
                Spacer(modifier = Modifier.weight(1f))
                IconButton(onClick = { /*TODO*/ }) {
                    Icon(Icons.Filled.ShoppingCart, contentDescription = null)
                }
                IconButton(onClick = { /*TODO*/ }) {
                    Icon(Icons.Filled.Info, contentDescription = null)
                }
            }
        }
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

效果:

4.3 floatingActionButton

floatingActionButton是专门为FloatingActionButton准备的槽位,配合floatingActionButtonPosition可以改变槽位的位置,目前只支持底部居中和底部靠右:

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold() {
    Scaffold(
        topBar = {
            ...
        },
        bottomBar = {
            ...
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { /*TODO*/ }) {
                Text(text = "hi")
            }
        },
        floatingActionButtonPosition = FabPosition.Center
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

效果:

4.4 snackbarHost

snackbarHost槽位用于展示一个提示SnackbarHost,需要通过SnackbarHostState来控制该子组件的显示:

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold2() {
    val scaffoldState by remember { mutableStateOf(SnackbarHostState()) }
    val scope = rememberCoroutineScope()

    Scaffold(
        topBar = {
            ...
        },
        bottomBar = {
            ...
        },
        floatingActionButton = {
            FloatingActionButton(onClick = {
                scope.launch {
                    scaffoldState.showSnackbar("hi,this is snack bar")
                }
            }) {
                Text(text = "hi")
            }
        },
        snackbarHost = { SnackbarHost(hostState = scaffoldState) },
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

效果:

SnackbarHostState还支持显示的时长,相应的点击动作,基于协程返回消失或点击动作的结果:

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold2() {
    val scaffoldState by remember { mutableStateOf(SnackbarHostState()) }
    val scope = rememberCoroutineScope()

    Scaffold(
        topBar = {
            ...
        },
        bottomBar = {
            ...
        },
        floatingActionButton = {
            FloatingActionButton(onClick = {
                scope.launch {
                    val result = scaffoldState.showSnackbar(
                        message = "hi,this is snack bar",
                        duration = SnackbarDuration.Short,
                        actionLabel = "click"
                    )

                    when (result) {
                        SnackbarResult.ActionPerformed -> {
                            /* Handle snackbar action performed */
                            scaffoldState.currentSnackbarData?.dismiss()
                        }
                        SnackbarResult.Dismissed -> {
                            /* Handle snackbar dismissed */
                        }
                    }
                }
            }) {
                Text(text = "hi")
            }
        },
        snackbarHost = { SnackbarHost(hostState = scaffoldState) },
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

效果:

4.5 MD2-drawerContent

drawerContent是抽屉菜单的槽位,它是一个ColumnScope,注意目前MD3版本并不支持,如果要使用,需要MD2Scaffold

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffoldDrawer() {
    val drawerState = rememberScaffoldState()
    val scope = rememberCoroutineScope()

    androidx.compose.material.Scaffold(
        topBar = {
            TopAppBar(
                title = {//标题
                    Text(
                        modifier = Modifier.padding(start = 10.dp),
                        text = "topBar"
                    )
                },
                navigationIcon = {//导航图标
                    Icon(
                        modifier = Modifier.clickable {
                            scope.launch {
                                drawerState.drawerState.apply {
                                    if (isClosed) open() else close()
                                }
                            }
                        },
                        imageVector = Icons.Default.List,
                        contentDescription = null
                    )
                },
                colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
            )
        },
        drawerContent = {
            Text("title")

            Row(modifier = Modifier.padding(start = 10.dp,top = 10.dp)) {
                Image(Icons.Default.Phone, contentDescription = null)
                Text(text = "my phone")
            }
            Row(modifier = Modifier.padding(start = 10.dp,top = 10.dp)) {
                Image(Icons.Default.Call, contentDescription = null)
                Text(text = "call")
            }
            Row(modifier = Modifier.padding(start = 10.dp,top = 10.dp)) {
                Image(Icons.Default.Delete, contentDescription = null)
                Text(text = "delete cache")
            }
        },
        scaffoldState = drawerState
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

效果:

5. MD2-ModalDrawer

ModalDrawer仅仅是抽屉栏,同样只在MD2中才有,需要DrawerState控制展开和收起:

@Preview
@Composable
fun MyModalDrawer() {
    val drawerState =
        androidx.compose.material.rememberDrawerState(androidx.compose.material.DrawerValue.Closed)
    val scope = rememberCoroutineScope()

    ModalDrawer(
        drawerState = drawerState,
        drawerContent = {
            // Drawer content
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Text("hi")
            }
        }
    ) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            // Screen content
            Button(onClick = {
                scope.launch {
                    drawerState.apply {
                        if (isClosed) open() else close()
                    }
                }
            }) {
                Text("点我展开抽屉")
            }
        }
    }
}

效果:

此外BottomDrawer代表底部的抽屉栏,用法上和ModalDrawer差不多

6.MD2-ModalBottomSheetLayout

ModalBottomSheetLayout是底部菜单,需要使用ModalBottomSheetState控制显示和消失:

@OptIn(ExperimentalMaterialApi::class)
@Preview
@Composable
fun MyModalBottomSheetLayout() {
    val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
    val scope = rememberCoroutineScope()

    ModalBottomSheetLayout(
        sheetState = sheetState,
        sheetContent = {
            // Sheet content
            Box(
                modifier = Modifier.height(400.dp),
                contentAlignment = Alignment.Center
            ) {
                Text("Sheet")
            }
        }
    ) {
        // Screen content
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Button(onClick = {
                scope.launch {
                    sheetState.apply {
                        if (isVisible) hide() else show()
                    }
                }
            }) {
                Text("点我展开")
            }
        }
    }
}

效果:

此外,BottomSheetScaffold代表带有底部sheetContent槽位的Scaffold,用法和Scaffold差不多

7.MD2-BackdropScaffold

BackdropScaffold官方的说法为背景幕,就是两个布局可以堆叠,并前面的布局可以下移隐藏,通过BackdropScaffoldState控制是否隐藏:

@OptIn(ExperimentalMaterialApi::class)
@Preview
@Composable
fun MyBackdropScaffold() {
    val scaffoldState = rememberBackdropScaffoldState(
        BackdropValue.Concealed
    )
    val scope = rememberCoroutineScope()

    BackdropScaffold(
        scaffoldState = scaffoldState,
        appBar = {
            // Top app bar
            androidx.compose.material.TopAppBar(
                title = {//标题
                    Text(
                        modifier = Modifier.padding(start = 10.dp),
                        text = "topBar"
                    )
                },
                navigationIcon = {//导航图标
                    Icon(
                        modifier = Modifier.clickable {
                            scope.launch {
                                scaffoldState.apply {
                                    if (isConcealed) reveal() else conceal()
                                }
                            }
                        },
                        imageVector = Icons.Default.List,
                        contentDescription = null
                    )
                }
            )
        },
        backLayerContent = {
            // Back layer content
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.White)
            ) {

            }
        },
        frontLayerContent = {
            // Front layer content
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Magenta)
            ) {

            }
        }
    )
}

效果:

8.MD3-ModalNavigationDrawer

ModalNavigationDrawer是MD3中的抽屉栏,配合ModalDrawerSheet组件,可以达到抽屉栏列表MD3风格的样式:

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyPermanentNavigationDrawer() {
    val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
    val scope = rememberCoroutineScope()

    ModalNavigationDrawer(
        drawerState = drawerState,
        modifier = Modifier.fillMaxHeight(),
        drawerContent = {
            ModalDrawerSheet() {
                NavigationRailItem(
                    selected = true,
                    onClick = {},
                    icon = { Icon(Icons.Default.Home, contentDescription = null) },
                    label = { Text("home") }
                )
                NavigationRailItem(
                    selected = false,
                    onClick = {},
                    icon = { Icon(Icons.Default.Info, contentDescription = null) },
                    label = { Text("info") }
                )
                NavigationRailItem(
                    selected = false,
                    onClick = {},
                    icon = { Icon(Icons.Default.Call, contentDescription = null) },
                    label = { Text("call") }
                )
            }
        },
    ) {
        Scaffold(
            topBar = {
                CenterAlignedTopAppBar(
                    title = { Text("CenterAlignedTopAppBar") },
                    navigationIcon = {
                        Icon(
                            modifier = Modifier.clickable {
                                scope.launch {
                                    drawerState.apply {
                                        if (isClosed) open() else close()
                                    }
                                }
                            },
                            imageVector = Icons.Default.List,
                            contentDescription = null
                        )
                    },
                    actions = { /* App bar actions */ })
            },
        ) { paddings ->
            Box(modifier = Modifier.padding(paddings))
        }
    }
}

效果:

六、总结

最后总结下这篇文章的各个组件的作用,当然了compose中还有其他的组件,以及后续会出更多的新组件,目前也介绍了大部分组件的使用:

组件 分类 备注
Text 文本
TextField 文本输入
OutlinedTextField 文本输入 带边框
Image 图片
Icon 图标 渲染方式比Image少
Button 按钮
IconButton 图标按钮
IconToggleButton 选中状态图标按钮 通过State切换是否选中
Switch 开关样式图标按钮 通过State切换是否选中
RadioButton 单选按钮 通过State切换是否选中
Checkbox 复选按钮 通过State切换是否选中
FloatingActionButton 悬浮按钮
ExtendedFloatingActionButton 可展开悬浮按钮 通过State切换是否展开
SnackbarHost 提示消息 通过SnackbarHostState是否显示
Spacer 间距
Divider 分割线
Row 横向布局
Column 纵向布局
Box 堆叠布局
Scaffold 槽位布局 通过ScaffoldState切换是否展开抽屉栏
TopAppBar 标题导航栏
CenterAlignedTopAppBar 标题居中导航栏
BottomAppBar 底部导航栏
BottomNavigation 底部导航栏
ModalDrawer 抽屉栏 通过DrawerState切换是否展开抽屉栏
ModalBottomSheetLayout 底部抽屉菜单栏 通过ModalBottomSheetState切换是否显示菜单栏
BackdropScaffold 背景幕 通过BackdropScaffoldState切换是否前幕布下移
ModalNavigationDrawer 抽屉栏 通过DrawerState切换是否展开抽屉栏
ModalDrawerSheet 抽屉栏菜单布局
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容