联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

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

作者:访客发布时间:2023-12-26分类:程序开发学习浏览:112


导读:前言在之前的开发中,已经实现了登陆页、首页、朋友圈等部分内容。在一篇文章中,我将使用JetpackCompose实现聊天界面,介绍Jetpack库的Room组件的使...

前言

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

界面拆分

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

image.png

聊天记录列表

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

Jetpack Room 介绍

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

引入 Room 相关依赖

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 创建

@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创建

@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

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

枚举类 MessageType

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

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

RoomDatabase数据库类 - COMChatDataBase创建

@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 查看,结果如下:

image.png

页面的实现

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

顶部标题的实现

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的实现

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 功能,所以只是使用模拟数据来模拟对方发送的信息。

列表的页面实现

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(倒序)

底部输入框部分的实现

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依赖引入的方式为:

implementation 'androidx.emoji2:emoji2-emojipicker:1.4.0-beta05'

EmojiPicker的实现

@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)
                        }
                    }
                }
            }
        }
    ){}
}

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

gt_20231226_18656_v_gif.gif

页面ChatScreen的全部代码为:

@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))
                        )
                    }
                }
            } 
        }
    }
}

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

gt_20231226_1743_v_gif.gif

总结

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


标签:实战界面Compose喷气式飞机仿微信用户界面


程序开发学习排行
最近发表
网站分类
标签列表