[SwiftUI-Lab] SwiftUI高级过渡动画

2,909 阅读5分钟

文章源地址:swiftui-lab.com/advanced-tr…

作者: Javier

翻译: Liaoworking

SwiftUI高级过渡动画

本文我们将从简单到复杂的去探索过渡的不同形式,讨论一些不同的方面。如何去配置、组合和触发过渡。我们将学习到什么是提前出现的过渡动画。但首先要确保你对之前提到的一些概念已经熟悉。

什么是过渡动画?

过渡动画决定了在层次结构中插入或者删除一个视图的效果。过渡动画本身不起作用,需要配合动画才行。例如:

image

注意,在XCode11.2以后, 过渡动画不在对隐式动画起作用。所以下面的代码只针对于老版本的xcode起作用,还好其他的例子和版本无关,谢谢Tyler Casselman在留言中提醒。

struct ContentView: View {
    @State private var show = false
    
    var body: some View {
        
        VStack {
            Spacer()
            
            if show {
                LabelView()
                    .animation(.easeInOut(duration: 1.0))
                    .transition(.opacity)
            }
            
            Spacer()
            
            Button("Animate") {
                self.show.toggle()
            }.padding(20)
        }
    }
}

struct LabelView: View {
    var body: some View {
        Text("Hi there!")
            .padding(10)
            .font(.title)
            .foregroundColor(.white)
            .background(RoundedRectangle(cornerRadius: 8).fill(Color.green).shadow(color: .gray, radius: 3))
    }
}

你如果对什么是隐式动画和显示动画没有具体的了解,可以看我的上一篇文章《Advanced SwiftUI Animations》,对于上面的例子,你可以用这两种动画方式。如下。

if show {
    LabelView()
        .transition(.opacity)
}

Spacer()

Button("Animate") {
    withAnimation(.easeInOut(duration: 1.0)) {
        self.show.toggle()
    }
}.padding(20)

另外一种方式就是将动画和过渡关联,请注意动画是作用于过渡上的,并不是视图(例如这里是在.transition()里)

if show {
    LabelView()
        .transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
}

Spacer()

Button("Animate") {
    self.show.toggle()
}.padding(20)

不对称的过渡

一般情况下,当视图添加到视图层级时,过渡会起作用。 当视图被移除的时候,会有一个相反的效果。opacity将会在添加视图的时候淡入。移除的时候就会淡出。这都可以改变的。

如果我们想特定添加或者移除的动画,我们可以使用 .asymmetric选项。

image

.transition(.asymmetric(insertion: .opacity, removal: .scale))

组合过渡

如果你想要在过渡过程中使用几种转场,例如滑动的时候添加渐入效果。你可以使用下面这种过渡:

image

.transition(AnyTransition.opacity.combined(with: .slide))

注意你可以同时使用 .asymmetric.combined

.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .slide), removal: .scale))

带参数的转换

我们前面用的过渡.opacity, .slide, .scale 都是没有参数的,然而一些转换可以添加一些额外的参数,如下:

.scale(scale: 0.0, anchor: UnitPoint(x: 1, y: 0))
.scale(scale: 2.0)
.move(edge: .leading)
.offset(x: 30)
.offset(y: 50)
.offset(x: 100, y: 10)

自定义过渡,一个有趣的开始

让我们开始吧~

过渡是怎样工作的?

系统内部,自定义过渡和标准库的过渡都是以同样的原理工作,都需要为动画的开始和结尾添各加一个修饰器(modifier)。只要两个修饰器组件的差异是可变的,swiftUI都会自己处理。 我们假设.opacity过渡不存在,我们需要去自定义一个,先取名叫.myOpacity. 这里是具体实现。

 extension AnyTransition {
    static var myOpacity: AnyTransition { get {
        AnyTransition.modifier(
            active: MyOpacityModifier(opacity: 0),
            identity: MyOpacityModifier(opacity: 1))
        }
    }
}

struct MyOpacityModifier: ViewModifier {
    let opacity: Double
    
    func body(content: Content) -> some View {
        content.opacity(opacity)
    }
}

下面你就像正常的过渡去用就行了。

.transition(.myOpacity)

正如你所看到的,我们为AnyTransition创建了一个extension. 这里我们指定了两个修饰符来创建过渡。一个用于开始,另外一个用于结束,当移除视图时SwiftUI将倒过来使用这些修饰符。

Transitions at Full Throttle

利用现在我们所掌握的,我们就可以创建一下新的过渡了, 为现有的.rotationEffect().transformEffect() 开辟了新的可能性。 不过当你打算去写新的过渡时,你可能会发现你其实无从下手。。不过"SwiftUI 高级动画"中的所有知识你都可以用起来了。这时候GeometryEffect 和 Shapes会变的很有用。

带有GeometryEffect的自定义过渡。

过渡对于弹出和收起一个面板都特别的合适。如果你想要创建自己的modal体系,你应该好好的阅读下面的内容。

我们下面的练习是创建一个简单的转场去演示怎么去弹出和收起一个视图

extension AnyTransition {
    static var fly: AnyTransition { get {
        AnyTransition.modifier(active: FlyTransition(pct: 0), identity: FlyTransition(pct: 1))
        }
    }
}

struct FlyTransition: GeometryEffect {
    var pct: Double
    
    var animatableData: Double {
        get { pct }
        set { pct = newValue }
    }
    
    func effectValue(size: CGSize) -> ProjectionTransform {

        let rotationPercent = pct
        let a = CGFloat(Angle(degrees: 90 * (1-rotationPercent)).radians)
        
        var transform3d = CATransform3DIdentity;
        transform3d.m34 = -1/max(size.width, size.height)
        
        transform3d = CATransform3DRotate(transform3d, a, 1, 0, 0)
        transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
        
        let affineTransform1 = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
        let affineTransform2 = ProjectionTransform(CGAffineTransform(scaleX: CGFloat(pct * 2), y: CGFloat(pct * 2)))
        
        if pct <= 0.5 {
            return ProjectionTransform(transform3d).concatenating(affineTransform2).concatenating(affineTransform1)
        } else {
            return ProjectionTransform(transform3d).concatenating(affineTransform1)
        }
    }
}

代码在:transiftion-present-dismiss.swift

通过 Shapes创建的自定义过渡

另外一个有用的使用场景就是在两个View之间做转场动画,一个渐入,另外一个渐出。 我们第一直觉是把两个视图放在一个Zstack中,并改变它们的透明度。这样的确OK,不过我们可以使用其他方法做出更炫酷的东西。

代码在: transiftion-present-dismiss.swift 示例代码需要你添加4张图片到你的asset catalog中(命名:photo1, photo2, photo3 和 photo4)这个示例专为iPad的横竖屏设计。

你可以在gist文件中查看代码,其实所有的过渡都遵循相同的模式, 它们都使用了可动形状(animated shape)来裁剪进入和退出的图片。因为它们是z-stacked,我们有一个好看的交叉效果。

总结

这个文章中,我们并没有去讲你需要创建你自己的SwiftUI过渡,你需要的就是释放你的想象力去创作你自己的炫酷特效。

如果想要知道最新文章,欢迎评论和关注我的twitter,下期见。