Jetpack Compose–Android UI 开发的未来方向
本文最后更新于 121 天前,其中的信息可能已经有所发展或是发生改变。

碎碎念

Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助我们简化并加快 Android 界面开发。

Jetpack Compose 是 Google 推动的 Android UI 开发的未来方向,所以从 Android Studio 的较新版本(如 Hedgehog、Iguana、Jellyfish)开始,默认新建项目(Empty Activity 等)的模板就是 Jetpack Compose,而不再是传统的 View-Based 模板。

Ethan 在学习 Android 开发的过程中所接触的知名的 Android 开发书籍、博客、课程等仍然在讲解传统的 View-Based 形式,实践起来会有诸多疑惑,所以通过这篇文章入门 Jetpack 库对和 Ethan 有相同困扰的人是必要的。


1. 传统的 View-Based 形式

1.1 你的第一个 Android 项目

任何一个编程语言写出的第一个程序毫无疑问都是Hello World,这是自20世纪70年代流传下来的传统,在编程界已成为永恒的经典,那在 Android 中也不会例外。如果在早期的 Android Studio 版本中,通过 Empty Activity 创建新项目,运行后会在屏幕中直接显示 Hello World,而并不需要其它额外操作。

HelloWorld程序.jpg

1.2 MainActivity 的 Kotlin 代码

Activity 是 Android 应用程序的门面,凡是在应用中你看得到的东西,都是放在 Activity 中的。由于我们只是创建了一个新项目,并没有添加任何的代码,因此你在中看到的界面,其实就是 MainActivity 。打开 MainActivity. kt,代码如下所示:

class MainActivity : AppCompatActivity() { 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main) 
    } 
}

可以看到 MainActivity 中有一个 onCreate() 方法,这个方法是一个Activity 被创建时必定要执行的方法,其中只有两行代码,并且没有“Hello World! ”的字样。那么显示的“Hello World! ”是在哪里定义的呢?

1.3 MainActivity 的布局文件

其实 Android 程序的设计讲究逻辑和视图分离,因此是不推荐在 Activity 中直接编写界面的。一种更加通用的做法是,在布局文件中编写界面,然后在 Activity 中引入进来。这就是所谓的 View-Based 形式。

我们在代码中可以看到,在 onCreate() 方法的第二行调用了 setContentView() 方法,就是这个方法给当前的 Activity 引入了一个 activity_main 布局,那 “Hello World!” 一定就是在这里定义的了。

早期的 Android Studio 中,布局文件都是定义在 res/layout 目录下的。而在新版的 Android Studio 中,我们如果想使用布局文件定义,我们需要手动创建 res/layout 文件夹并添加布局文件。

当你展开 layout 目录,你会看到 activity_main.xml 这个文件。打开该文件并切换到 Text 视图,代码如下所示:

<androidx. Constraintlayout. Widget. ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:app="http://schemas.android.com/apk/res-auto" 
    xmlns:tools="http://schemas.android.com/tools" 
    Android:layout_width="match_parent" 
    Android:layout_height="match_parent" 
    Tools:context=". MainActivity"> 
    <TextView 
        Android:layout_width="wrap_content" 
        Android:layout_height="wrap_content" 
        Android:text="Hello World!" 
        App:layout_constraintBottom_toBottomOf="parent" 
        App:layout_constraintLeft_toLeftOf="parent" 
        App:layout_constraintRight_toRightOf="parent" 
        App:layout_constraintTop_toTopOf="parent" /> 

</androidx.constraintlayout.widget.ConstraintLayout> 

看到上面代码中有一个 TextView,这是 Android 系统提供的一个控件,用于在布局中显示文字。然后你终于在 TextView 中看到了“Hello World!”的字样!


2. Jetpack Compose 工具包

Jetpack Compose 包提供声明性的函数构建一个简单的界面组件。我们无需修改任何 XML 布局,也不需要使用布局编辑器。相反,我们只需调用可组合函数来定义所需的元素,Compose 编译器即会完成后面的所有工作。

在最新版本的 Android Studio 中选择 New Project 来创建一个应用,默认模板已包含一些 Compose 元素,运行后会自动显示 “Hello Android”字样。

HelloAndroid.jpg

默认模板已包含一些 Compose 元素以显示”Hello Android”。但在此教程中,我们需要逐步地构建它。


3. 可组合函数

Jetpack Compose 是围绕可组合函数构建的。这些函数可让我们以程序化方式定义应用的界面,只需描述应用界面的外观并提供数据依赖项,而不必关注界面的构建过程(初始化元素、将其附加到父项等)。如需创建可组合函数,只需将 @Composable 注解添加到函数名称中即可。

3.1 添加文本元素

首先,通过在 onCreate 方法内添加文本元素,让系统显示“Hello world!”文本。可以通过定义内容块并调用 Text 可组合函数来实现此目的。setContent 块定义了 activity 的布局,我们会在其中调用可组合函数。可组合函数只能从其他可组合函数调用。

Jetpack Compose 使用 Kotlin 编译器插件将这些可组合函数转换为应用的界面元素。例如,由 Compose 界面库定义的 Text 可组合函数会在屏幕上显示一个文本标签。

// .. 其它依赖库
import androidx.compose.material3.Text

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello world!")
        }
    }
}

注意:在传统的 View-Based 形式中,我们采用 setContentView() 绑定布局的 xml 文件,而在 Jetpack Compose 中,我们使用 setContent 块来定义 Activity 的布局。

3.2 定义可组合函数

如需使函数成为可组合函数,我们需要添加 @Composable 注解。我们定义一个 MessageCard 函数并向其传递一个名称,然后该函数就会使用该名称配置文本元素。

// ...
import androidx.compose.runtime.Composable

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MessageCard("Android")
        }
    }
}

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

3.3 在 Android Studio 中预览函数

借助 @Preview 注解,我们可以在 Android Studio 中预览可组合函数,而无需构建应用并将其安装到 Android 设备或模拟器中。该注解必须用于不接受参数的可组合函数

注意:我们只能预览无参函数

因此,我们无法直接预览 MessageCard 函数,而是需要创建另一个名为 PreviewMessageCard 的函数,由该函数使用适当的参数调用 MessageCard。请在 @Composable 上方添加 @Preview 注解。

// ...
import androidx.compose.ui.tooling.preview.Preview

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

@Preview
@Composable
fun PreviewMessageCard() {
    MessageCard("Android")
}

重新构建项目。由于新的 PreviewMessageCard 函数未在任何位置受到调用,因此应用本身不会更改,但 Android Studio 会添加一个预览窗口,我们可以点击拆分(spilt)视图以展开此窗口:

点击spilt以预览.jpg

任何时候,如需更新预览,我们可以点击预览窗口顶部的刷新按钮:

刷新按钮.jpg

4. 布局

界面元素采用多层次结构,元素中又包含其他元素。在 Compose 中,我们可以通过从可组合函数中调用其他可组合函数来构建界面层次结构。

4.1 添加多个文本

到目前为止,我们已经构建了第一个可组合函数和预览。为探索更多 Jetpack Compose 功能,我们将构建一个简单的消息界面,界面上显示可以展开且具有动画效果的消息列表。

首先,通过显示消息发送者和消息内容,使消息可组合项更丰富。我们需要先将可组合参数更改为接受 Message 对象(而非 String),然后在 MessageCard 可组合项中添加另一个 Text 可组合项。请务必同时更新预览。

// ...
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MessageCard(Message("Android", "Jetpack Compose"))
        }
    }
}

data class Message(val author: String, val body: String)

@Composable
fun MessageCard(msg: Message) {
    Text(text = msg.author)
    Text(text = msg.body)
}

@Preview
@Composable
fun PreviewMessageCard() {
    MessageCard(
        msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!")
    )
}

预览效果如下:

添加多个文本.jpg

这段代码会在内容视图中创建两个文本元素。不过,由于我们的代码中未提供有关如何排列这两个文本元素的信息,因此它们会相互重叠,使文本无法阅读。

4.2 使用 Column 垂直排列元素

Column 函数可让我们垂直排列元素。向 MessageCard 函数中添加一个 Column
当然我们还可以使用 Row 水平排列各项,并使用 Box 堆叠元素。

// ...
import androidx.compose.foundation.layout.Column

@Composable
fun MessageCard(msg: Message) {
    Column {
        Text(text = msg.author)
        Text(text = msg.body)
    }
}

预览效果如下:

Column垂直排列元素.jpg

4.3 添加图片元素

下面我们来添加消息发送者的个人资料照片,以丰富消息卡片。使用 Resource Manager 从照片库中导入图片,或使用 Coli 图片加载库加载在线图片(本文不涉及)。

ResManager位置.jpg

Android Studio 使用 Resource Manager 将图像资源导入到项目中,我们可以执行以下任意操作:

  • 将图像直接拖动到 Resource Manager (资源管理器) 窗口中
  • 或者:
    • 单击加号图标 (+)
    • 选择 Import Drawables
    • 选择要导入的文件和文件夹

添加一个 Row 可组合项,以实现良好的设计结构,并向该可组合项中添加一个 Image 可组合项。

// ...
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.res.painterResource

@Composable
fun MessageCard(msg: Message) {
    Row {
        Image(
            painter = painterResource(R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
        )

       Column {
            Text(text = msg.author)
            Text(text = msg.body)
        }
    }

}

预览效果如下:

Image预览.jpg

4.4 配置布局

现在我们的消息布局拥有良好的结构,但其元素的间距不合理,并且图片过大!为了装饰或配置可组合项,Compose 使用了修饰符。通过修饰符,我们可以更改可组合项的大小、布局、外观,还可以添加高级互动,例如使元素可点击。我们可以将这些修饰符链接起来,以创建更丰富的可组合项。

// ...
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp

@Composable
fun MessageCard(msg: Message) {
    // 使用 Row 横排头像和消息内容,并设置组件内边距 8dp
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            // 加载本地资源
            painter = painterResource(R.drawable.profile_picture),
            // 无障碍功能描述
            contentDescription = "Contact profile picture",
            modifier = Modifier
                // 设置图片大小 40*40 dp
                .size(40.dp)
                // 将图片裁剪成圆形
                .clip(CircleShape)
        )

        // 在头像和消息内容之间添加水平间距
        Spacer(modifier = Modifier.width(8.dp))

        Column {
            Text(text = msg.author)
            // 作者和消息正文之间添加垂直间距
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = msg.body)
        }
    }
}

Modifier 是 Jetpack Compose 中用于装饰(修饰)UI组件的一个工具类,你可以用它来:

  • 设置尺寸(size)
  • 添加内边距或外边距(padding)
  • 设置背景、边框、阴影(background、border、shadow)
  • 控制布局行为(weight、align、offset)
  • 添加点击事件(clickable)
  • 控制组件的显示/隐藏/透明度等

预览效果如下:

预览效果.jpg

5. Material Design

Jetpack Compose 支持 Material Design 原则。它的许多界面元素都原生支持 Material Design。在上一节中,我们的消息设计现在已有布局,但看上去还不是特别理想。

Material Design 是围绕 ColorTypographyShape 这三大要素构建的。在这一节中我们将逐一添加这些要素。

5.1 使用 Material Design

首先,使用在我们的项目中创建的 Material 主题 ComposeTutorialTheme 和 Surface 来封装 MessageCard 函数。在 @Preview 和 setContent 函数中都需要执行此操作。这样一来,可组合项即可沿用应用主题中定义的样式,从而在整个应用中确保一致性。

Empty Compose Activity 模板会为项目生成默认主题,使我们能够自定义 MaterialTheme,可以在 ui.theme 子软件包的 Theme.kt 文件中找到我们的自定义主题:

Theme主题位置.jpg
// ...
import androidx.compose.material3.Surface

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTutorialTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    MessageCard(Message("Android", "Jetpack Compose"))
                }
            }
        }
    }
}

@Preview
@Composable
fun PreviewMessageCard() {
    ComposeTutorialTheme {
        Surface {
            MessageCard(
                msg = Message("Lexi", "Take a look at Jetpack Compose, it's great!")
            )
        }
    }
}

5.2 Color

通过 MaterialTheme.colorScheme,使用已封装主题中的颜色来设置样式。我们可以在需要颜色的任何位置使用主题中的这些值。本文使用动态主题颜色(由设备偏好设置定义),可以在 MaterialTheme.kt 文件中将 dynamicColor 设置为 false,以更改此设置。

设置标题样式,并为图片添加边框:

// ...
import androidx.compose.foundation.border
import androidx.compose.material3.MaterialTheme

@Composable
fun MessageCard(msg: Message) {
   Row(modifier = Modifier.padding(all = 8.dp)) {
       Image(
           painter = painterResource(R.drawable.profile_picture),
           contentDescription = null,
           modifier = Modifier
               .size(40.dp)
               .clip(CircleShape)
               .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
       )

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

       Column {
           Text(
               text = msg.author,
               color = MaterialTheme.colorScheme.secondary
           )

           Spacer(modifier = Modifier.height(4.dp))
           Text(text = msg.body)
       }
   }
}

预览效果如下:

添加边框和颜色的预览.jpg

5.3 Typography(排版)

MaterialTheme 中提供了 Material Typography 样式,只需将其添加到 Text 可组合项中即可。

// ...

@Composable
fun MessageCard(msg: Message) {
   Row(modifier = Modifier.padding(all = 8.dp)) {
       Image(
           painter = painterResource(R.drawable.profile_picture),
           contentDescription = null,
           modifier = Modifier
               .size(40.dp)
               .clip(CircleShape)
               .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
       )
       Spacer(modifier = Modifier.width(8.dp))

       Column {
           Text(
               text = msg.author,
               color = MaterialTheme.colorScheme.secondary,
               style = MaterialTheme.typography.titleSmall
           )

           Spacer(modifier = Modifier.height(4.dp))

           Text(
               text = msg.body,
               style = MaterialTheme.typography.bodyMedium
           )
       }
   }
}

5.4 Shape(形状)

通过 Shape,我们可以添加最后的“点睛之笔”。首先,将消息正文封装在 Surface 可组合项中。这样即可自定义消息正文的形状和高度。此外,还要为消息添加内边距,以改进布局。

// ...
import androidx.compose.material3.Surface

@Composable
fun MessageCard(msg: Message) {
   Row(modifier = Modifier.padding(all = 8.dp)) {
       Image(
           painter = painterResource(R.drawable.profile_picture),
           contentDescription = null,
           modifier = Modifier
               .size(40.dp)
               .clip(CircleShape)
               .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
       )
       Spacer(modifier = Modifier.width(8.dp))

       Column {
           Text(
               text = msg.author,
               color = MaterialTheme.colorScheme.secondary,
               style = MaterialTheme.typography.titleSmall
           )

           Spacer(modifier = Modifier.height(4.dp))

           Surface(shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp) {
               Text(
                   text = msg.body,
                   modifier = Modifier.padding(all = 4.dp),
                   style = MaterialTheme.typography.bodyMedium
               )
            }
       }
   }
}

这段代码用一个具有圆角和阴影效果的卡片(Surface)来包裹一段消息文本(Text),形成一个简洁好看的气泡样式消息块,比如聊天界面里的一条消息。

  • Surface 是一个 带有背景、圆角、阴影的容器,Compose 推荐用它包裹 UI 元素以自动应用 Material 风格。
  • shape = MaterialTheme.shapes.medium:表示这个容器使用 中等圆角,也就是系统主题中预设的圆角值(比如卡片样式的圆角)。
  • shadowElevation = 1.dp:设置 阴影高度为 1dp,让这个块有轻微的立体效果,看起来像浮起来一点。

5.5 启用深色主题

我们可以启用深色主题(或夜间模式),以避免显示屏过亮(尤其是在夜间),或者只是节省设备电量。由于支持 Material Design,Jetpack Compose 默认能够处理深色主题。使用 Material Design 颜色、文本和背景时,系统会自动适应深色背景。

我们可以在文件中以单独函数的形式创建多个预览,也可以向同一个函数中添加多个注解。

添加新的预览注解并启用夜间模式。

// ...
import android.content.res.Configuration

@Preview(name = "Light Mode")
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true,
    name = "Dark Mode"
)
@Composable
fun PreviewMessageCard() {
   ComposeTutorialTheme {
    Surface {
      MessageCard(
        msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!")
      )
    }
   }
}

6. 列表和动画

6.1 创建消息列表

只包含一条消息的聊天略显孤单,因此我们将更改对话,使其包含多条消息。您需要创建一个可显示多条消息的 Conversation 函数。对于此用例,请使用 Compose 的 LazyColumn 和 LazyRow。这些可组合项只会呈现屏幕上显示的元素,因此,对于较长的列表,使用它们会非常高效。

在此代码段中,我们可以看到 LazyColumn 包含一个 items 子项。它接受 List 作为参数,并且其 lambda 会收到我们命名为 message 的参数(可以随意为其命名),该参数是 Message 的实例。简而言之,系统会针对提供的 List 的每个项调用此 lambda。将示例数据集复制到项目中,以便快速引导对话。

// ...
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items

@Composable
fun Conversation(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageCard(message)
        }
    }
}

@Preview
@Composable
fun PreviewConversation() {
    ComposeTutorialTheme {
        Conversation(SampleData.conversationSample)
    }
}

预览效果如下:

多条消息预览效果.jpg

6.2 在展开消息时显示动画效果

对话变得更加有趣了。是时候添加动画效果了!您将添加展开消息以显示更多内容的功能,同时为内容大小和背景颜色添加动画效果。为了存储此本地界面状态,您需要跟踪消息是否已展开。为了跟踪这种状态变化,您必须使用 remember 和 mutableStateOf 函数。

可组合函数可以使用 remember 将本地状态存储在内存中,并跟踪传递给 mutableStateOf 的值的变化。该值更新时,系统会自动重新绘制使用此状态的可组合项(及其子项)。这称为重组

通过使用 Compose 的状态 API(如 remember 和 mutableStateOf),系统会在状态发生任何变化时自动更新界面。

// ...
import androidx.compose.foundation.clickable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           ComposeTutorialTheme {
               Conversation(SampleData.conversationSample)
           }
       }
   }
}

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(R.drawable.profile_picture),
            contentDescription = null,
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
        )
        Spacer(modifier = Modifier.width(8.dp))

        // 定义一个状态变量,记录消息是否展开,初始为未展开(false)
        var isExpanded by remember { mutableStateOf(false) }

        // 用 Column 垂直排列作者名字和消息内容
        // 并给整个列添加点击事件,点击时切换消息展开状态
        Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
            Text(
                text = msg.author,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleSmall
            )

            Spacer(modifier = Modifier.height(4.dp))

            Surface(
                shape = MaterialTheme.shapes.medium,
                shadowElevation = 1.dp,
            ) {
                Text(
                    text = msg.body,
                    modifier = Modifier.padding(all = 4.dp),
                    // 根据是否展开控制显示行数,展开时显示全文,否则只显示一行
                    maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

我们来为背景颜色添加动画效果,使其值逐步从 MaterialTheme.colorScheme.surface 更改为 MaterialTheme.colorScheme.primary(反之亦然),而不只是切换 Surface 的背景颜色。为此,您将使用 animateColorAsState 函数。最后,您将使用 animateContentSize 修饰符顺畅地为消息容器大小添加动画效果:

// ...
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(R.drawable.profile_picture),
            contentDescription = null,
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)
        )
        Spacer(modifier = Modifier.width(8.dp))

        // 定义状态变量,记录消息是否展开,初始为未展开(false)
        var isExpanded by remember { mutableStateOf(false) }

        // 根据 isExpanded 状态,使用动画平滑地改变 Surface 背景色
        // 展开时颜色为主题主色 primary,收起时颜色为主题表面色 surface
        val surfaceColor by animateColorAsState(
            if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface,
        )

        // 使用 Column 垂直排列作者和消息内容,整个区域添加点击事件切换展开状态
        Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
            Text(
                text = msg.author,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleSmall
            )

            Spacer(modifier = Modifier.height(4.dp))
            // 使用 Surface 作为消息内容的容器,支持背景色、形状和阴影
            Surface(
                shape = MaterialTheme.shapes.medium,
                shadowElevation = 1.dp,

                color = surfaceColor,
                // 添加动画让 Surface 尺寸变化平滑(展开/收起时高度变化)
                modifier = Modifier.animateContentSize().padding(1.dp)
            ) {
                Text(
                    text = msg.body,
                    modifier = Modifier.padding(all = 4.dp),
                    maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇