Android使用Compose实现简单微信朋友圈

Android使用Compose实现简单微信朋友圈

1.前言:

微信朋友圈相信大家不陌生,之前也实现过,最近用Compose写了一个简单版微信朋友圈,也遇到不少问题,于是总结了一下,直接上代码.

2.MainScreen代码:

ini 复制代码
package com.example.composecircledemo.ui
​
import android.Manifest
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import coil.ImageLoader
import com.example.composecircledemo.PlayVideoActivity
import com.example.composecircledemo.ui.model.CircleBean
import com.example.composecircledemo.ui.model.CommentListBean
import com.example.composecircledemo.ui.model.LikeListBean
import com.example.composecircledemo.ui.model.WeatherEvent
import com.example.composecircledemo.utils.Constants
import com.example.composecircledemo.utils.ImageSaveUtil
import com.example.composecircledemo.utils.RxBus
import com.example.composecircledemo.utils.ToastUtil
import com.example.composecircledemo.utils.rememberStoragePermissionLauncher
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.launch
​
/**
 * @author: njb
 * @date:   2025/8/18 1:49
 * @desc:   描述
 */
@Composable
fun MainScreen() {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    val keyboardController = LocalSoftwareKeyboardController.current
    val lazyListState = rememberLazyListState()
    val compositeDisposable = remember { CompositeDisposable() }
​
    // 全局状态管理
    var circleData by remember { mutableStateOf<List<CircleBean.DataBean>>(emptyList()) }
    var isCommentVisible by remember { mutableStateOf(false) }
    var commentContent by remember { mutableStateOf("") }
    var currentReplyPos by remember { mutableStateOf(-1) }
    var currentToUserId by remember { mutableStateOf("") }
    var currentToUserName by remember { mutableStateOf("") }
    var isImagePreviewVisible by remember { mutableStateOf(false) }
    var previewImages by remember { mutableStateOf(emptyList<String>()) }
    var previewIndex by remember { mutableStateOf(0) }
    var showDeleteDialog by remember { mutableStateOf(false) }
    var deletePos by remember { mutableStateOf(-1) }
    var showActionDialog by remember { mutableStateOf(false) } // 操作弹框状态
    var currentActionPos by remember { mutableStateOf(-1) }    // 当前操作的动态位置
​
    // 存储权限 launcher
    val storagePermissionLauncher = rememberStoragePermissionLauncher(
        onGranted = {
            scope.launch {
                // 修复:避免预览列表为空时崩溃
                val targetImg = previewImages.getOrNull(previewIndex) ?: return@launch
                val success = ImageSaveUtil.saveImageToGallery(
                    context, targetImg, ImageLoader(context)
                )
                ToastUtil.show(context, if (success) "图片保存成功" else "图片保存失败")
            }
        },
        onDenied = { ToastUtil.show(context, "缺少存储权限,无法保存图片") }
    )
​
    // 1. 初始化数据
    LaunchedEffect(Unit) {
        circleData = loadMockCircleData()
    }
​
    // 2. 修复 RxBus 重复订阅问题(原代码重复订阅+提前取消,导致事件接收失败)
    LaunchedEffect(Unit) {
        val disposable = RxBus.toObservable<WeatherEvent>()
            .subscribe(
                { event ->
                    println("Weather: ${event.cityName} ${event.temperature}℃")
                },
                { error -> error.printStackTrace() }
            )
        compositeDisposable.add(disposable) // 仅添加,不提前取消
    }
​
    // 3. 布局结构:列表 + 操作弹框 + 评论栏 + 图片预览
    Box(modifier = Modifier.fillMaxSize()) {
        // 朋友圈列表
        LazyColumn(state = lazyListState, modifier = Modifier.fillMaxSize()) {
            item { CircleHeader() }
            itemsIndexed(circleData, key = { _, item -> item.id ?: "default_key" }) { index, item ->
                CircleItem(
                    data = item,
                    // 关键:确保"操作按钮"(如右上角三个点)绑定此回调
                    onActionClick = {
                        currentActionPos = index // 记录当前操作的动态位置
                        showActionDialog = true  // 打开弹框
                    },
                    onReplyClick = { comment ->
                        currentReplyPos = index
                        currentToUserId = comment.user_id ?: ""
                        currentToUserName = comment.user_name ?: ""
                        isCommentVisible = true
                        commentContent = ""
                        keyboardController?.show()
                        scope.launch {
                            lazyListState.scrollToItem(index + 1)
                        }
                    },
                    onDeleteClick = {
                        deletePos = index
                        showDeleteDialog = true
                    },
                    onVideoClick = { videoUrl ->
                        val intent = Intent(context, PlayVideoActivity::class.java)
                        intent.putExtra("url", videoUrl)
                        startActivity(context, intent, null)
                    },
                    onImageClick = { images, imgIndex ->
                        previewImages = images
                        previewIndex = imgIndex
                        isImagePreviewVisible = true
                    },
                    onCommentLongClick = { comment ->
                        ToastUtil.show(context, "已复制:${comment.content ?: ""}")
                    }
                )
            }
        }
​
        // 4. 新增:点赞/评论操作弹框(底部弹出,点击操作按钮显示)
        if (showActionDialog && currentActionPos != -1) {
            // 弹框背景(点击空白关闭)
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Black.copy(alpha = 0.3f))
                    .clickable { showActionDialog = false }
            )
            // 弹框内容(底部显示,圆角设计)
            Column(
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .fillMaxWidth()
                    .background(Color.White)
                    .padding(16.dp)
            ) {
                // 获取当前操作的动态数据(安全判断)
                val currentItem = circleData.getOrNull(currentActionPos) ?: run {
                    showActionDialog = false
                    return@Column
                }
​
                // 点赞按钮
                Text(
                    text = if (currentItem.is_like == 1) "取消点赞" else "点赞",
                    fontSize = 18.sp,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 6.dp)
                        .clickable {
                            // 执行点赞/取消点赞逻辑
                            val updatedLikeList =
                                currentItem.like_list?.toMutableList() ?: mutableListOf()
                            if (currentItem.is_like == 1) {
                                // 取消点赞:移除当前用户
                                updatedLikeList.removeAll { it.user_id == "current_user_id" }
                            } else {
                                // 点赞:添加当前用户
                                updatedLikeList.add(
                                    LikeListBean(
                                        user_id = "current_user_id",
                                        user_name = "当前用户",
                                        circle_id = currentItem.id ?: ""
                                    )
                                )
                            }
                            // 更新列表数据
                            circleData = circleData.toMutableList().apply {
                                this[currentActionPos] = currentItem.copy(
                                    is_like = if (currentItem.is_like == 1) 0 else 1,
                                    like_list = updatedLikeList
                                )
                            }
                            ToastUtil.show(
                                context,
                                if (currentItem.is_like == 1) "取消点赞成功" else "点赞成功"
                            )
                            showActionDialog = false // 关闭弹框
                        }
                )
​
                // 分割线
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(1.dp)
                        .background(Color.LightGray.copy(alpha = 0.5f))
                )
​
                // 评论按钮
                Text(
                    text = "评论",
                    fontSize = 18.sp,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 6.dp)
                        .clickable {
                            // 触发评论输入栏
                            currentReplyPos = currentActionPos
                            currentToUserId = ""
                            currentToUserName = ""
                            isCommentVisible = true
                            commentContent = ""
                            keyboardController?.show()
                            // 滚动到当前动态,避免遮挡
                            scope.launch {
                                lazyListState.scrollToItem(currentActionPos + 1)
                            }
                            showActionDialog = false // 关闭弹框
                        }
                )
​
                // 分割线
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(1.dp)
                        .background(Color.LightGray.copy(alpha = 0.5f))
                )
​
                // 取消按钮
                Text(
                    text = "取消",
                    fontSize = 18.sp,
                    color = Color.Red,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 12.dp)
                        .clickable {
                            showActionDialog = false
                        }
                )
            }
        }
​
        // 5. 底部评论输入栏
        if (isCommentVisible && currentReplyPos != -1) { // 修复:增加currentReplyPos判断,避免空指针
            CommentInputBar(
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .fillMaxWidth()
                    .background(Color.White)
                    .padding(8.dp),
                hint = if (currentToUserId.isBlank()) "说点什么..." else "回复 $currentToUserName...",
                content = commentContent,
                onContentChange = { commentContent = it },
                onSendClick = {
                    if (commentContent.isBlank()) {
                        ToastUtil.show(context, "请输入评论内容")
                        return@CommentInputBar
                    }
                    val newComment = CommentListBean(
                        id = System.currentTimeMillis().toString(),
                        user_id = "current_user_id",
                        user_name = "当前用户",
                        to_user_id = currentToUserId,
                        to_user_name = currentToUserName,
                        circle_id = circleData[currentReplyPos].id ?: "",
                        content = commentContent,
                        type = "0",
                        createon = System.currentTimeMillis().toString(),
                        replyUser = CommentListBean.CommentsUser(
                            userId = currentToUserId.takeIf { it.isNotEmpty() } ?: "0",
                            userName = currentToUserName.takeIf { it.isNotEmpty() } ?: ""
                        ),
                        commentsUser = CommentListBean.CommentsUser(
                            userId = "current_user_id",
                            userName = "当前用户"
                        ),
                        is_del = "0",
                        deleteon = ""
                    )
                    // 更新评论列表(修复:确保集合可变)
                    circleData = circleData.toMutableList().apply {
                        val targetItem = this[currentReplyPos]
                        val newComments =
                            targetItem.comments_list?.toMutableList() ?: mutableListOf()
                        newComments.add(newComment)
                        this[currentReplyPos] = targetItem.copy(comments_list = newComments)
                    }
                    // 重置状态
                    isCommentVisible = false
                    commentContent = ""
                    keyboardController?.hide()
                    ToastUtil.show(
                        context,
                        if (currentToUserId.isBlank()) "评论成功" else "回复成功"
                    )
                },
                onDismiss = {
                    isCommentVisible = false
                    keyboardController?.hide()
                }
            )
        }
​
        // 6. 图片预览弹窗
        if (isImagePreviewVisible && previewImages.isNotEmpty()) { // 修复:判断预览列表非空
            ImagePreview(
                images = previewImages,
                currentIndex = previewIndex,
                onDismiss = { isImagePreviewVisible = false },
                onLongClick = {
                    storagePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                }
            )
        }
​
        // 7. 删除动态对话框
        if (showDeleteDialog && deletePos != -1) {
            AlertDialog(
                title = { Text("提示") },
                text = { Text("你确定要删除这条动态吗?") },
                onDismissRequest = { showDeleteDialog = false },
                confirmButton = {
                    Button(onClick = {
                        circleData = circleData.toMutableList().apply { removeAt(deletePos) }
                        showDeleteDialog = false
                        deletePos = -1
                        ToastUtil.show(context, "删除成功")
                    }) {
                        Text("确定")
                    }
                },
                dismissButton = {
                    Button(onClick = { showDeleteDialog = false }) {
                        Text("取消")
                    }
                }
            )
        }
    }
​
    // 8. 清理资源(页面销毁时取消RxBus订阅)
    DisposableEffect(Unit) {
        onDispose {
            compositeDisposable.dispose()
        }
    }
}
​
// 模拟加载朋友圈数据(替代原AssetsUtil.getStates)
private fun loadMockCircleData(): List<CircleBean.DataBean> {
    // 统一空集合常量,避免重复创建且明确泛型
    val emptyStringList: MutableList<String> = mutableListOf<String>()
    val emptyCommentList: MutableList<CommentListBean> = mutableListOf<CommentListBean>()
    val emptyLikeList: MutableList<LikeListBean> = mutableListOf<LikeListBean>()
​
    return listOf(
        // 第1条:网页动态(修复:补充video参数、明确空集合泛型)
        CircleBean.DataBean(
            id = "190383",
            user_id = "12",
            type = Constants.TYPE_WEB, // "4"(网页动态)
            content = "热修复测试",
            files = emptyStringList,
            share_title = "茫茫的长白大山瀚的草原,浩始森林,大山脚下,原始森林环抱中散落着几十户人家的,一个小山村,茅草房,对面炕,烟筒立在屋后边。在村东头有一个独立的房子,那就是青年点,窗前有一道小溪流过。学子在这里吃饭,由这里出发每天随社员去地里干活。干的活要么上山伐,树,抬树,要么砍柳树毛子开荒种地。在山里,可听那吆呵声:"顺山倒了!"放树谨防回头棒!,树上的枯枝打到别的树上再蹦回来,这回头棒打人最厉害。",
            share_image = "http://imgsrc.baidu.com/forum/w=580/sign=852e30338435e5dd902ca5d746c7a7f5/bd3eb13533fa828b6cf24d82fc1f4134960a5afa.jpg",
            share_url = "",
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "3天前",
            user_name = "机器猫",
            head_img = "http://b167.photo.store.qq.com/psb?/V14EhGon2OmAUI/hQN450lNoDPF.dO82PVKEdFw0Qj5qyGeyN9fByKgWd0!/m/dJWKmWNZEwAAnull",
            comments_list = emptyCommentList,
            like_list = emptyLikeList,
            is_like = 0,
            video = "" // 补充非视频动态的video参数(空字符串)
        ),
​
        // 第2条:文本动态(修复:补充video参数、明确空集合泛型)
        CircleBean.DataBean(
            id = "190382", // 修正id(原190383与第1条重复)
            user_id = "12",
            type = Constants.TYPE_TEXT, // "1"(文本动态)
            content = "茫茫的长白大山瀚的草原,浩始森林,大山脚下,原始森林环抱中散落着几十户人家的,一个小山村,茅草房,对面炕,烟筒立在屋后边。在村东头有一个独立的房子,那就是青年点,窗前有一道小溪流过。学子在这里吃饭,由这里出发每天随社员去地里干活。干的活要么上山伐,树,抬树,要么砍柳树毛子开荒种地。在山里,可听那吆呵声:"顺山倒了!"放树谨防回头棒!,树上的枯枝打到别的树上再蹦回来,这回头棒打人最厉害。",
            files = emptyStringList,
            share_title = "事件分发的过程",
            share_image = "http://imgsrc.baidu.com/forum/w=580/sign=852e30338435e5dd902ca5d746c7a7f5/bd3eb13533fa828b6cf24d82fc1f4134960a5afa.jpg",
            share_url = "",
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "3天前",
            user_name = "机器猫",
            head_img = "http://b167.photo.store.qq.com/psb?/V14EhGon2OmAUI/hQN450lNoDPF.dO82PVKEdFw0Qj5qyGeyN9fByKgWd0!/m/dJWKmWNZEwAAnull",
            comments_list = emptyCommentList,
            like_list = emptyLikeList,
            is_like = 0,
            video = "" // 补充video参数
        ),
​
        // 第3条:图片动态(修复:移除强转、明确files泛型)
        CircleBean.DataBean(
            id = "190381",
            user_id = "3372840",
            type = Constants.TYPE_IMAGE, // "2"(图片动态)
            content = "",
            // 直接使用List<String>,无需强转MutableList
            files = listOf(
                "http://gips1.baidu.com/it/u=1410005327,4082018016&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280",
                "https://gips3.baidu.com/it/u=3732338995,3528391142&fm=3028&app=3028&f=JPEG&fmt=auto&q=100&size=f600_800",
                "http://pic.3h3.com/up/2014-3/20143314140858312456.gif",
                "http://gips2.baidu.com/it/u=1506371362,2755593126&fm=3028&app=3028&f=JPEG&fmt=auto?w=1440&h=2560",
                "http://gips1.baidu.com/it/u=2778784063,1731001818&fm=3028&app=3028&f=JPEG&fmt=auto?w=1920&h=2560",
                "http://gips2.baidu.com/it/u=1192674964,3939660937&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=960",
                "http://gips1.baidu.com/it/u=2178851102,884542160&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280",
                "http://gips2.baidu.com/it/u=1757244148,2197425658&fm=3028&app=3028&f=JPEG&fmt=auto?w=1440&h=2560",
                "https://gips1.baidu.com/it/u=926030969,4240391978&fm=3028&app=3028&f=JPEG&fmt=auto&q=100&size=f576_1024"
            ) as MutableList<String>,
            share_title = "",
            share_image = "",
            share_url = "",
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "4天前",
            user_name = "一笑的小馆子",
            head_img = "http://b167.photo.store.qq.com/psb?/V14EhGon2OmAUI/hQN450lNoDPF.dO82PVKEdFw0Qj5qyGeyN9fByKgWd0!/m/dJWKmWNZEwAAnull",
            comments_list = emptyCommentList,
            like_list = emptyLikeList,
            is_like = 0,
            video = "" // 补充video参数
        ),
​
        // 第4条:网页动态(修复:补充video参数、明确空集合泛型)
        CircleBean.DataBean(
            id = "190380",
            user_id = "3372793",
            type = Constants.TYPE_WEB, // "4"(网页动态)
            content = "",
            files = emptyStringList,
            share_title = "《王者荣耀》真的很好玩,快来一起玩吧!现实里我独自面对生活的风雨,但只要进入峡谷,就有队友为我挡箭。今天天气好好,阳光懒懒地洒在石板路上,满大街的情侣和独自一人的我,让我黯然神伤。但一想到还差1颗星上王者,瞬间满血复活!来不来?",
            share_image = "http://imgsrc.baidu.com/forum/w=580/sign=852e30338435e5dd902ca5d746c7a7f5/bd3eb13533fa828b6cf24d82fc1f4134960a5afa.jpg",
            share_url = "", // 补充原缺失的share_url参数(按构造函数顺序)
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "4天前",
            user_name = "天宫一号",
            head_img = "http://b167.photo.store.qq.com/psb?/V14EhGon2OmAUI/hQN450lNoDPF.dO82PVKEdFw0Qj5qyGeyN9fByKgWd0!/m/dJWKmWNZEwAAnull",
            comments_list = listOf(
                CommentListBean(
                    id = "15563",
                    circle_id = "190380",
                    user_id = "3191031",
                    to_user_id = "0",
                    type = "0",
                    content = "巴结",
                    is_del = "0",
                    deleteon = "0",
                    createon = "1577085821",
                    to_user_name = "",
                    user_name = "一笑的小管子",
                    // 被回复者信息:对应to_user_id和to_user_name
                    replyUser = CommentListBean.CommentsUser(
                        userId = "0",
                        userName = "",
                    ),
                    // 评论者信息:对应当前评论的user_id和user_name
                    commentsUser = CommentListBean.CommentsUser(
                        userId = "0",
                        userName = ""
                    )
                )
            ) as MutableList<CommentListBean>,
            like_list = listOf(
                LikeListBean(
                    user_id = "3191031",
                    circle_id = "190380",
                    user_name = "一笑的小管子"
                )
            ) as MutableList<LikeListBean>,
            is_like = 1,
            video = "" // 补充video参数
        ),
​
        // 第5条:视频动态(正常,保留原代码)
        CircleBean.DataBean(
            id = "190379",
            user_id = "3372793",
            type = Constants.TYPE_VIDEO, // "3"(视频动态)
            content = "哈哈",
            files = listOf(
                "http://img.my.csdn.net/uploads/201701/17/1484647897_1978.jpg"
            ) as MutableList<String>,
            share_title = "",
            share_image = "",
            share_url = "",
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "4天前",
            user_name = "笑嘻嘻",
            head_img = "http://b167.photo.store.qq.com/psb?/V14EhGon2OmAUI/hQN450lNoDPF.dO82PVKEdFw0Qj5qyGeyN9fByKgWd0!/m/dJWKmWNZEwAAnull",
            video = "https://vjs.zencdn.net/v/oceans.mp4", // 视频链接
            comments_list = emptyCommentList,
            like_list = emptyLikeList,
            is_like = 0
        ),
​
        // 第6条:图片动态(修复:like_list的circle_id统一、明确泛型)
        CircleBean.DataBean(
            id = "190378",
            user_id = "3372793",
            type = Constants.TYPE_IMAGE, // "2"(图片动态)
            content = "",
            files = listOf(
                "https://inews.gtimg.com/news_bt/O_qHdht-j-Pc6-oSPkO0Nh5d2-gPK6YTa-ruo4P1aDQ5MAA/1000",
                "https://img1.baidu.com/it/u=2208277039,107339662&fm=253&fmt=auto&app=138&f=JPEG?w=219&h=332",
                "https://img1.baidu.com/it/u=3651140891,1638849478&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800",
                "https://img2.baidu.com/it/u=3143194608,2162960312&fm=253&fmt=auto&app=138&f=JPEG?w=499&h=300",
                "https://img1.baidu.com/it/u=174076537,4056466491&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=1201",
                "https://img2.baidu.com/it/u=3121773192,1157460465&fm=253&fmt=auto&app=138&f=JPEG?w=333&h=500",
                "https://img1.baidu.com/it/u=2377094960,1480051748&fm=253&fmt=auto&app=138&f=JPEG?w=759&h=389",
                "http://gips2.baidu.com/it/u=195724436,3554684702&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=960",
                "http://gips1.baidu.com/it/u=3874647369,3220417986&fm=3028&app=3028&f=JPEG&fmt=auto?w=720&h=1280",
            ) as MutableList<String>,
            share_title = "",
            share_image = "",
            share_url = "",
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "4天前",
            user_name = "天宫2号",
            head_img = "http://b162.photo.store.qq.com/psb?/V14EhGon4cZvmh/z2WukT5EhNE76WtOcbqPIgwM2Wxz4Tb7Nub.rDpsDgo!/b/dOaanmAaKQAA",
            comments_list = emptyCommentList,
            like_list = emptyLikeList,
            is_like = 0,
            video = ""
        ),
    )
}

3.图片预览ImagePreview:

kotlin 复制代码
package com.example.composecircledemo.ui
​
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.compose.rememberAsyncImagePainter
​
/**
 * @author: njb
 * @date:   2025/8/18 17:15
 * @desc:   描述
 */
@Composable
fun ImagePreview(
    images: List<String>,
    currentIndex: Int,
    onDismiss: () -> Unit,
    onLongClick: () -> Unit
){
    val pagerState = rememberPagerState(initialPage = currentIndex) { images.size }
​
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
            .pointerInput(Unit) {
                detectTapGestures(
                    onTap = { onDismiss() }, // 点击事件
                    onLongPress = { onLongClick() } // 长按事件
                )
            },
    ) {
        // 水平分页预览
        HorizontalPager(state = pagerState) { page ->
            Image(
                // 修复:用 ImageRequest 配置 crossfade(淡入效果)
                painter = rememberAsyncImagePainter(
                    model = ImageRequest.Builder(LocalContext.current)
                        .data(images[page]) // 图片地址
                        .crossfade(true) // 淡入效果(新版本正确用法)
                        .build()
                ),
                contentDescription = "预览图片 $page",
                modifier = Modifier.fillMaxSize(),
                contentScale = ContentScale.Fit
            )
        }
​
        // 页码指示器
        Text(
            text = "${pagerState.currentPage + 1}/${images.size}",
            color = Color.White,
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(24.dp)
        )
    }
}

4.九宫格图片:

kotlin 复制代码
package com.example.composecircledemo.ui
​
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
​
/**
 * @author: njb
 * @date:   2025/8/18 17:10
 * @desc:   描述
 */
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun NineGridImages(
    images: List<Any?>,
    modifier: Modifier = Modifier,
    onImageClick: (List<String>, Int) -> Unit,
    overallPadding: Dp = 14.dp,
    itemSpacing: Dp = 6.dp
){
    val imageCount = images.size
/*    val itemSize = when (imageCount) {
        1 -> 200.dp
        2, 3 -> 200.dp
        else -> 80.dp
    }*/
    val screenWidth = androidx.compose.ui.platform.LocalConfiguration.current.screenWidthDp.dp
    val totalOverallPadding = overallPadding.times(2)
    val totalItemSpacing = itemSpacing.times(2)
    val availableWidth = screenWidth.minus(totalOverallPadding).minus(totalItemSpacing)
    val itemSize = availableWidth.div(3)
    Log.d("NineGridImages", "图片总数: ${images.size}")
​
    androidx.compose.foundation.layout.FlowRow(
        maxItemsInEachRow = 3,
        horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(4.dp),
        verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(4.dp),
        modifier = Modifier.fillMaxWidth().padding(overallPadding)
    ) {
        images.take(9).forEachIndexed { index, url ->
            val imageUrl = url?.toString() ?: ""
​
            Image(
                painter = rememberAsyncImagePainter(
                    model = ImageRequest.Builder(LocalContext.current)
                        .data(imageUrl)
                        .build()
                ),
                contentDescription = "朋友圈图片 $index",
                modifier = Modifier
                    .size(itemSize)
                    .aspectRatio(1f) // 确保正方形
                    .clickable {
                        // 安全转换列表类型
                        val stringList = images.filterIsInstance<String>()
                        onImageClick(stringList, index)
                    },
                contentScale = ContentScale.Crop
            )
        }
    }
}

5.可展开收起的TextView:

ini 复制代码
package com.example.composecircledemo.ui
​
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
​
/**
 * @author: njb
 * @date:   2025/8/18 17:08
 * @desc:   描述
 */
@Composable
fun ExpandableText(
    text: String,
    modifier: Modifier = Modifier,
    maxLines: Int = 2,
    style: TextStyle = TextStyle(fontSize = 14.sp)
){
    var isExpanded by remember { mutableStateOf(false) }
    var textLayoutResult by remember { mutableStateOf<androidx.compose.ui.text.TextLayoutResult?>(null) }
    val hasMoreLines = textLayoutResult?.lineCount ?: 0 > maxLines
​
    Column(modifier = modifier) {
        Text(
            text = text,
            style = style,
            maxLines = if (isExpanded) Int.MAX_VALUE else maxLines,
            onTextLayout = { textLayoutResult = it },
            modifier = Modifier.fillMaxWidth()
        )
​
        if (hasMoreLines) {
            Text(
                text = if (isExpanded) "收起" else "展开",
                style = TextStyle(
                    fontSize = 12.sp,
                    color = Color.Blue
                ),
                modifier = Modifier
                    .padding(top = 4.dp)
                    .clickable { isExpanded = !isExpanded }
            )
        }
    }
}

6.评论回复Item:

kotlin 复制代码
package com.example.composecircledemo.ui
​
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.composecircledemo.ui.model.CommentListBean
​
/**
 * @author: njb
 * @date:   2025/8/18 17:11
 * @desc:   描述
 */
@Composable
fun CommentItem(
    comment: CommentListBean,
    onReplyClick: () -> Unit,
    onLongClick: () -> Unit
){
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp)
            .pointerInput(Unit) {
                detectTapGestures(
                    onTap = { onReplyClick() }, // 点击事件
                    onLongPress = { onLongClick() } // 长按事件
                )
            },
        verticalAlignment = Alignment.CenterVertically
    ) {
        // 评论内容(区分普通评论和回复)
        val commentText = if (comment.to_user_id.isNullOrBlank()) {
            "${comment.user_name}:${comment.content}"
        } else {
            "${comment.user_name} 回复 ${comment.to_user_name}:${comment.content}"
        }
​
        Text(
            text = commentText,
            style = TextStyle(
                fontSize = 12.sp,
                color = Color.DarkGray
            ),
            modifier = Modifier.weight(1f)
        )
​
        Text(
            text = "回复",
            style = TextStyle(
                fontSize = 12.sp,
                color = Color.Blue
            ),
            modifier = Modifier.padding(start = 8.dp)
        )
    }
}

7.评论输入框:

ini 复制代码
package com.example.composecircledemo.ui
​
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
 * @author: njb
 * @date:   2025/8/18 17:14
 * @desc:   描述
 */
@Composable
fun CommentInputBar(
    modifier: Modifier = Modifier,
    hint: String,
    content: String,
    onContentChange: (String) -> Unit,
    onSendClick: () -> Unit,
    onDismiss: () -> Unit
){
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black.copy(alpha = 0.3f)) // 半透明黑色遮罩
            .clickable { onDismiss() } // 点击外部关闭
    ) {
        Row(
            modifier = modifier
                .align(Alignment.BottomCenter)
                .clickable(
                    onClick = { /* 点击输入框区域不做任何操作 */ },
                    indication = null, // 移除点击反馈
                    interactionSource = androidx.compose.foundation.interaction.MutableInteractionSource()
                ),
            verticalAlignment = Alignment.CenterVertically
        ) {
            OutlinedTextField(
                value = content,
                onValueChange = onContentChange,
                placeholder = {
                    Text(text = hint, fontSize = 14.sp)
                },
                modifier = Modifier.weight(1f),
                maxLines = 3,
                singleLine = false,
                textStyle = TextStyle(fontSize = 14.sp),
                shape = TextFieldDefaults.outlinedShape,
                minLines = 1,
            )
​
            Spacer(modifier = Modifier.width(8.dp))
​
            Button(
                onClick = onSendClick,
                enabled = content.isNotBlank(),
                modifier = Modifier.padding(horizontal = 8.dp)
            ) {
                Text(text = "发送", fontSize = 14.sp)
            }
        }
    }
}

8.朋友圈Item:

ini 复制代码
package com.example.composecircledemo.ui
​
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
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.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.IconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter
import com.example.composecircledemo.R
import com.example.composecircledemo.ui.model.CircleBean
import com.example.composecircledemo.ui.model.CommentListBean
import com.example.composecircledemo.utils.Constants
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import coil.request.ImageRequest
import com.example.composecircledemo.ui.model.LikeListBean
​
/**
 * @author: njb
 * @date:   2025/8/18 17:05
 * @desc:   描述
 */
@Composable
fun CircleItem(
    data: CircleBean.DataBean,
    onActionClick: () -> Unit,
    onReplyClick: (CommentListBean) -> Unit,
    onDeleteClick: () -> Unit,
    onVideoClick: (String) -> Unit,
    onImageClick: (List<String>, Int) -> Unit,
    onCommentLongClick: (CommentListBean) -> Unit
){
    val likeList = data.like_list
​
    Column(modifier = Modifier.fillMaxWidth()) {
        // 1. 用户信息栏(头像+用户名+时间+删除按钮)
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp, 16.dp, 16.dp, 8.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            // 用户头像
            Image(
                painter = rememberAsyncImagePainter(data.head_img ?: "https://picsum.photos/200/200?default"),
                contentDescription = "用户头像",
                modifier = Modifier
                    .size(48.dp)
                    .clip(CircleShape)
                    .clickable { /* 头像点击 */ },
                contentScale = ContentScale.Crop
            )
​
            Spacer(modifier = Modifier.width(12.dp))
​
            // 用户名+时间
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = data.user_name.toString(),
                    style = TextStyle(
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Medium
                    )
                )
                Text(
                    text = data.createon.toString(),
                    style = TextStyle(
                        fontSize = 12.sp,
                        color = Color.Gray
                    ),
                    modifier = Modifier.padding(top = 2.dp)
                )
            }
​
            // 删除按钮(仅自己的动态显示)
            if (data.id == "current_user_id") {
                IconButton(onClick = onDeleteClick) {
                    Icon(
                        painter = painterResource(id = R.drawable.message_bg_delete), // 需自行添加图标资源
                        contentDescription = "删除",
                        tint = Color.Gray
                    )
                }
            }
        }
​
        // 2. 动态内容(可展开文本)
        if (!data.content.isNullOrBlank()) {
            ExpandableText(
                text = data.content,
                modifier = Modifier.padding(horizontal = 12.dp),
                maxLines = 2
            )
        }
​
        // 3. 动态内容区(根据类型渲染)
        when (data.type) {
            Constants.TYPE_IMAGE -> {
                NineGridImages(
                    images = data.files,
                    onImageClick = onImageClick,
                    overallPadding = 14.dp,
                    itemSpacing = 6.dp
                )
            }
            Constants.TYPE_VIDEO -> {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(12.dp)
                        .clickable { data.video.let { onVideoClick(it) } }
                ) {
                    // 视频封面图
                    Image(
                        painter = rememberAsyncImagePainter(
                            model = ImageRequest.Builder(LocalContext.current)
                                .data("https://picsum.photos/800/450") // 更换为可靠的测试图片
                                .crossfade(true)
                                .build()
                        ),
                        contentDescription = "视频封面",
                        modifier = Modifier
                            .fillMaxWidth()
                            .aspectRatio(16f / 9f), // 设置宽高比,确保图片可见
                        contentScale = ContentScale.Crop
                    )
​
                    // 播放图标
                    Icon(
                        painter = painterResource(id = R.mipmap.ic_video_play),
                        contentDescription = "播放视频",
                        modifier = Modifier
                            .size(48.dp)
                            .align(Alignment.Center)
                            .background(Color.Black.copy(alpha = 0.3f), CircleShape)
                            .padding(8.dp),
                        tint = Color.White
                    )
                }
            }
            Constants.TYPE_WEB -> {
                Card(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
                        .background(Color(0xFFF5F5F5), shape = RoundedCornerShape(8.dp))
                        .clickable { /* 网页点击事件 */ },
                    elevation = CardDefaults.cardElevation(2.dp)
                ) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(8.dp), // 增加内边距让内容不贴边
                        verticalAlignment = Alignment.Top
                    ) {
                        // 网页图标 - 固定尺寸
                        Image(
                            painter = rememberAsyncImagePainter(data.share_image ?: "https://picsum.photos/80/80?web"),
                            contentDescription = "网页图标",
                            modifier = Modifier
                                .size(80.dp)
                                .clip(RoundedCornerShape(4.dp)), // 可选:添加圆角
                            contentScale = ContentScale.Crop
                        )
​
                        Spacer(modifier = Modifier.width(12.dp))
​
                        // 网页标题容器 - 使用weight确保占据剩余空间
                        Box(
                            modifier = Modifier
                                .weight(1f) // 关键:占据Row中剩余的所有空间
                                .padding(vertical = 4.dp) // 垂直方向内边距
                        ) {
                            Text(
                                text = data.share_title ?: "网页分享",
                                style = TextStyle(
                                    fontSize = 14.sp,
                                    color = Color.Black // 确保文本颜色与背景对比明显
                                ),
                                maxLines = 5,
                                overflow = TextOverflow.Ellipsis, // 超出部分显示省略号
                                softWrap = true, // 允许自动换行
                                modifier = Modifier.fillMaxWidth()
                            )
                        }
                    }
                }
            }
        }
​
        // 4. 地址
        if (!data.position.isNullOrBlank() && data.position != "该位置信息暂无") {
            Text(
                text = data.position,
                style = TextStyle(
                    fontSize = 12.sp,
                    color = Color.Gray
                ),
                modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
            )
        }
​
        // 5. 点赞+评论区
        Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp)) {
            // 点赞列表
            if (data.like_list.isNotEmpty()) {
                Row(modifier = Modifier.padding(bottom = 8.dp)) {
                    Icon(
                        painter = painterResource(id = R.mipmap.menu_add), // 需自行添加图标资源
                        contentDescription = "点赞",
                        modifier = Modifier.size(16.dp),
                        tint = if (data.is_like == 1) Color.Red else Color.Gray
                    )
                    Spacer(modifier = Modifier.width(4.dp))
                    LazyRow(
                        modifier = Modifier.fillMaxWidth(), // 可选:添加宽度修饰,避免内容溢出
                        contentPadding = PaddingValues(horizontal = 4.dp) // 可选:添加内边距
                    ) {
                        // 1. 用 itemsIndexed 直接获取索引,避免 indexOf 问题
                        itemsIndexed(likeList) { index, item ->
                            // item 显式指定为 LikeListBean 类型(或通过类型推断)
                            val likeItem = item as LikeListBean // 若推断失败,显式强转(确保集合类型正确)
​
                            // 2. 修复 user_name → userName(匹配 LikeListBean 字段名)
                            Text(
                                text = likeItem.user_name ?: "未知用户",
                                style = TextStyle(
                                    fontSize = 12.sp,
                                    color = if (data.is_like == 1) Color.Red else Color.Gray
                                ),
                                modifier = Modifier.padding(horizontal = 2.dp)
                            )
​
                            // 3. 用 index 判断是否为最后一个元素(无需 indexOf)
                            if (index != likeList.lastIndex) {
                                Text(
                                    text = "、",
                                    fontSize = 12.sp,
                                    color = Color.Gray,
                                    modifier = Modifier.padding(horizontal = 2.dp)
                                )
                            }
                        }
                    }
                }
            }
​
            // 评论列表
            if (data.comments_list.isNotEmpty() == true) {
                Column(modifier = Modifier.padding(bottom = 8.dp)) {
                    data.comments_list.forEach { comment ->
                        CommentItem(
                            comment = comment,
                            onReplyClick = { onReplyClick(comment) },
                            onLongClick = { onCommentLongClick(comment) }
                        )
                    }
                }
            }
​
            // 操作按钮(点赞+评论)
            IconButton(onClick = onActionClick, modifier = Modifier.fillMaxWidth()) {
                Row(
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier.fillMaxWidth()
                ) {
                    // 点赞图标+文字
                    Icon(
                        painter = painterResource(id = if (data.is_like == 1) R.mipmap.live_gift else R.mipmap.live_share),
                        contentDescription = "点赞",
                        modifier = Modifier.size(16.dp),
                        tint = if (data.is_like == 1) Color.Red else Color.Gray
                    )
                    Spacer(modifier = Modifier.width(4.dp))
                    Text(
                        text = if (data.is_like == 1) "已点赞" else "点赞",
                        fontSize = 12.sp,
                        color = Color.Gray
                    )
​
                    Spacer(modifier = Modifier.weight(1f)) // 占位,将评论按钮推到右侧
​
                    // 评论图标+文字
                    Icon(
                        painter = painterResource(id = R.mipmap.live_comment),
                        contentDescription = "评论",
                        modifier = Modifier.size(16.dp),
                        tint = Color.Gray
                    )
                    Spacer(modifier = Modifier.width(4.dp))
                    Text(
                        text = "评论(${data.comments_list.size})",
                        fontSize = 12.sp,
                        color = Color.Gray
                    )
                }
            }
        }
​
        // 分割线
        Spacer(
            modifier = Modifier
                .fillMaxWidth()
                .height(1.dp)
                .background(Color.LightGray.copy(alpha = 0.3f))
        )
    }
}

9.用户头像和回复的信息:

ini 复制代码
package com.example.composecircledemo.ui
​
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
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.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
​
/**
 * @author: njb
 * @date:   2025/8/18 17:04
 * @desc:   描述
 */
@Composable
fun CircleHeader(){
    Column(modifier = Modifier.fillMaxWidth()) {
        Box(modifier = Modifier.height(120.dp)) {
            Image(
                painter = rememberAsyncImagePainter("https://picsum.photos/1080/300"),
                contentDescription = "朋友圈背景",
                modifier = Modifier.fillMaxSize(),
                contentScale = ContentScale.Crop
            )
​
            Box(
                modifier = Modifier
                    .align(Alignment.BottomEnd)
                    .padding(end = 16.dp),
                contentAlignment = Alignment.BottomEnd
            ) {
                Row(
                    verticalAlignment = Alignment.CenterVertically, // 用户名与头像垂直居中
                    modifier = Modifier
                        .align(Alignment.TopEnd) // 从顶部开始定位
                        .padding(end = 16.dp) // 右侧间距
                        .offset(y = 30.dp)
                ) {
                    // 用户名(在头像左侧)
                    Text(
                        text = "当前用户",
                        color = Color.White,
                        modifier = Modifier.padding(end = 12.dp). offset(x=10.dp,y = (-8).dp)
                    )
​
                    // 用户头像
                    Image(
                        painter = rememberAsyncImagePainter("https://picsum.photos/200/200?user"),
                        contentDescription = "用户头像",
                        modifier = Modifier
                            .size(60.dp)
                            .clip(CircleShape)
                            .clickable { /* 头像点击事件 */ }
                            .align(Alignment.Bottom),
                        contentScale = ContentScale.Crop
                    )
                }
            }
        }
​
        Card(
            modifier = Modifier
                .width(220.dp)
                .height(80.dp)
                .padding(12.dp)
                .offset(x = 20.dp)
                .clickable { /* 消息点击事件 */ },
            elevation = CardDefaults.cardElevation(4.dp)
        ) {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Image(
                    painter = rememberAsyncImagePainter("https://picsum.photos/40/40?msg"),
                    contentDescription = "消息图标",
                    modifier = Modifier.size(24.dp)
                )
                Spacer(modifier = Modifier.width(8.dp))
                Text(text = "10条新信息")
            }
        }
    }
}

10.视频播放详情页:

kotlin 复制代码
package com.example.composecircledemo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.ui.StyledPlayerView
import kotlinx.coroutines.awaitCancellation

/**
 * @author: njb
 * @date:   2025/8/18 18:27
 * @desc:   描述
 */
class PlayVideoActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val videoUrl = intent.getStringExtra("url") ?: ""
        setContent {
            VideoPlayerScreen(videoUrl = videoUrl)
        }
    }

    @Composable
    fun VideoPlayerScreen(videoUrl: String) {
        val context = LocalContext.current
        val exoPlayer = remember {
            ExoPlayer.Builder(context).build().apply {
                val mediaItem = MediaItem.fromUri(videoUrl)
                // val mediaItem = MediaItem.Builder().setUri(videoUrl).build()
                setMediaItem(mediaItem)
                prepare() // 准备播放
                playWhenReady = true // 自动播放
            }
        }

        AndroidView(
            factory = { ctx ->
                StyledPlayerView(ctx).apply {
                    player = exoPlayer
                    layoutParams = android.view.ViewGroup.LayoutParams(
                        android.view.ViewGroup.LayoutParams.MATCH_PARENT,
                        android.view.ViewGroup.LayoutParams.MATCH_PARENT
                    )
                }
            },
            modifier = Modifier.fillMaxSize()
        )
        LaunchedEffect(Unit) {
            try {
                awaitCancellation()
            } finally {
                // 协程取消时释放播放器
                exoPlayer.release()
            }
        }
    }

}

11.在MainActivity调用:

kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposeCircleDemoTheme {
                MainScreen()
            }
        }
    }
}

12.实现效果:

13.总结:

以上就是的内容,使用Compose实现简单微信朋友圈,主要熟悉Compose的各种语法和视频播放等等.

  • 发现图片最开始没有铺满全屏,间距也有问题。
  • 回复评论的时候会崩溃。
  • 视频播放和预览有问题,exoplayer导包和回收等各种状态控制
  • 用户头像没有居中,也不在底部,约束没有调整好。
  • 网页的TextView没有显示完整,也是需要自己进行动态调整。
  • 评论和点赞弹框不显示,输入框点击外部不消失。
  • 用户回复的信息会遮挡等等。
  • 目前这只是一个很简单的ui界面,后面熟悉了加入网络请求,发布和上传图片、视频等。

14.项目源码地址:

gitee.com/jackning_ad...

相关推荐
JulyYu4 分钟前
Android系统保存重名文件后引发的异常解决
android·操作系统·源码
叽哥7 分钟前
Kotlin学习第 2 课:Kotlin 基础语法:掌握变量、数据类型与运算符
android·kotlin·app
tangweiguo0305198711 分钟前
Android原生(Kotlin)与Flutter混合开发 - 设备控制与状态同步解决方案
android·flutter
安卓开发者2 小时前
驾驭复杂表单:用 RxJava 实现响应式表单处理
android·rxjava
xiangxiongfly9153 小时前
Android 圆形和圆角矩形总结
android·圆形·圆角·imageview
幻雨様9 小时前
UE5多人MOBA+GAS 45、制作冲刺技能
android·ue5
Jerry说前后端11 小时前
Android 数据可视化开发:从技术选型到性能优化
android·信息可视化·性能优化
Meteors.11 小时前
Android约束布局(ConstraintLayout)常用属性
android
alexhilton12 小时前
玩转Shader之学会如何变形画布
android·kotlin·android jetpack