当前位置: 首页 > news >正文

Android,jetpack Compose模仿QQ侧边栏

SwipeMainActivity代码如下:

在这里插入图片描述

package com.example.myapplicationimport android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.myapplication.ui.SwipeMenuListclass SwipeMainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {val context = LocalContext.current // 提前获取 contextMaterialTheme {Surface(color = Color(0xFFF5F5F5)) {Column {Text("高仿QQ侧滑菜单",modifier = Modifier.fillMaxWidth().padding(16.dp),fontSize = 20.sp,fontWeight = FontWeight.Bold)SwipeMenuList (items = List(20) { "联系人 ${it + 1}" },modifier = Modifier.fillMaxWidth(),onItemTop = { Toast.makeText(context, "置顶: $it", Toast.LENGTH_SHORT).show() },onItemUnread = { Toast.makeText(context, "标为未读: $it", Toast.LENGTH_SHORT).show() },onItemDelete = { Toast.makeText(context, "删除: $it", Toast.LENGTH_SHORT).show() })}}}}}@Preview(showBackground = true)@Composablefun SwipeMenuPreview() {MaterialTheme {Surface(color = Color(0xFFF5F5F5)) {Column {Text("高仿QQ侧滑菜单",modifier = Modifier.fillMaxWidth().padding(16.dp),fontSize = 20.sp,fontWeight = FontWeight.Bold)SwipeMenuList(items = List(5) { "联系人 ${it + 1}" },modifier = Modifier.fillMaxWidth(),onItemTop = {},onItemUnread = {},onItemDelete = {})}}}}
}

SwipeMenuItem代码如下:

// ui/components/SwipeMenuItem.kt
package com.example.myapplication.ui.componentsimport androidx.compose.animation.core.animateIntOffsetAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import com.example.myapplication.utils.SwipeState@Composable
fun SwipeMenuItem(modifier: Modifier = Modifier,content: @Composable () -> Unit,onTop: () -> Unit,onUnread: () -> Unit,onDelete: () -> Unit,swipeState: SwipeState
) {val scope = rememberCoroutineScope()// 动画偏移:主内容跟随手指val targetOffset = IntOffset(swipeState.offsetX, 0)val animatedOffset by animateIntOffsetAsState(targetValue = targetOffset, label = "contentOffset")Box(modifier = modifier.clip(RoundedCornerShape(12.dp)).shadow(2.dp).background(Color.White)// ✅ 使用 detectHorizontalDragGestures,仅处理水平滑动手势.pointerInput(swipeState) {detectHorizontalDragGestures(onDragStart = { },onHorizontalDrag = { change, dragAmount ->val newOffset = swipeState.offsetX + dragAmount.toInt()if (dragAmount < 0) {// 向左滑:打开菜单swipeState.updateOffset(newOffset)} else if (dragAmount > 0 && swipeState.isOpen) {// 向右滑:关闭菜单swipeState.updateOffset(newOffset)}change.consume() // ✅ 消费事件,防止传递给父布局},onDragEnd = {scope.launch {if (swipeState.offsetX < -SwipeState.menuWidth / 2) {swipeState.open()} else {swipeState.close()}}},onDragCancel = {scope.launch {if (swipeState.offsetX < -SwipeState.menuWidth / 2) {swipeState.open()} else {swipeState.close()}}})}) {// ========== 右侧操作按钮(从右向左滑入)==========if (swipeState.offsetX < 0) {Row(modifier = Modifier.fillMaxSize(),horizontalArrangement = Arrangement.End) {// 删除Box(modifier = Modifier.width(90.dp).fillMaxHeight().background(Color(0xFFEE6363)).clickable {scope.launch {swipeState.close()onDelete()}},contentAlignment = Alignment.Center) {Text("删除", color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.Bold)}// 标记为未读Box(modifier = Modifier.width(90.dp).fillMaxHeight().background(Color(0xFFFFC125)).clickable {scope.launch {swipeState.close()onUnread()}},contentAlignment = Alignment.Center) {Text("标记为未读", color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.Bold)}// 置顶Box(modifier = Modifier.width(90.dp).fillMaxHeight().background(Color(0xFF0099FF)).clickable {scope.launch {swipeState.close()onTop()}},contentAlignment = Alignment.Center) {Text("置顶", color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.Bold)}}}// ========== 主内容层(联系人)==========Box(modifier = Modifier.offset { animatedOffset }.fillMaxSize().padding(horizontal = 16.dp),contentAlignment = Alignment.CenterStart) {content()}}
}

SwipeMenuList代码如下

// ui/SwipeMenuList.kt
package com.example.myapplication.uiimport androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.myapplication.ui.components.SwipeMenuItem
import com.example.myapplication.utils.SwipeState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch@Composable
fun SwipeMenuList(items: List<String>,modifier: Modifier = Modifier,onItemTop: (String) -> Unit,onItemUnread: (String) -> Unit,onItemDelete: (String) -> Unit
) {val openStates = remember { mutableStateMapOf<String, SwipeState>() }val states by remember(items) {derivedStateOf {items.associateWith { item ->openStates.getOrPut(item) { SwipeState() }}}}// ✅ 新增:获取所有打开的 SwipeStateval openSwipeStates = remember { mutableStateListOf<SwipeState>() }// 获取当前协程作用域val coroutineScope = rememberCoroutineScope()LazyColumn(modifier = modifier.fillMaxSize()) {items(items) { item ->val state = states[item]!!// ✅ 更新:监听 isOpen 变化,同步到 openSwipeStatesLaunchedEffect(state.isOpen) {if (state.isOpen) {// 当前打开 → 內部处理关闭其他coroutineScope.launch {openSwipeStates.forEach { it.close() }openSwipeStates.clear()openSwipeStates.add(state)}} else {// 当前关闭 → 从列表移除openSwipeStates.remove(state)}}// ✅ 为每个 item 添加点击监听:点击即关闭所有打开的菜单val itemModifier = Modifier.fillMaxWidth().height(70.dp).clickable(onClick = {// 点击任意 item → 关闭所有打开的菜单if (openSwipeStates.isNotEmpty()) {// 使用协程作用域来调用 suspend 函数coroutineScope.launch {openSwipeStates.forEach { it.close() }openSwipeStates.clear()}}})SwipeMenuItem(modifier = itemModifier,swipeState = state,onTop = { onItemTop(item) },onUnread = { onItemUnread(item) },onDelete = { onItemDelete(item) },content = {Row(modifier = Modifier.fillMaxWidth(),verticalAlignment = Alignment.CenterVertically) {Text(item, fontSize = 16.sp, fontWeight = FontWeight.Medium)Spacer(modifier = Modifier.weight(1f))Text("左滑←←←", color = Color.Gray, fontSize = 14.sp)}})}}
}

SwipeState代码如下:

// utils/SwipeState.kt
package com.example.myapplication.utilsimport androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlinx.coroutines.delay/*** 侧滑菜单状态管理类(右侧滑出菜单)*/
class SwipeState(private val onOpened: () -> Unit = {},private val onClosed: () -> Unit = {}
) {// offsetX: 0 = 关闭, 负值 = 向左滑出右侧菜单var offsetX by mutableStateOf(0)private setvar isOpen by mutableStateOf(false)private setcompanion object {const val menuWidth = 270 // 90 * 3}/*** 安全更新偏移量,限制在 [-menuWidth, 0]*/fun updateOffset(newOffset: Int) {val clamped = newOffset.coerceIn(-menuWidth, 0)if (clamped != offsetX) {offsetX = clamped}}/*** 动画打开菜单(滑出右侧按钮)*/suspend fun open() {if (isOpen) returnwhile (offsetX > -menuWidth) {offsetX -= 20.coerceAtMost(offsetX + menuWidth)delay(16)}offsetX = -menuWidthisOpen = trueonOpened()}/*** 动画关闭菜单*/suspend fun close() {if (!isOpen && offsetX == 0) returnwhile (offsetX < 0) {offsetX += 20.coerceAtMost(-offsetX)delay(16)}offsetX = 0isOpen = falseonClosed()}
}

最终效果:
请添加图片描述

http://www.xdnf.cn/news/1466569.html

相关文章:

  • 华为云昇腾云服务
  • 数据安全成焦点:基于Hadoop+Spark的信用卡诈骗分析系统实战教程
  • 为什么外网主机可以telnet通内网nginx端口,但是http请求失败?
  • Mysql:由逗号分隔的id组成的varchar联表替换成对应文字
  • Tenda AC20路由器缓冲区溢出漏洞分析
  • iOS 抓包工具有哪些?开发、测试与安全场景的实战选择
  • 软考 系统架构设计师系列知识点之杂项集萃(140)
  • 使用 chromedp 高效爬取 Bing 搜索结果
  • 安装Codex(需要用npm)
  • Chrome 插件开发入门指南:从基础到实践
  • 达梦数据守护集群监视器详解与应用指南
  • vsan高可用:确保可访问性、全部数据迁移,两种类型权衡
  • 软件启动时加配置文件 vs 不加配置文件
  • Go 1.25.1基本包
  • 凌力尔特(LINEAR)滤波器LTC1068的二阶滤波器模块设计
  • STM32 USBx Device HID standalone 移植示例 LAT1466
  • 全球企业内容管理ECM市场规模增长趋势与未来机遇解析
  • (4)什么时候引入Seata‘‘
  • 黄金上门回收小程序开发
  • 多路转接介绍及代码实现
  • Rust 基础语法
  • 设计模式笔记
  • 从技术选型到现场配置:DDC 楼宇自控系统全流程落地方案(2025 版)
  • 织信低代码:用更聪明的方式,把想法变成现实!
  • 多语言Qt Linguist
  • 职场礼仪实训室:健康管理专业人才培养的核心支柱与创新实践
  • Springboot实现国际化(MessageSource)
  • AI Compass前沿速览:Kimi K2、InfinityHuman-AI数字人、3D-AI桌面伴侣、叠叠社–AI虚拟陪伴
  • 查询语言的进化:SQL之后,为什么是GQL?数据世界正在改变
  • 生态 | 华院计算与深至科技达成战略合作,携手推动AI+医学影像算法升级迭代