Jetpack Compose -> Jetpack Compose 初探

前言

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

什么是声明式UI

传统UI

传统 UI 方式来声明UI

ini 复制代码
<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 中直接设置,通过下面这种方式设置

ini 复制代码
android:text="@string/app_name"

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

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

ini 复制代码
TextView textView = findViewById(R.id.text);
textView.setText("xxxx");

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

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

声明式UI

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

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

例如我们写一个 Compose 的 Text

ini 复制代码
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 的声明

scss 复制代码
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;

ini 复制代码
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 系统的,

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

scss 复制代码
implementation("io.coil-kt:coil-compose:2.5.0")

然后还是使用 Image 函数

less 复制代码
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 关键字;

别忘了添加网络权限

ini 复制代码
<uses-permission android:name="android.permission.INTERNET"/>

运行效果如下:

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

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

图标 Icon 的声明

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

ini 复制代码
Column {
    Icon(    
        imageVector = Icons.Filled.Favorite, contentDescription = ""
    )
}

运行效果如下:

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

按钮 Button 的声明

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

先来看下不配合 Text 的效果

ini 复制代码
Button(onClick = {}) {    }

运行效果如下:

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

ini 复制代码
Button(onClick = {}) { 
    Text(text = "我是老A")
}

运行效果如下:

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

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

输入框 TextFiled 的声明

在 Compose 中,使用 TextFiled 代替了 EditText

ini 复制代码
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()

ini 复制代码
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() 表示纵向和横向;

ini 复制代码
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)
    )
}

运行效果如下:

ini 复制代码
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 来修改相对位置;

scss 复制代码
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 里面;

scss 复制代码
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

ini 复制代码
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;

ini 复制代码
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,

vbnet 复制代码
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

vbnet 复制代码
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() 函数就可以实现竖向滑动;

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

运行效果如下:

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

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

运行效果如下:

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

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

运行效果如下:

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

ini 复制代码
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,我们可以这么实现

scss 复制代码
Row(Modifier.padding(8.dp)) {
    
}

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

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

less 复制代码
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 设置一个形状,我们来看下效果

less 复制代码
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,可以切各种东西,例如可以切图片,我们看下面这个例子

less 复制代码
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

less 复制代码
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 来给任意的控件设置一个监听

less 复制代码
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 的初探,就先到这里吧~~

下一节预告

状态订阅和自动更新;

相关推荐
大白要努力!2 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟3 小时前
Android音频采集
android·音视频
小白也想学C4 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程4 小时前
初级数据结构——树
android·java·数据结构
闲暇部落6 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX8 小时前
Android 分区相关介绍
android
大白要努力!9 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee9 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood10 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-12 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记