如何在使用 MatchedGeometryEffect 时修复 Z 堆栈视图重新排序?

问题描述 投票:0回答:1

我想创建一个简单的自定义分段控制组件,其动画类似于本机组件。我见过很多人为此使用

matchedGeometryEffect
,但是当我实现该组件时,我注意到动画期间的行为:当我的背景矩形向右滑动时,先前选择的选项卡移动到该视图后面,这会产生令人不快的撕裂运动而不是平滑的动画。我怀疑这个问题与 Z-Stack 视图重新排序有关,但我不知道如何解决它,请帮助我。

这是我正在使用的代码

import SwiftUI

struct TabSegmentedControlView: View {
    @State private var currentTab = 0
    
    var body: some View {
        VStack {
            TabBarView(currentTab: $currentTab)
        }
    }
}

struct TabBarView: View {
    @Binding var currentTab: Int
    @Namespace var namespace
    
    var tabBarOptions: [String] = ["shield", "house", "hands.clap"]
    
    var body: some View {
        HStack(spacing: 0) {
            ForEach(
                Array(zip(self.tabBarOptions.indices,
                          self.tabBarOptions)),
                id: \.0,
                content: { id, name in
                    TabBarTabView(
                        currentTab: $currentTab,
                        namespace: namespace.self,
                        icon: name,
                        title: name,
                        tab: id
                    )
                }
            )
        }
        .padding(.all, 2)
        .background(.gray.opacity(0.2))
        .cornerRadius(16)
        .padding(.horizontal, 16)
    }
}

struct TabBarTabView: View {
    @Binding var currentTab: Int
    let namespace: Namespace.ID
    
    let icon: String
    let title: String
    let tab: Int
    
    var body: some View {
        Button(action: {
            self.currentTab = tab
        }) {
            ZStack {
                if tab == currentTab {
                    Color.white
                        .frame(height: 60)
                        .frame(maxWidth: .infinity)
                        .cornerRadius(14)
                        .shadow(color: .black.opacity(0.04), radius: 0.5, x: 0, y: 3)
                        .shadow(color: .black.opacity(0.12), radius: 4, x: 0, y: 3)
                        .transition(.offset())
                        .matchedGeometryEffect(
                            id: "slidingRect",
                            in: namespace,
                            properties: .frame
                        )
                }
                VStack(spacing: 4) {
                    Image(systemName: icon)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 24, height: 24)
                    Text(title)
                }
                .padding()
                .frame(maxWidth: .infinity)
                .font(Font.body.weight(.medium))
                .foregroundColor(tab == currentTab ? .black : .gray)
                .transition(.opacity)
                .cornerRadius(14)
                .frame(height: 60)
            }
            .animation(.easeInOut, value: self.currentTab)
        }
    }
}

#Preview {
    TabSegmentedControlView()
}

预览:点击此处

swift swiftui matchedgeometryeffect swiftui-matchedgeometryeffect
1个回答
0
投票

您目前的方式是,每个按钮都有自己的白色背景。您正在使用

.matchedGeometryEffect
来动画化从一个按钮背景到另一个按钮背景的变化,但这并不是无缝工作的。

我建议,实现移动背景的更好方法是只有一个在按钮之间移动的形状。主要变化如下:

  • 删除各个按钮的背景,并将形状添加到

    HStack
    的背景中。

  • 按钮应用作

    .matchedGeometryEffect
    的源,因此每个按钮应具有唯一的 id。

  • 背景应有

    isSource: false
    并使用所选选项卡的 id。这样,背景就会与所选按钮相匹配。

  • .animation
    修改器从
    TabBarTabView
    移至
    HStack

其他建议:

  • 无需对背景形状应用任何尺寸,因为尺寸来自

    .matchedGeometryEffect

  • 也不需要任何

    .transition
    修饰符,因为没有视图出现或消失。

  • 修饰符

    .foregroundColor
    已弃用,请使用
    .foregroundStyle
    代替。

  • 修饰符

    .cornerRadius
    也已弃用。您可以将
    .clipShape
    RoundedRectangle
    一起使用,或者只是将
    RoundedRectangle
    放在背景中并用所需的颜色填充它。

  • 修饰符

    .scaledToFit()
    的作用与
    .aspectRatio(contentMode: .fit)
    相同,但可能更简单。

  • 无需使用

    self
    限定变量。如果颜色和字体已经是使用上下文中的预期类型,则无需使用
    Color
    Font
    来限定颜色和字体。

  • ForEach
    的数组可以通过将
    .enumerated()
    应用到源数组来构建,而不是使用索引压缩内容。

  • 尽可能使用尾随闭包。

这是完全更新的示例:

struct TabBarView: View {
    @Binding var currentTab: Int
    @Namespace var namespace

    var tabBarOptions: [String] = ["shield", "house", "hands.clap"]

    var body: some View {
        HStack(spacing: 0) {
            ForEach(Array(tabBarOptions.enumerated()), id: \.offset) { id, name in
                TabBarTabView(
                    currentTab: $currentTab,
                    namespace: namespace,
                    icon: name,
                    title: name,
                    tab: id
                )
            }
        }
        .background {
            RoundedRectangle(cornerRadius: 14)
                .fill(.white)
                .shadow(color: .black.opacity(0.04), radius: 0.5, x: 0, y: 3)
                .shadow(color: .black.opacity(0.12), radius: 4, x: 0, y: 3)
                .matchedGeometryEffect(
                    id: currentTab,
                    in: namespace,
                    isSource: false
                )
        }
        .padding(2)
        .background {
            RoundedRectangle(cornerRadius: 16)
                .fill(.gray.opacity(0.2))
        }
        .padding(.horizontal, 16)
        .animation(.easeInOut, value: currentTab)
    }
}

struct TabBarTabView: View {
    @Binding var currentTab: Int
    let namespace: Namespace.ID
    let icon: String
    let title: String
    let tab: Int

    var body: some View {
        Button {
            currentTab = tab
        } label: {
            VStack(spacing: 4) {
                Image(systemName: icon)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 24, height: 24)
                Text(title)
            }
            .padding()
            .frame(maxWidth: .infinity)
            .font(.body.weight(.medium))
            .foregroundStyle(tab == currentTab ? .black : .gray)
            .frame(height: 60)
        }
        .matchedGeometryEffect(
            id: tab,
            in: namespace,
            isSource: true
        )
    }
}

Animation

© www.soinside.com 2019 - 2024. All rights reserved.