SwiftUI中matchedGeometryEffect修饰符的使用

SwiftUI 中,动画是提升应用用户体验的重要手段之一。MatchedGeometryEffect 是 SwiftUI 提供的一个强大的工具,它可以在视图层次结构中不同位置的视图之间创建平滑的动画过渡。这种效果特别适用于列表和详情视图之间的动画,或者在视图布局改变时保持视觉上的连贯性。

MatchedGeometryEffect 允许开发者标记两个视图,使得 SwiftUI 可以在这两个视图之间自动计算和应用动画,从而创建一个连续的用户体验。这是通过在两个视图上使用相同的 id 来实现的。

首先看一下这个修饰符的定义:

func matchedGeometryEffect<ID>(
    id: ID,
    in namespace: Namespace.ID,
    properties: MatchedGeometryProperties = .frame,
    anchor: UnitPoint = .center,
    isSource: Bool = true
) -> some View where ID : Hashable

参数详解

  1. id: 类型ID,必须遵循 Hashable 协议。这个参数用于唯一标识匹配的视图。只有当两个视图使用相同的 id 时,matchedGeometryEffect 才能正确地在它们之间创建动画过渡。这意味着你可以有多组视图使用不同的 id 进行独立的动画过渡。
  2. namespace: Namespace.ID。命名空间用于将不同视图的动画过渡关联起来。你需要在视图的环境中定义一个 Namespace,并将其传递给参与动画的所有视图。这个命名空间确保只有标记了相同命名空间的视图之间才会应用动画效果。
  3. properties: MatchedGeometryProperties(默认值为 .frame)。这个参数定义了哪些属性将包含在动画过渡中。可选的属性包括:
    • .frame: 包括视图的位置和大小。
    • .opacity: 包括视图的不透明度。
    • .scale: 包括视图的缩放比例。
    • 也可以使用数组语法组合多个属性,例如 [.frame, .opacity]
  4. anchor: UnitPoint(默认值为 .center)。锚点定义了视图的哪一部分用作动画的参考点。例如,.center 表示动画将围绕视图的中心进行。其他选项包括 .topLeading, .top, .topTrailing, .leading, .trailing, .bottomLeading, .bottom, .bottomTrailing 等,这些都可以改变动画的行为和视觉效果。
  5. isSource: Bool(默认值为 true)。在使用 matchedGeometryEffect 时用于指定视图是动画的起点(源)还是终点(目标)。在动画过渡中,源视图的布局属性(如位置和尺寸)会被捕获,并在动画过程中转移到目标视图。

大多数情况下也只会用到idnamespace两个参数。

下面先看一个简单的示例:

struct MatchedGeometryEffectDemo: View {

  @Namespace private var imageNamespace
  @State private var showImage: Bool = false

  var body: some View {
    ZStack {
      if showImage {
        Image("Image_0")
          .resizable()
          .matchedGeometryEffect(id: "Image_0", in: imageNamespace, isSource: true)
          .frame(maxWidth: .infinity, maxHeight: .infinity)
          .aspectRatio(contentMode: .fit)
          .onTapGesture {
            withAnimation(.spring()) {
              showImage = false
            }
          }
      } else {
        VStack {
          Image("Image_0")
            .resizable()
            .matchedGeometryEffect(id: "Image_0", in: imageNamespace, isSource: true)
            .frame(width: 100, height: 80)
            .aspectRatio(contentMode: .fit)
            .onTapGesture {
              withAnimation(.spring()) {
                showImage = true
              }
            }
          Spacer()
        }
      }
    }
  }
}

上面的代码中,我们添加了两个Image,并都给添加了.matchedGeometryEffect修饰符,传入相同的idnamespace,在逻辑上,这两个Image是同一个视图。当点击的时候,由一个视图切换到另一个视图去显示。

效果图如下:

在这里插入图片描述
现在将上面的代码稍微加工一下:

struct MatchedGeometryEffectDemo: View {
  let columns: [GridItem] = [
    GridItem(.flexible(minimum: 100, maximum: 150), spacing: 8),
    GridItem(.flexible(minimum: 100, maximum: 150), spacing: 8),
    GridItem(.flexible(minimum: 100, maximum: 150), spacing: 8)]
  let images: [String] = ["Image_0", "Image_1", "Image_2", "Image_3", "Image_4"]
  @Namespace private var imageNamespace
  @State private var selectedImage: String = ""

  var body: some View {
    if !selectedImage.isEmpty {
      Image(selectedImage)
        .resizable()
        .matchedGeometryEffect(id: selectedImage, in: imageNamespace)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .aspectRatio(contentMode: .fit)
        .onTapGesture {
          withAnimation(.easeInOut) {
            selectedImage = ""
          }
        }
    } else {
      VStack {
        LazyVGrid(columns: columns, content: {
          ForEach(images, id: \.self) { image in
            Image(image)
              .resizable()
              .matchedGeometryEffect(id: image, in: imageNamespace)
              .frame(height: 80)
              .aspectRatio(contentMode: .fit)
              .onTapGesture {
                withAnimation(.easeInOut) {
                  selectedImage = image
                }
              }
          }
        })
        .padding()
        Spacer()
      }
    }
  }
}

在这里插入图片描述
上面是一个图片的列表界面,点击每个图片,就将这个图片全屏幕放大。

有了上面的基础后,再看一个类似Segment的组件:

struct MatchedGeometryEffectViewDemo: View {
  let categories: [String] = ["Home", "Videos", "Article", "Event"]
  @State private var selected: String = "Home"
  @Namespace private var namespace

  var body: some View {
    HStack {
      ForEach(categories, id: \.self) { category in
        ZStack(alignment: .bottom) {
          if selected == category {
            RoundedRectangle(cornerRadius: 10)
              .fill(Color.red)
              .matchedGeometryEffect(id: "category", in: namespace)
              .frame(height: 2)
              .offset(y: 10)
          }
          Text(category)
            .font(.headline)
            .foregroundColor(selected == category ? .red : .black)
        }
        .frame(maxWidth: .infinity)
        .frame(height: 65)
        .onTapGesture {
          withAnimation(.spring()) {
            selected = category
          }
        }
      }
    }
  }
}

该组件同样使用了matchedGeometryEffect修饰符,动态的将文字底部的指示器切换位置。效果如下:
在这里插入图片描述

MatchedGeometryEffectSwiftUI 中一个非常强大的特性,它可以帮助开发者在不同视图之间创建平滑且吸引人的动画过渡。通过正确使用这一特性,可以显著提升应用的用户体验。然而,也需要注意其对性能的潜在影响,并确保在适当的场景中使用。

最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值