SwiftUI 如何实现带动画的 L 形自定义菜单?
结论
可以把菜单按钮和展开面板放进同一个容器,用 overlay(alignment: .topTrailing) 将面板锚定在按钮右侧,再用 transition 添加展开动画。下面的示例采用“顶部是横向按钮、菜单向下展开”的 L 形布局。要改变 L 形的方向,可以调整面板的对齐位置。
当前的 VStack 会把下拉列表放在整条胶囊按钮下方,而列表宽度又设为 pillWidth,所以展开后看起来像上下堆叠的矩形区域。用 GeometryReader 在 onAppear 中记录宽度也会增加状态管理。如果菜单宽度固定,直接指定面板宽度即可。
调整布局和动画
保留现有的 row(_:),把按钮和菜单放进同一个 ZStack。面板对齐到右上角,再根据按钮高度和间距向下偏移:
struct ContentView: View {
@State private var isExpanded = false
private let height: CGFloat = 40
private let menuWidth: CGFloat = 180
private let spacing: CGFloat = 3
var body: some View {
ZStack(alignment: .topTrailing) {
HStack(spacing: spacing) {
Button {
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
isExpanded.toggle()
}
} label: {
HStack(spacing: 3) {
Image(systemName: "person.crop.square.on.square.angled.fill")
Text("1 Sign")
.fontWeight(.regular)
}
.padding(.horizontal, 16)
.frame(height: height)
.background(Color.gray.opacity(0.2))
.clipShape(
CustomCorners(
tl: height / 2,
tr: 2,
bl: height / 2,
br: 2
)
)
}
.buttonStyle(.plain)
Image(systemName: "chevron.right")
.rotationEffect(.degrees(isExpanded ? 90 : 0))
.foregroundColor(.gray)
.frame(width: height, height: height)
.background(Color.gray.opacity(0.2))
.clipShape(
CustomCorners(
tl: 2,
tr: height / 2,
bl: 2,
br: height / 2
)
)
}
.zIndex(1)
if isExpanded {
VStack(alignment: .leading, spacing: 15) {
row("2 Sign")
row("3 Sign")
row("4 Sign")
}
.padding(.vertical, 17)
.frame(width: menuWidth)
.background {
RoundedRectangle(cornerRadius: 28)
.fill(Color.gray.opacity(0.2))
}
.offset(y: height + spacing)
.transition(.move(edge: .top).combined(with: .opacity))
.zIndex(0)
}
}
.padding(40)
}
private func row(_ title: String) -> some View {
HStack {
Text(title)
Spacer()
Image(systemName: "chevron.right")
}
.padding(.horizontal, 16)
.frame(height: 28)
}
}
ZStack(alignment: .topTrailing) 会让展开面板与按钮右边缘对齐,.offset(y: height + spacing) 则将面板移到按钮下方。withAnimation 驱动展开和收起,.transition 控制面板插入或移除时从顶部滑入并淡入。若要让菜单左边缘对齐,可将容器改为 ZStack(alignment: .topLeading)。调整对齐锚点和偏移量,也能改变 L 形的方向。
示例中的 menuWidth 是固定值。如果按钮或面板宽度需要随内容变化,可以用布局测量来同步尺寸;如果宽度固定,就可以删掉原先的 pillWidth 和 GeometryReader。
放入导航栏时
导航栏工具栏通常会限制项目的可用空间,展开内容可能因此被裁切,也可能影响导航栏布局。可以用工具栏按钮触发展开,再由页面内容层显示自定义面板;也可以按菜单需求使用系统 Menu。具体表现取决于导航容器和平台布局。仅凭当前代码无法判断你遇到的问题,建议先在页面主体中验证菜单的锚点、尺寸和动画,再移入工具栏测试。
备注:内容仅供参考。
