Jetpack Compose 实战之仿微信UI -实现聊天界面(五)

前言

在之前的开发中,已经实现了登陆页、首页、朋友圈等部分内容。在一篇文章中,我将使用 Jetpack Compose 实现聊天界面,介绍 Jetpack 库的 Room 组件的使用以及Room 结合 Paging 分页组件实现本地数据分页查询。

界面拆分

我将界面主要分为三部分,顶部标题聊天记录列表底部输入框部分,如下图:

聊天记录列表

聊天记录的内容主要包括当前用户的头像,好友的头像,对话内容和对话时间;当前用户的头像从个人信息获取,好友的头像从首页的对话列表获取,所以这里实体类不用管头像的事,只需要实现其他字段即可,在这里我将使用 Room 组件实现聊天记录的保存。

Jetpack Room 介绍

Jetpack Room 是Google官方在SQLite基础上封装的一款数据持久库,是Jetpack全家桶的一员,和Jetpack其他库有着可以高度搭配协调的天然优势。它抽象了SQLite,通过提供方便的API来查询数据库,并在编译时验证。并且可以使用SQLite的全部功能,同时拥有Java SQL查询生成器提供的类型安全。

引入 Room 相关依赖

kotlin 复制代码
val room_version = "2.5.0"

implementation "androidx.room:room-ktx:$room_version"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
testImplementation("androidx.room:room-testing:$room_version")
implementation("androidx.room:room-paging:$room_version")

Entity类 - 聊天记录实体类 ChatSmsEntity 创建

less 复制代码
@Entity(tableName = "chatsms")
data class ChatSmsEntity (
    /**
     * 主键,自增id
     */
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id: Long = 0,
    /**
     * 用户id
     */
    @ColumnInfo(name = "userId")
    var userId: Long = 0,
    /**
     * 消息内容
     */
    @ColumnInfo(name = "message")
    var message: String,
    /**
     * 消息内容类型,与枚举类MediaType对应
     */
    @ColumnInfo(name = "mediaType")
    var mediaType: Int = 1,
    /**
     * 消息类型,与枚举类MessageType对应
     */
    @ColumnInfo(name = "messageType")
    var messageType: Int = 0,
    /**
     * 创建时间
     */
    @ColumnInfo(name = "createBy")
    var createBy: Long = 0,
)

这里的实体类其实就是表的结构,表名为 chatsms ,主要使用 @Entity 注解和 @ColumnInfo 注解分别标识表名和字段名。

Dao接口 - ChatSmsDao创建

less 复制代码
@Dao
interface ChatSmsDao {
    /**
     * 新增聊天记录
     */
    @Insert
    suspend fun insert(entity: ChatSmsEntity): Long

    /**
     * 删除指定聊天记录
     */
    @Query("DELETE FROM chatsms WHERE id=:id")
    suspend fun delete(id: Long)

    /**
     * 删除指定用户聊天记录
     */
    @Query("DELETE FROM chatsms WHERE userId =:userId")
    suspend fun deleteAllByUserId(userId: Long)

    /**
     * 删除所有聊天记录
     */
    @Query("DELETE FROM chatsms")
    suspend fun deleteAll()

    /**
     * 分页查询聊天记录
     */
    @Query("SELECT * FROM chatsms WHERE userId =:userId ORDER BY id DESC")
    fun getAllByPagingSource(userId: Long): PagingSource<Int, ChatSmsEntity>
}

这里主要使用注解 @Dao 标识接口,然后写CRUD的逻辑,除了代码中的 @Insert@Query ,还有 @Delete@Update 等常用注解,这里接口我主要使用了 新增聊天记录分页查询聊天记录 ,在分页查询聊天记录时我使用了 PagingSource<Int, ChatSmsEntity> ,PagingSource是分页组件 Paging 的内容,在前面的文章中我有介绍。

枚举类 MediaType

scss 复制代码
enum class MediaType(var value: Int) {
    TEXT(1),
    VIDEO(2),
    IMAGE(3),
    AUDIO(4);
}

枚举类 MessageType

scss 复制代码
enum class MessageType(val value: Int) {
    /**
     * 接收的信息
     */
    RECEIVE(0),

    /**
     * 发送的信息
     */
    SEND(1)
}

RoomDatabase数据库类 - COMChatDataBase创建

kotlin 复制代码
@Database(
    entities = [ChatSmsEntity::class],
    version = 1,
    exportSchema = false
)
abstract class COMChatDataBase: RoomDatabase() {

    abstract fun chatSmsDao(): ChatSmsDao

    companion object {
        val instance = Room.databaseBuilder(ComposeWechatApp.instance, COMChatDataBase::class.java, "compose_chat_db").build()
    }
}

这里主要是用注解 @Database 标识关联的表(entities ),数据库版本号(version)等。

到这里,聊天记录保存的逻辑已经实现,我们测试新增一些数据,然后使用Android Studio的工具 App Inspection 查看,结果如下:

页面的实现

使用脚手架 Scaffold 来实现页面布局,其中 topBar 实现顶部标题,content 实现对话列表,bottomBar 实现底部输入框。

顶部标题的实现

ini 复制代码
topBar = {
    CenterAlignedTopAppBar(
        title = {
            Text(
                session.name,
                maxLines = 1,
                fontSize = 16.sp,
                overflow = TextOverflow.Ellipsis,
                color = Color(0xff000000)
            )
        },
        actions = {
            IconButton(
                onClick = {
                    /* doSomething() */
                }) {
                Icon(
                    imageVector = Icons.Filled.MoreHoriz,
                    contentDescription = null,
                    modifier = Modifier.size(20.dp),
                    tint = Color(0xff000000)
                )
            }
        },
        colors = TopAppBarDefaults.mediumTopAppBarColors(
            containerColor = Color(ContextCompat.getColor(context, R.color.nav_bg)),
            scrolledContainerColor = Color(ContextCompat.getColor(context, R.color.nav_bg)),
            navigationIconContentColor = Color.White,
            titleContentColor = Color(ContextCompat.getColor(context, R.color.black_10)),
            actionIconContentColor = Color(ContextCompat.getColor(context, R.color.black_10)),
        ),
        navigationIcon = {
            IconButton(
                onClick = {
                    /* doSomething() */
                }) {
                Icon(
                    imageVector = Icons.Filled.ArrowBackIos,
                    contentDescription = null,
                    modifier = Modifier
                        .size(20.dp)
                        .clickable {
                            context.finish()
                        },
                    tint = Color(0xff000000)
                )
            }
        }
    )
},

这里包含返回图标,中间的好友名称以及右边的navigationIcon操作图标。

对话列表的实现

这里使用 LazyColumn 实现列表的展示,通过ViewModel来处理交互的逻辑,其中:

ChatViewModel的实现

ini 复制代码
class ChatViewModel(userId: Long) : ViewModel() {

    private val _userId: Long = userId

    val chatSmsItems: Flow<PagingData<ChatSmsEntity>> =
        Pager(PagingConfig(pageSize = 10, prefetchDistance = 1)) {
            /** 查询表里的聊天记录 */
            COMChatDataBase.instance.chatSmsDao().getAllByPagingSource(_userId)
        }.flow
        .flowOn(Dispatchers.IO)
        .cachedIn(viewModelScope)

    /**
     * 模拟对方发来的随机对话
     */
    private val _mockReceiveSms = listOf(
        "你好啊,最近过得怎么样?",
        "嗨,我挺好的,谢谢关心。最近工作有点忙,但也在努力调整自己的状态。",
        "是啊,工作总是让人有些疲惫。你有什么放松的方法吗?",
        "你对未来有什么规划吗?",
        "其实我在考虑出国留学,但还在考虑中。你呢?有什么打算?",
        "我打算创业,自己开一家咖啡店。希望能在未来几年内实现这个梦想",
        "你觉得什么样的人最吸引你?",
        "是啊,和有趣的人在一起总是让人感到快乐。我也很喜欢和有趣的人交朋友",
        "我觉得不诚实、虚伪、不尊重他人的人最不吸引我。我觉得人与人之间的尊重和理解很重要",
        "您能告诉我您的时间安排吗?"
    )

    /**
     * 发送信息(保存到本地数据库)
     */
    fun sendMessage (message: String, messageType: MessageType) {
        val entity: ChatSmsEntity
        if (messageType == MessageType.SEND) {
            entity = ChatSmsEntity(
                userId = _userId,
                mediaType = MediaType.TEXT.value,
                message = message,
                messageType = messageType.value,
                createBy = currentTimeMillis()
            )
        } else {
            val random = Random()
            /** 生成0-9之间的随机数*/
            val index = random.nextInt(10)
            entity = ChatSmsEntity(
                userId = _userId,
                mediaType = MediaType.TEXT.value,
                message = _mockReceiveSms[index],
                messageType = MessageType.RECEIVE.value,
                createBy = currentTimeMillis()
            )
        }
        viewModelScope.launch(Dispatchers.IO) {
            val id = COMChatDataBase.instance.chatSmsDao().insert(entity)
            println("id============$id")
        }
    }
}

这里的逻辑主要使用 Kotlin Flow 来处理分页查询的聊天记录数据以及保存聊天记录到本地数据库,因为没有接 IM 功能,所以只是使用模拟数据来模拟对方发送的信息。

列表的页面实现

ini 复制代码
content = { innerPadding ->
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(ContextCompat.getColor(context, R.color.nav_bg))),
        contentAlignment = Alignment.BottomEnd
    ) {
        LazyColumn(
            state = scrollState,
            contentPadding = innerPadding,
            modifier = Modifier.padding(start = 15.dp, end = 15.dp,bottom = 60.dp),
            reverseLayout = true,
            verticalArrangement = Arrangement.Top,
        ) {
            items(lazyChatItems) {
                it?.let {
                    MessageItemView(it, session)
                }
            }
            lazyChatItems.apply {
                when (loadState.append) {
                    is LoadState.Loading -> {
                        item { Loading() }
                    } else -> {}
                }
            }
        }
    }
}

这里需要说明的是列表是从底部向上 的,所以设置了容器Box的属性 contentAlignment = Alignment.BottomEnd(位于底部) ,LazyColumn的属性 reverseLayout = true(倒序)

底部输入框部分的实现

ini 复制代码
bottomBar = {
    NavigationBar(
        modifier = Modifier.height(60.dp),
        containerColor = Color(ContextCompat.getColor(context, R.color.nav_bg)),
    ) {
        Column(modifier = Modifier.fillMaxSize()) {
            CQDivider()
            Row(modifier = Modifier
                .fillMaxSize()) {
                Box(
                    modifier = Modifier
                        .fillMaxHeight()
                        .weight(1f),
                    contentAlignment = Alignment.Center
                ) {
                    Icon(
                        imageVector = Icons.Filled.PauseCircleOutline,
                        contentDescription = null,
                        modifier = Modifier.size(30.dp),
                        tint = Color(0xff000000)
                    )
                }
                Box(
                    modifier = Modifier
                        .padding(6.dp)
                        .fillMaxHeight()
                        .weight(6f)
                ) {
                    BasicTextField(
                        value = inputText,
                        onValueChange = {
                            inputText = it
                        },
                        textStyle = TextStyle(
                            fontSize = 16.sp
                        ),
                        modifier = Modifier
                            .defaultMinSize(minHeight = 45.dp, minWidth = 280.dp)
                            .focusRequester(focusRequester),
                        decorationBox = { innerTextField->
                            Box(
                                modifier = Modifier
                                    .fillMaxSize()
                                    .clip(RoundedCornerShape(4.dp))
                                    .background(Color.White)
                                    .padding(start = 6.dp),
                                contentAlignment = Alignment.CenterStart
                            ) {
                                innerTextField()
                            }
                        },
                        /** 光标颜色*/
                        cursorBrush = SolidColor(Color(0xff5ECC71))
                    )
                }
                if (inputText == "") {
                    Box(
                        modifier = Modifier
                            .fillMaxHeight()
                            .weight(1f),
                        contentAlignment = Alignment.Center
                    ) {
                        Icon(
                            imageVector = if (!sheetState.isVisible )Icons.Filled.TagFaces else Icons.Filled.BlurCircular,
                            contentDescription = null,
                            modifier = Modifier
                                .size(30.dp)
                                .click {
                                    focusRequester.requestFocus()
                                    scope.launch {
                                        if (!sheetState.isVisible) {
                                            keyboardController?.hide()
                                            sheetState.show()
                                        } else {
                                            sheetState.hide()
                                            keyboardController?.show()
                                        }
                                    }
                                },
                            tint = Color(0xff000000)
                        )
                    }
                    Box(
                        modifier = Modifier
                            .fillMaxHeight()
                            .weight(1f),
                        contentAlignment = Alignment.Center
                    ) {
                        Icon(
                            imageVector = Icons.Filled.AddCircleOutline,
                            contentDescription = null,
                            modifier = Modifier.size(30.dp),
                            tint = Color(0xff000000)
                        )
                    }
                } else {
                    Box(modifier = Modifier
                        .padding(
                            start = 10.dp,
                            top = 12.dp,
                            bottom = 12.dp,
                            end = 10.dp
                        )
                        .fillMaxHeight()
                        .weight(2f),
                        contentAlignment = Alignment.Center ) {
                        Text(
                            text = "发送",
                            fontSize = 15.sp,
                            color = Color.White,
                            modifier = Modifier
                                .fillMaxSize()
                                .clip(RoundedCornerShape(8.dp))
                                .background(Color(0xff5ECC71))
                                .padding(top = 4.dp)
                                .clickable {
                                    viewModel.sendMessage(inputText, MessageType.SEND)
                                    // 发送信息后滚动到最底部
                                    scope.launch {
                                        scrollState.scrollToItem(0)
                                        inputText = ""
                                        sheetState.hide()
                                        /** 本人发送一条信息后保存一条对面回复模拟信息*/
                                        delay(200)
                                        viewModel.sendMessage(
                                            "",
                                            MessageType.RECEIVE
                                        )
                                    }
                                },
                            textAlign = TextAlign.Center
                        )
                    }
                }
            }
        }
    }
}

这里主要是一个文本输入框和左右的操作图标,输入框使用了组件BasicTextField,没有使用TextField和OutlinedTextField的原因是发现他们的内边距比较大,目前处理不了。除了实现文本输入外,这里还实现了表情包的选择弹窗。

表情包弹窗面板的实现

我这里使用了 emoji2 ,通过底部弹窗组件 ModalBottomSheetLayout 装载emoji布局,emoji2依赖引入的方式为:

arduino 复制代码
implementation 'androidx.emoji2:emoji2-emojipicker:1.4.0-beta05'

EmojiPicker的实现

less 复制代码
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun EmojiPicker(
    modalBottomSheetState: ModalBottomSheetState,
    onPicked: (emoji: String) -> Unit
) {
    ModalBottomSheetLayout(
        sheetState = modalBottomSheetState,
        sheetShape = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp),
        sheetContent = {
            Column {
                /** Spacer: 解决报错 java.lang.IllegalArgumentException:
                 *  The initial value must have an associated anchor.
                 */
                Spacer(modifier = Modifier.height(1.dp))

                Box(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
                    AndroidView(
                        factory = { context ->
                            EmojiPickerView(context)
                        },
                        modifier = Modifier.fillMaxWidth()
                    ) { it ->
                        it.setOnEmojiPickedListener {
                            onPicked(it.emoji)
                        }
                    }
                }
            }
        }
    ){}
}

看下表情包面板的实现效果

页面ChatScreen的全部代码为:

ini 复制代码
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class,
    ExperimentalComposeUiApi::class
)
@Composable
fun ChatScreen(viewModel: ChatViewModel, session: ChatSession) {
    val context = LocalContext.current as Activity
    val scrollState = rememberLazyListState()
    var inputText by remember { mutableStateOf("") }
    /** 聊天消息 */
    val lazyChatItems = viewModel.chatSmsItems.collectAsLazyPagingItems()
    val scope = rememberCoroutineScope()
    val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
    val keyboardController = LocalSoftwareKeyboardController.current
    val focusRequester = remember { FocusRequester() }

    rememberSystemUiController().setStatusBarColor(Color(ContextCompat.getColor(context, R.color.nav_bg)), darkIcons = true)
    Surface(
        Modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background)
            .autoCloseKeyboard()
    ) {
        Scaffold(
            topBar = {
                CenterAlignedTopAppBar(
                    title = {
                        Text(
                            session.name,
                            maxLines = 1,
                            fontSize = 16.sp,
                            overflow = TextOverflow.Ellipsis,
                            color = Color(0xff000000)
                        )
                    },
                    actions = {
                        IconButton(
                            onClick = {
                                /* doSomething() */
                            }) {
                            Icon(
                                imageVector = Icons.Filled.MoreHoriz,
                                contentDescription = null,
                                modifier = Modifier.size(20.dp),
                                tint = Color(0xff000000)
                            )
                        }
                    },
                    colors = TopAppBarDefaults.mediumTopAppBarColors(
                        containerColor = Color(ContextCompat.getColor(context, R.color.nav_bg)),
                        scrolledContainerColor = Color(ContextCompat.getColor(context, R.color.nav_bg)),
                        navigationIconContentColor = Color.White,
                        titleContentColor = Color(ContextCompat.getColor(context, R.color.black_10)),
                        actionIconContentColor = Color(ContextCompat.getColor(context, R.color.black_10)),
                    ),
                    navigationIcon = {
                        IconButton(
                            onClick = {
                                /* doSomething() */
                            }) {
                            Icon(
                                imageVector = Icons.Filled.ArrowBackIos,
                                contentDescription = null,
                                modifier = Modifier
                                    .size(20.dp)
                                    .clickable {
                                        context.finish()
                                    },
                                tint = Color(0xff000000)
                            )
                        }
                    }
                )
            },
            bottomBar = {
                NavigationBar(
                    modifier = Modifier.height(60.dp),
                    containerColor = Color(ContextCompat.getColor(context, R.color.nav_bg)),
                ) {
                    Column(modifier = Modifier.fillMaxSize()) {
                        CQDivider()
                        Row(modifier = Modifier
                            .fillMaxSize()) {
                            Box(
                                modifier = Modifier
                                    .fillMaxHeight()
                                    .weight(1f),
                                contentAlignment = Alignment.Center
                            ) {
                                Icon(
                                    imageVector = Icons.Filled.PauseCircleOutline,
                                    contentDescription = null,
                                    modifier = Modifier.size(30.dp),
                                    tint = Color(0xff000000)
                                )
                            }
                            Box(
                                modifier = Modifier
                                    .padding(6.dp)
                                    .fillMaxHeight()
                                    .weight(6f)
                            ) {
                                BasicTextField(
                                    value = inputText,
                                    onValueChange = {
                                        inputText = it
                                    },
                                    textStyle = TextStyle(
                                        fontSize = 16.sp
                                    ),
                                    modifier = Modifier
                                        .defaultMinSize(minHeight = 45.dp, minWidth = 280.dp)
                                        .focusRequester(focusRequester),
                                    decorationBox = { innerTextField->
                                        Box(
                                            modifier = Modifier
                                                .fillMaxSize()
                                                .clip(RoundedCornerShape(4.dp))
                                                .background(Color.White)
                                                .padding(start = 6.dp),
                                            contentAlignment = Alignment.CenterStart
                                        ) {
                                            innerTextField()
                                        }
                                    },
                                    /** 光标颜色*/
                                    cursorBrush = SolidColor(Color(0xff5ECC71))
                                )
                            }
                            if (inputText == "") {
                                Box(
                                    modifier = Modifier
                                        .fillMaxHeight()
                                        .weight(1f),
                                    contentAlignment = Alignment.Center
                                ) {
                                    Icon(
                                        imageVector = if (!sheetState.isVisible )Icons.Filled.TagFaces else Icons.Filled.BlurCircular,
                                        contentDescription = null,
                                        modifier = Modifier
                                            .size(30.dp)
                                            .click {
                                                focusRequester.requestFocus()
                                                scope.launch {
                                                    if (!sheetState.isVisible) {
                                                        keyboardController?.hide()
                                                        sheetState.show()
                                                    } else {
                                                        sheetState.hide()
                                                        keyboardController?.show()
                                                    }
                                                }
                                            },
                                        tint = Color(0xff000000)
                                    )
                                }
                                Box(
                                    modifier = Modifier
                                        .fillMaxHeight()
                                        .weight(1f),
                                    contentAlignment = Alignment.Center
                                ) {
                                    Icon(
                                        imageVector = Icons.Filled.AddCircleOutline,
                                        contentDescription = null,
                                        modifier = Modifier.size(30.dp),
                                        tint = Color(0xff000000)
                                    )
                                }
                            } else {
                                Box(modifier = Modifier
                                    .padding(
                                        start = 10.dp,
                                        top = 12.dp,
                                        bottom = 12.dp,
                                        end = 10.dp
                                    )
                                    .fillMaxHeight()
                                    .weight(2f),
                                    contentAlignment = Alignment.Center ) {
                                    Text(
                                        text = "发送",
                                        fontSize = 15.sp,
                                        color = Color.White,
                                        modifier = Modifier
                                            .fillMaxSize()
                                            .clip(RoundedCornerShape(8.dp))
                                            .background(Color(0xff5ECC71))
                                            .padding(top = 4.dp)
                                            .clickable {
                                                viewModel.sendMessage(inputText, MessageType.SEND)
                                                // 发送信息后滚动到最底部
                                                scope.launch {
                                                    scrollState.scrollToItem(0)
                                                    inputText = ""
                                                    sheetState.hide()
                                                    /** 本人发送一条信息后保存一条对面回复模拟信息*/
                                                    delay(200)
                                                    viewModel.sendMessage(
                                                        "",
                                                        MessageType.RECEIVE
                                                    )
                                                }
                                            },
                                        textAlign = TextAlign.Center
                                    )
                                }
                            }
                        }
                    }
                }
            },
            content = { innerPadding ->
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(Color(ContextCompat.getColor(context, R.color.nav_bg))),
                    contentAlignment = Alignment.BottomEnd
                ) {
                    LazyColumn(
                        state = scrollState,
                        contentPadding = innerPadding,
                        modifier = Modifier.padding(start = 15.dp, end = 15.dp,bottom = 60.dp),
                        reverseLayout = true,
                        verticalArrangement = Arrangement.Top,
                    ) {
                        items(lazyChatItems) {
                            it?.let {
                                MessageItemView(it, session)
                            }
                        }
                        lazyChatItems.apply {
                            when (loadState.append) {
                                is LoadState.Loading -> {
                                    item { Loading() }
                                } else -> {}
                            }
                        }
                    }
                    EmojiPicker(
                        modalBottomSheetState = sheetState,
                        onPicked = { emoji ->
                            inputText += emoji
                        }
                    )
                }
            }
        )
    }
}

@Composable
fun MessageItemView(it: ChatSmsEntity, session: ChatSession) {
    Box(
        contentAlignment = if (it.messageType == MessageType.RECEIVE.value) Alignment.CenterStart else Alignment.CenterEnd,
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight()
            .padding(
                top = 30.dp,
                start = if (it.messageType == MessageType.RECEIVE.value) 0.dp else 40.dp,
                end = if (it.messageType == MessageType.SEND.value) 0.dp else 40.dp
            ),
    ) {
        Column(modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight()
        ) {
            /*** 对话时间*/
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = 20.dp),
                contentAlignment = Alignment.Center
            ) {
               Text(
                   text = toTalkTime(it.createBy),
                   fontSize = 12.sp,
                   color = Color(0xff888888)
               )
            }
            /*** 对话信息*/
            Row(modifier = Modifier
                .wrapContentWidth()
                .wrapContentHeight()
            ) {
                /*** 他人头像(左边)*/
                if(it.messageType == MessageType.RECEIVE.value) {
                    Box(
                        modifier = Modifier
                            .size(40.dp)
                            .clip(RoundedCornerShape(4.dp))
                            .background(Color.White)
                            .weight(1f)
                    ) {
                        Image(
                            painter = rememberCoilPainter(request = session.avatar),
                            contentDescription = null,
                            contentScale = ContentScale.Crop,
                            modifier = Modifier
                                .fillMaxSize()
                                .clip(RoundedCornerShape(4.dp))
                        )
                    }
                }
                Box(
                    modifier = Modifier
                        .weight(6f)
                        .wrapContentHeight(),
                    contentAlignment =
                    if (it.messageType == MessageType.RECEIVE.value) Alignment.TopStart
                    else Alignment.TopEnd
                ) {
                    /*** 尖角*/
                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(top = 10.dp),
                        contentAlignment = if (it.messageType == MessageType.RECEIVE.value) Alignment.TopStart else Alignment.TopEnd
                    ) {
                        Icon(
                            imageVector = Icons.Filled.PlayArrow,
                            contentDescription = null,
                            modifier = Modifier
                                .size(20.dp)
                                .rotate(if (it.messageType == MessageType.SEND.value) 0f else 180f),
                            tint = if (it.messageType == MessageType.RECEIVE.value) Color.White
                            else Color(0xffA9EA7A)
                        )
                    }
                    /*** 文本内容*/
                    Box(
                        modifier = Modifier
                            .padding(
                                start = if (it.messageType == MessageType.RECEIVE.value) 12.dp else 0.dp,
                                end = if (it.messageType == MessageType.RECEIVE.value) 0.dp else 12.dp,
                            )
                            .clip(RoundedCornerShape(4.dp))
                            .wrapContentWidth()
                            .wrapContentHeight(Alignment.CenterVertically)
                            .background(
                                if (it.messageType == MessageType.RECEIVE.value) Color.White else Color(
                                    0xffA9EA7A
                                )
                            ),
                    ) {
                        Text(
                            modifier = Modifier.padding(8.dp),
                            text = it.message,
                            fontSize = 16.sp
                        )
                    }
                }
                /*** 本人头像(右边)*/
                if(it.messageType == MessageType.SEND.value) {
                    Box(
                        modifier = Modifier
                            .size(40.dp)
                            .clip(RoundedCornerShape(4.dp))
                            .background(Color.White)
                            .weight(1f)
                    ) {
                        Image(
                            painter = rememberCoilPainter(request = myAvatar),
                            contentDescription = null,
                            contentScale = ContentScale.Crop,
                            modifier = Modifier
                                .fillMaxSize()
                                .clip(RoundedCornerShape(4.dp))
                        )
                    }
                }
            } 
        }
    }
}

到这里,聊天页面的基本功能就实现了,我们看下效果

总结

在这一篇文章中,主要介绍了数据库 SQLite 基础上封装的一款数据持久库Jetpack Room 组件的使用,以及Room 结合 Paging 组件实现本地数据分页查询的功能。此外,还介绍了表情包 Emoji2 组件的使用。

相关推荐
x02414 天前
Android Room(SQLite) too many SQL variables异常
sqlite·安卓·android jetpack·1024程序员节
alexhilton17 天前
深入理解观察者模式
android·kotlin·android jetpack
Wgllss17 天前
花式高阶:插件化之Dex文件的高阶用法,极少人知道的秘密
android·性能优化·android jetpack
上官阳阳20 天前
使用Compose创造有趣的动画:使用Compose共享元素
android·android jetpack
沐言人生24 天前
Android10 Framework—Init进程-15.属性变化控制Service
android·android studio·android jetpack
IAM四十二1 个月前
Android Jetpack Core
android·android studio·android jetpack
王能1 个月前
Kotlin真·全平台——Kotlin Compose Multiplatform Mobile(kotlin跨平台方案、KMP、KMM)
android·ios·kotlin·web·android jetpack·kmp·kmm
alexhilton1 个月前
让Activity更加优雅地跳转
android·kotlin·android jetpack
沐言人生1 个月前
Android10 Framework—Init进程-11.客户端操作属性
android·android studio·android jetpack
沐言人生1 个月前
Android10 Framework—Init进程-9.服务端属性值初始化
android·android studio·android jetpack