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()}
}
最终效果: