Animation

  • 经过transition增加和删去视图
  • 组合transition
  • 创立不对称transition
  • 自界说transition
  • Text的Animation
  • 运用transactions重写动画
  • 当动画完结时运转一个回调函数
  • 运用相位动画器创立多步动画

概述

文章主要分享SwiftUI Modifier的学习进程,将运用事例的方法进行说明。内容浅显易懂,Animation未做调试成果,不过测验代码是彻底的。假如想要运转成果,能够移步Github下载code -> github事例链接

1、经过transition增加和删去视图

能够在设计中包括和排除一个视图,只需要运用一个惯例的判别条件即可。

struct FFTransitionView: View {
    @State private var showDetails = false
    @State private var showDetails1 = false
    var body: some View {
        //例如,点击按钮增加或删去文本
        VStack {
            Button("Press to show details") {
                withAnimation {
                    showDetails.toggle()
                }
            }
            if showDetails {
                Text("Details go here")
            }
        }
        //默许状况下,SwiftUI运用淡出动画来插入或闪促视图,但是想要自界说作用,
        //能够运用transition()修饰符来自界说
        VStack {
            Button("Press to show details") {
                withAnimation {
                    showDetails1.toggle()
                }
            }
            if showDetails1 {
                Text("Details go here")
                    .transition(.move(edge: .bottom))
                Text("Details go here")
                    .transition(.slide)
                Text("Details go here")
                    .transition(.scale)
            }
        }
    }
}

2、组合transition

当增加或删去视图时,SwiftUI能够运用combined(with:)方法组合过度来制造新的动画。

AnyTransition扩展

为了使组合转化更简略运用和重用,能够在AnyTransition上创立他们作为扩展

extension AnyTransition {
    static var moveAndScale: AnyTransition {
        AnyTransition.move(edge: .bottom).combined(with: .scale)
    }
}

combined多组transition

struct FFTransitionsCombine: View {
    @State private var showDetails = false
    @State private var showDetails2 = false
    var body: some View {
        //例如,同时让视图移动和淡出
        VStack {
            Button("Press to show Details") {
                withAnimation {
                    showDetails.toggle()
                }
            }
            if showDetails {
                Text("Details go here")
                    .transition(AnyTransition.opacity.combined(with: .slide))
            }
        }
        VStack {
            Button("Press to show Details") {
                withAnimation {
                    showDetails2.toggle()
                }
            }
            //已经运用扩展声明了函数moveAndscale
            if showDetails2 {
                Text("Details go here")
                    .transition(.moveAndScale)
            }
        }
    }
}

3、创立不对称transition

SwiftUI能够在增加一个视图时指定transition,在删去时指定另一个transition。所有这些都是运用asymmetric()完结的。

struct FFTransitionAsymmetric: View {
    @State private var showDetails = false
    var body: some View {
        //创立一个运用不对称过渡的文本视图:增加时从左边呈现,删去时向下移动
        VStack {
            Button("Press to show details") {
                withAnimation {
                    showDetails.toggle()
                }
            }
            if showDetails {
                Text("Details go here")
                    .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .bottom)))
            }
        }
    }
}

4、自界说transition

虽然SwiftUI自带了一系列的转场动画,也能够自界说transition,这个进程分为三个过程:

  1. 创立一个ViewModifier来表明转化的任何状况
  2. 创立一个AnyTransition扩展,该扩展运用活动状况和标识状况的视图修饰符
  3. 运用transition()修饰符将动画应用到视图上

详细实操过程

编写一个形状和视图修饰符组合,仿照Keynote中的虹膜动画:

  1. 界说一个ScaledCircle形状,它在一个矩形中创立一个圆,该矩形根据一些可动画化的数据进行缩放。
  2. 创立一个自界说ViewModifier结构体,将任何形状(缩放后的圆)应用于另一个视图的剪辑形状。
  3. 在其包装在AnyTransition扩展中,以便将该修饰符包装在transition中,以便于调用。

ScaledCircle

struct ScaledCircle: Shape {
    //操操控作矩形内圆的巨细。但为0时,圆不行见,当为1时,圆填充矩形
    var animatableData: Double
    func path(in rect: CGRect) -> Path {
        let maximumCircleRadius = sqrt(rect.width * rect.width + rect.height * rect.height)
        let circleRadius = maximumCircleRadius * animatableData
        let x = rect.midX - circleRadius / 2
        let y = rect.midY - circleRadius / 2
        let circleRect = CGRect(x: x, y: y, width: circleRadius, height: circleRadius)
        return Circle().path(in: circleRect)
    }
}

ClipShapeModifier

通用修饰符,能够剪辑任何形状的视图。

struct ClipShapeModifier<T: Shape>: ViewModifier {
    let shape: T
    func body(content: Content) -> some View {
        content.clipShape(shape)
    }
}

AnyTransition

结合ScaledCircle和ClipShapeModifier的自界说transition

extension AnyTransition {
    static var iris: AnyTransition {
        .modifier(
            active: ClipShapeModifier(shape: ScaledCircle(animatableData: 0)),
            identity: ClipShapeModifier(shape: ScaledCircle(animatableData: 1)))
    }
}

运用自界说transition

struct FFTransitionCustom: View {
    @State private var isShowingRed = false
    var body: some View {
        ZStack {
            Color.blue
                .frame(width: 200, height: 200)
            if isShowingRed {
                Color.red
                    .frame(width: 200, height: 200)
                    .transition(.iris)
                    .zIndex(1)
            }
        }
        .padding(50)
        .onTapGesture {
            withAnimation(.easeInOut) {
                isShowingRed.toggle()
            }
        }
    }
}

5、Text的Animation

iOS16今后,SwiftUI能够将Text动画化。因此像这样的代码能够在两种不同的巨细之间苹果的显示动画,主动的重新渲染文本。

struct FFAnimateTextSize: View {
    @State private var fontSize = 32.0
    var body: some View {
        Text("Hi, metaBBLv")
            .font(.custom("Georgia", size: fontSize))
            .onTapGesture {
                withAnimation(.spring(response: 0.5, dampingFraction: 0.5, blendDuration: 1).repeatForever()) {
                    fontSize = 72
                }
            }
    }
}

6、运用transactions重写动画

SwiftUI供给了一个withTransaction()函数,能够在运转时重写动画,例如删去隐式动画并用自界说内容替换他。

struct FFAnimationsOverride: View {
    @State private var isZoomed = false
    @State private var isZoomed1 = false
    @State private var isZoomed2 = false
    var body: some View {
        VStack {
            Button("Toggle zoom") {
                isZoomed.toggle()
            }
            Spacer()
                .frame(height: 50)
            Text("Zoom text")
                .font(.title)
                .scaleEffect(isZoomed ? 3 : 1)
                .animation(.easeInOut(duration: 2), value: isZoomed)
        }
        //transactions能够掩盖现有的动画。例如,你可能决定在一个特定状况下,文本的动画以一种款素、线性的方法产生,而不是先有的动画。
        //要做到这一点,首要运用想要的动画创立一个新的Transaction实例,然后将他的disablesAnimations值设置为true,这样就能够掩盖任何的现有动画。当全部准备好时,运用withTranscation()。然后持续调整你想要要变的状况。它将运用你的Transcation被动画化。
        Spacer()
        VStack {
            Button("Toggle zoom") {
                var transaction = Transaction(animation: .linear)
                transaction.disablesAnimations = true
                withTransaction(transaction) {
                    isZoomed1.toggle()
                }
            }
            Spacer()
                .frame(height: 50)
            Text("Zoom text")
                .font(.title)
                .scaleEffect(isZoomed1 ? 3 : 1)
                .animation(.easeInOut(duration: 2), value: isZoomed1)
        }
        Spacer()
        //关于更多的操控,能够将transaction()修饰符附加到任何视图上。从而能够掩盖该应用在该视图的任何事物。
        VStack {
            Button("Toggle Zoom") {
                var transaction = Transaction(animation: .linear)
                transaction.disablesAnimations = true
                withTransaction(transaction) {
                    isZoomed2.toggle()
                }
            }
            Spacer()
                .frame(height: 50)
            Text("Zoom Text 1")
                .font(.title)
                .scaleEffect(isZoomed2 ? 3 : 1)
            Spacer()
                .frame(height: 50)
            Text("Zoom Text 2")
                .font(.title)
                .scaleEffect(isZoomed2 ? 3 : 1)
                .transaction { t in
                    t.animation = .none
                }
        }
    }
}

7、当动画完结时运转一个回调函数

能够挑选为SwiftUI的withAnimation()函数供给完结后的回调,并在动画完结时运转代码。这可能是调整某些程序状况的地方,但能够将其用作将动画衔接在一起的简略方法。对一个事物进行动画处理,然后对其他事物进行动画处理。

struct FFAnimationFinishCallback: View {
    @State private var scaleUp = false
    @State private var fadeOut = false
    @State private var scaleUp1 = false
    @State private var fadeOut1 = false
    var body: some View {
        //点击按钮然后扩大并淡出
        Button("Tap Me!") {
            withAnimation {
                scaleUp = true
            } completion: {
                withAnimation {
                    fadeOut = true
                }
            }
        }
        .scaleEffect(scaleUp ? 3 : 1)
        .opacity(fadeOut ? 0 : 1)
        //这里有一个小细节可能会让你感觉到震惊,假如你运用弹簧动画,则最后可能会呈现很长的运动尾部,其中你的动画正在移动用户无法察觉的。
        //默许行为withAnimation()是认为动画完结的,及时荏苒产生微笑运动的肠胃,但假如希望100%完结,能够掩盖默许值。
        Button("Tap Me!") {
            withAnimation(.bouncy, completionCriteria: .removed) {
                scaleUp1 = true
            } completion: {
                withAnimation {
                    fadeOut1 = true
                }
            }
        }
        .scaleEffect(scaleUp1 ? 3 : 1)
        .opacity(fadeOut1 ? 0 : 1)
        //关于更杂乱的作用,请考虑运用相位动画器而不是动画完结的闭包
    }
}

8、运用相位动画器创立多步动画

SwiftUI的phaseAnimator视图和phaseAnimator修改器能够经过持续或触发时循环挑选动画段来执行多步动画,创立这些多阶动画需要三个过程:

  1. 界说阶段,能够是任何类型的序列,但运用枚举最简略CaseIterable。
  2. 读取相位动画中的一个相位,并调整视图以匹配想要该相位的外观。
  3. 增加一个触发器,使相位动画从头开端重复其序列

enum

//经过枚举设置
enum AnimationPhase: Double, CaseIterable {
    case fadingIn = 0
    case middle = 1
    case zoomingOut = 3
}
//能够根据你的指令触发动画序列,而不是无休止的重复。为此,能够增加一个触发器值而且经过SwiftUI监督,例如随机数UUID或递增数,当值产生变化时,开端动画并完好播放。
enum AnimationPhase1: CaseIterable {
    case start, middle, end
}
//向动画增加核算特点
enum AnimationPhase2: CaseIterable {
    case fadingIn, middle, zoomingOut
    var scale: Double {
        switch self {
        case .fadingIn: 0
        case .middle: 1
        case .zoomingOut: 3
        }
    }
    var opacity: Double {
        switch self {
        case .fadingIn: 0
        case .middle: 1
        case .zoomingOut: 0
        }
    }
}

运用上面的enum做的多种比如

struct FFAnimationmulti_step: View {
    @State private var animationStep = 0
    @State private var animationStep1 = 0
    var body: some View {
        //例如,创立一个简略动画,使某些文本开端很小且不行见,扩大到天然巨细并彻底不透明,然后扩大到非常大且不行见。
        Text("Hi, metaBBLv")
            .font(.largeTitle)
            .phaseAnimator([0,1,3]) { view, phase in
                view
                    .scaleEffect(phase)
                    .opacity(phase == 1 ? 1 : 0)
            }
        //因为没有供给触发器,所以它将永远运转。
        //运用包装视图PhaseAbunator来编写它,它的长处是多个视图能够在阶段之间一起移动
        VStack(spacing: 50) {
            PhaseAnimator([0,1,2]) { value in
                Text("Hi, metaBBLv")
                    .font(.largeTitle)
                    .scaleEffect(value)
                    .opacity(value == 1 ? 1 : 0)
                Text("Goodbye, metaBBLv")
                    .font(.largeTitle)
                    .scaleEffect(3 - value)
                    .opacity(value == 1 ? 1 : 0)
            }
        }
        Text("Hi, metaBBLv")
            .font(.largeTitle)
            .phaseAnimator(AnimationPhase.allCases) { view, phase in
                view
                    .scaleEffect(phase.rawValue)
                    .opacity(phase.rawValue == 1 ? 1 : 0)
            }
        //在下面的比如中,点击按钮会触发运用枚举状况的三步动画。首要,界说所需的各种动画阶段,然后每逢特点产生变化时都会遍历
        Button("Tap Me!") {
            animationStep += 1
        }
        .font(.largeTitle)
        .phaseAnimator(AnimationPhase1.allCases, trigger: animationStep) { content, phase in
            content
                .blur(radius: phase == .start ? 0 : 10)
                .scaleEffect(phase == .middle ? 3 : 1)
        }
        //为了取得更多的操控,能够精确指定每个阶段运用那个动画。.bouncy在快速和慢速动画之间移动.easeInOut取得更多的变化。
        Button("Tap Me 1") {
            animationStep1 += 1
        }
        .font(.largeTitle)
        .phaseAnimator(AnimationPhase1.allCases, trigger: animationStep1) { content, phase in
            content
                .blur(radius: phase == .start ? 0 : 10)
                .scaleEffect(phase == .middle ? 3 : 1)
        } animation: { phase in
            switch phase {
            case .start, .end: .bouncy
            case .middle: .easeInOut(duration: 2)
            }
        }
        //因为经过核算特点增加了动画,所以在调用时更加的简练
        Text("Hi, metaBBLv")
            .font(.largeTitle)
            .phaseAnimator(AnimationPhase2.allCases) { content, phase in
                content
                    .scaleEffect(phase.scale)
                    .opacity(phase.opacity)
            }
    }
}