SwipeMainActivity代码如下:
package com.example.myapplication
import 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.SwipeMenuList
class SwipeMainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val context = LocalContext.current // 提前获取 context
MaterialTheme {
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)
@Composable
fun 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.components
import 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.ui
import 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() }
}
}
}
// ✅ 新增:获取所有打开的 SwipeState
val openSwipeStates = remember { mutableStateListOf<SwipeState>() }
// 获取当前协程作用域
val coroutineScope = rememberCoroutineScope()
LazyColumn(modifier = modifier.fillMaxSize()) {
items(items) { item ->
val state = states[item]!!
// ✅ 更新:监听 isOpen 变化,同步到 openSwipeStates
LaunchedEffect(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.utils
import 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 set
var isOpen by mutableStateOf(false)
private set
companion 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) return
while (offsetX > -menuWidth) {
offsetX -= 20.coerceAtMost(offsetX + menuWidth)
delay(16)
}
offsetX = -menuWidth
isOpen = true
onOpened()
}
/**
* 动画关闭菜单
*/
suspend fun close() {
if (!isOpen && offsetX == 0) return
while (offsetX < 0) {
offsetX += 20.coerceAtMost(-offsetX)
delay(16)
}
offsetX = 0
isOpen = false
onClosed()
}
}
最终效果: