百日学 Swift(Day 33) – 项目 6 :第 2 部分

百日学 Swift(Day 33) – Project 6, part two(项目 6 :第 2 部分)

1. Controlling the animation stack(控制动画堆栈)

**大叔注:这个标题有些……直译的误导。视图后面的修饰器排列起来就像一个堆栈。动画堆栈实际上是说在修饰器堆栈里面多次使用动画修饰。

先看下面的代码,这两段代码说明了修饰器的顺序如何重要。

Button("Tap Me") {
    // do nothing
}
.background(Color.blue)
.frame(width: 200, height: 200)
.foregroundColor(.white)
Button("Tap Me") {
    // do nothing
}
.frame(width: 200, height: 200)    
.background(Color.blue)
.foregroundColor(.white)

其中的道理前面有讲,而且我们还反复使用background()padding()创造一个条纹边框效果。

这就是概念一:修饰符顺序很重要,因为SwiftUI用修饰符按应用顺序包裹视图。

概念二是我们可以animation()对视图应用修饰符,以使其隐含地对更改进行动画处理。

为了演示这一点,我们可以修改按钮代码,以便根据某些状态显示不同的颜色。首先,我们定义状态:

struct CustomViewModifier: View {
    @State var show = false			// 定义状态
    var body: some View {
        VStack(spacing: 15) {
            
            Button("点我变色"){
                self.show.toggle()	// 状态切换
            }
            .frame(width: 200, height: 100, alignment: .center)
            .foregroundColor(.white)
            .background(Color(show ? .red : .blue))		// 颜色切换
            .animation(.default)						// 动画
        }
    }
}

运行代码,将看到点击按钮会在蓝色和红色之间为其设置动画的颜色。

现在给按钮增加一个圆角修饰器,

Button("点我变色,圆角没有动画"){
    self.show.toggle()	// 状态切换
}
.frame(width: 200, height: 200)
.background(show ? Color.blue : Color.red)
.animation(.default)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: show ? 60 : 0))

运行后会看到点击按钮会使其在红色和蓝色之间进行动画处理,但是在正方形和圆角矩形之间的切换不会进行动画处理。

如果将clipShape()修改器移到动画之前,如下所示:

Button("点我变色和圆角都有动画") {
    self.enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? Color.blue : Color.red)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.default)

运行代码时,背景颜色和剪辑形状都将进行动画处理。再次说明顺序很重要:animation()仅影响在它之前发生的更改。

如果应用多个animation()修改器,则每个修改器控制着之前动画处理过的所有内容。这样能够以各种不同的方式为状态变化设置动画,而不是为所有属性统一设置。

例如,可以使用默认动画来进行颜色更改,但是对剪辑形状使用插值弹簧:

Button("点我变色和圆角有不同动画") {
    self.enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? Color.blue : Color.red)
.animation(.default)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.interpolatingSpring(stiffness: 10, damping: 1))

为了获得更多控制,可以通过传递nil到修饰器来完全禁用动画。如果需要立即进行颜色更改,但剪辑形状保留其动画,可以这样编写:

Button("点我变色无动画,圆角有动画") {
    self.enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? Color.blue : Color.red)
.animation(nil)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.interpolatingSpring(stiffness: 10, damping: 1))

2. Animating gestures(动画手势)

SwiftUI 允许将手势附加到任何视图,并且这些手势的效果也可以动画。稍后,我们将更详细地介绍手势,但现在让我们尝试一些相对简单的操作:可以在屏幕上拖动的卡片,但是放开后,它会卡回到其原始位置。

首先,我们的初始布局:

struct ContentView: View {
    var body: some View {
        LinearGradient(
            gradient: Gradient(colors: [.yellow, .red]), 
            startPoint: .topLeading, 
            endPoint: .bottomTrailing
        )
            .frame(width: 300, height: 200)
            .clipShape(RoundedRectangle(cornerRadius: 10))
    }
}

这样可以在屏幕中央绘制类似卡片的视图。我们想根据手指的位置在屏幕上移动它,这需要三个步骤。

首先,我们需要某种状态来存储其拖动量:

@State private var dragAmount = CGSize.zero

其次,我们要使用该大小来影响卡在屏幕上的位置。SwiftUI为此提供了一个专用的修饰符offset(),它使我们能够调整视图的 X 和 Y 坐标而无需在其周围移动其他视图。您可以根据需要输入离散的 X 和 Y 坐标,但是-绝非偶然- offset()也可以CGSize直接采用。

因此,第二步是将此修改器添加到线性渐变中:

.offset(dragAmount)

现在重要的部分到了:我们可以创建一个DragGesture并将其附加到卡上。在这里我们对拖动手势有用的两个额外的修饰符:移动时运行的onChanged() 和结束拖动时运行的onEnded()

它们都有一个参数,描述了拖动操作——它的开始位置,当前位置,移动距离等等。对于onChanged()修改器,我们将读取拖动的位移,该位移告诉我们拖动距起点有多远——可以直接将其赋值给dragAmount以便视图随手势一起移动。对于onEnded()要完全忽略输入,因为需要将设置dragAmount复位。

因此,现在将此修饰符添加到线性渐变中:

.gesture(
    DragGesture()
        .onChanged { self.dragAmount = $0.translation }
        .onEnded { _ in self.dragAmount = .zero }
)

如果运行代码,您会看到现在可以拖动渐变卡了,放开拖动时,它将跳回到中心。卡的偏移量由dragAmount确定,该偏移量又由拖动手势设置。

现在一切正常,我们可以通过一些动画使该动作栩栩如生,我们有两个选择:添加一个隐式动画以使拖动释放具有动画效果,或者添加一个显式动画以使释放成为动画。

要查看前者的实际效果,请将此修改器添加到线性渐变中:

.animation(.spring())

拖动时,由于弹簧动画的作用,卡会稍有延迟地移到拖动位置,但是如果突然移动,它也会轻轻地过冲。

要看到明确的动画在行动,删除animation()修改和改变现有的onEnded()拖拽手势的代码如下:

.onEnded { _ in
    withAnimation(.spring()) {
        self.dragAmount = .zero
    }
}

现在,这张卡将立即跟随您的拖动(因为没有被动画化),但是当您放开它时,它将进行动画处理。

如果我们将偏移动画与拖动手势并稍加延迟相结合,则无需大量代码就可以创建非常有趣的动画。

为了证明这一点,我们可以将文本“ Hello SwiftUI”编写为一系列单独的字母,每个字母的背景颜色和偏移量都由某个状态控制。使用Array("Hello SwiftUI")可以得到一个字符串数组:每个元素是一个字符。

struct ContentView: View {
    let letters = Array("Hello SwiftUI")
    @State private var enabled = false
    @State private var dragAmount = CGSize.zero

    var body: some View {
        HStack(spacing: 0) {
            ForEach(0..<letters.count) { num in
                Text(String(self.letters[num]))
                    .padding(5)
                    .font(.title)
                    .background(self.enabled ? Color.blue : Color.red)
                    .offset(self.dragAmount)
                    .animation(Animation.default.delay(Double(num) / 20))
            }
        }
        .gesture(
            DragGesture()
                .onChanged { self.dragAmount = $0.translation }
                .onEnded { _ in
                    self.dragAmount = .zero
                    self.enabled.toggle()
                }
        )
    }
}

如果运行该代码,您会发现可以拖动任意字母以使整个字符串都跟随该字符串,只是短暂的延迟会导致类似蛇的效果。当您释放拖动时,SwiftUI还将添加颜色更改,即使字母移回中心也可以在蓝色和红色之间进行动画显示。

3. Showing and hiding views with transitions(使用过渡显示或隐藏视图)

SwiftUI最强大的功能之一是能够自定义视图的显示和隐藏方式。之前,您已经了解了如何使用常规if条件有条件地包含视图,这意味着当条件发生变化时,我们可以从视图层次结构中插入或删除视图。

过渡控制插入和删除的方式,我们可以使用内置过渡,以不同方式组合它们,甚至创建完全自定义的过渡。

为了说明这一点,这里有一个VStack带有按钮和一个矩形的:

struct ContentView: View {
    var body: some View {
        VStack {
            Button("Tap Me") {
                // do nothing
            }

            Rectangle()
                .fill(Color.red)
                .frame(width: 200, height: 200)
        }
    }
}

我们可以使矩形仅在满足特定条件时显示。首先,我们添加一些可以操纵的状态:

@State private var isShowingRed = false

接下来,我们将该状态用作显示矩形的条件:

if isShowingRed {
    Rectangle()
        .fill(Color.red)
        .frame(width: 200, height: 200)
}

最后,我们可以isShowingRed在按钮的操作中在true和false之间切换:

self.isShowingRed.toggle()

如果运行该程序,则会看到按下按钮会显示并隐藏红色方块。没有动画。它只是出现而突然消失。

我们可以使用来包装状态更改withAnimation(),从而获得SwiftUI的默认视图过渡,如下所示:

withAnimation {
    self.isShowingRed.toggle()
}

有了较小的更改,应用程序现在就可以淡入和淡出红色矩形,同时还可以向上移动按钮以腾出空间。看起来不错,但我们可以使用transition()修饰符做得更好。

例如,我们可以通过在矩形上添加transition()修饰符来使矩形放大和缩小:

Rectangle()
    .fill(Color.red)
    .frame(width: 200, height: 200)
    .transition(.scale)

现在点击按钮看起来更好:矩形会随着按钮的腾出而扩大,然后再次点击时会缩小。

如果要尝试,还可以尝试其他几种转换。一个有用的是.asymmetric,它使我们可以在显示视图时使用一个过渡,而在消失时使用另一个过渡。要进行尝试,请使用以下命令替换矩形的现有过渡:

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

4. Building custom transitions using ViewModifier(使用 ViewModifier 创建自定义过渡)

为SwiftUI创建全新的过渡是可能的,而且实际上出乎意料的容易,这使我们可以使用完全自定义的动画添加和删除视图。

.modifier过渡使此功能成为可能,该过渡接受我们想要的任何视图修饰符。要注意的是,我们需要能够实例化修饰符,这意味着它必须是我们自己创建的修饰符。

为了尝试这一点,我们可以编写一个视图修改器,让我们模仿Keynote中的Pivot动画-它使新幻灯片从其左上角旋转入。用SwiftUI讲,这意味着创建一个视图修改器,使我们的视图从一个角旋转,而不会逃脱它应该位于的边界。SwiftUI实际上为我们提供了修改器来做到这一点:rotationEffect()让我们在2D空间中旋转视图,并clipped()阻止将视图绘制到其矩形空间的外部。

rotationEffect()与相似rotation3DEffect(),但它始终绕Z轴旋转。但是,它也使我们能够控制旋转的锚点 -视图的哪一部分应固定在旋转中心。SwiftUI为我们提供了一个UnitPoint用于控制锚,它可以让我们指定确切的X / Y点的许多内置选项旋转或使用一个类型- ,.topLeading.bottomTrailing.center等等。

让我们通过创建一个CornerRotateModifier结构来构造所有代码,这些结构具有一个锚点来控制旋转的位置,并控制一个旋转量:

struct CornerRotateModifier: ViewModifier {
    let amount: Double
    let anchor: UnitPoint

    func body(content: Content) -> some View {
        content.rotationEffect(.degrees(amount), anchor: anchor).clipped()
    }
}

clipped()那里的添加意味着当视图旋转时,不会绘制位于其自然矩形之外的零件。

我们可以使用.modifier过渡直接尝试一下,但这有点笨拙。一个更好的主意是将其包装到的扩展中AnyTransition,使它在其最前端的角从-90旋转到0:

extension AnyTransition {
    static var pivot: AnyTransition {
        .modifier(
            active: CornerRotateModifier(amount: -90, anchor: .topLeading),
            identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
        )
    }
}

有了这个,我们现在可以使用以下方法将透视动画附加到任何视图:

.transition(.pivot)

(大叔注:说实话,3 和 4 的实验效果不理想)

发布了77 篇原创文章 · 获赞 16 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/hh680821/article/details/105381211