Android,jetpack Compose模仿QQ侧边栏

发布于:2025-09-07 ⋅ 阅读:(22) ⋅ 点赞:(0)

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

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


网站公告

今日签到

点亮在社区的每一天
去签到