从这一课开始,进入到了课程中等难度部分,前面学的内容都相对比较简单,本课主要讲解通过自己定义的视图修改器实现代码可共享的视图效果及动画与过渡效果的区别与原理。视图修改器与动画是息息相关的,过渡效果在视图之间切换时才有的效果,而动画则是当前视图变化时才会有的效果。
一般要实现动画有2个方式,本课中将会在Shape与ViewModifier中应用。当查看Shape的介绍时候:protocol Shape : Animatable, View,形状是可以通过ViewModifier经常变化的。
1、构建一个简单的ViewModifier
首先我们新建一个对卡片进行修改的Cardify.swift文件并写入以下内容:
import SwiftUI
//需要符合ViewModifier
struct Cardify:ViewModifier {
var isFaceUp: Bool
//如果忘记body后的参数可以参考文档里的例子
func body(content: Content) -> some View {
//下面代码从CardView剪切过来的
ZStack{
let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
if isFaceUp{
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: DrawingConstants.lineWidth)
content //这里就是要传入被修改的视图内容
}else{
shape.fill()
}
}
}
//参数控制器也剪切过来
private struct DrawingConstants{
static let cornerRadius: CGFloat = 10//圆角值
static let lineWidth: CGFloat = 3//strokeBorder的线宽
}
}
2、把我们构建的Cardify应该到CardView上:
我们上面创建了自己的ViewModifier(Cardify)后需要在被修改的视图上使用,首先要清除Cardify里已有的修饰代码。得到干净的下面代码:
struct CardView:View {
let card:MemoryGame.Card
var body: some View{
GeometryReader{ geomery in
ZStack{
//这里的判断卡片是否被翻转已被移除,将应用在Cardify里
Pie(startAngle: Angle(degrees: 0-90), endAngle: Angle(degrees: 110-90))
.padding(5).opacity(0.5)
Text(card.content).font(font(in: geomery.size))
}
//引用自己定义的Cardify并传入判断参数
.modifier(Cardify(isFaceUp: card.isFaceUp))
}
}
//返回Emoji的大小设置
private func font(in size:CGSize) -> Font{
Font.system(size: min(size.width,size.height) * DrawingConstants.fontScale)
}
//参数控制器移出Cardify部分
private struct DrawingConstants{
static let fontScale: CGFloat = 0.7//Emoji缩放比例
}
}
尝试编译,一次性通过。通过上面的代码我们看到编写ViewModifier与我们编写View几乎是一样的。只需要理解content是传入被修改的原视图即可。很容易 就理解了。在使用上更为方便快捷。今后的开发中会大量的用到ViewModifier。现在我们使用自动定义的Cardify还需要配合 .modifier(Cardify(isFaceUp: card.isFaceUp)) 这样来用,老师将在后面的代码里通过extension View的办法,去掉.modifier参数。
3、隐藏掉.modifier参数
像上面说的,我们使用扩展的方式隐藏掉.modifier参数,其实并不会减少代码。只是通过扩展的时候 使用了这个参数,在以后的视图修改里就不需要重复的再去使用:
extension View{
//这里的cardify是小写的,返回some View
func cardify(isFaceUp:Bool) -> some View {
//在扩展里使用modifier替换掉在被修改的视图上的
//这里的self还不知道去掉和保留的区别,目前效果都一样
self.modifier(Cardify(isFaceUp: isFaceUp))
//这里的应用的是Cardify(首字大写)我们自己定义的ModifierView
}
}
然后我们在视图修改器里就可以直接使用cardify修饰视图了:
.cardify(isFaceUp: card.isFaceUp)//像.padding()这样使用modifier
隐藏.modifier参数后的cardify是不是像系统内置的修改器一样,老师的课程很多时候都在解释官方框架实现的原理,我们应该像官方代码习惯一样去编码,这样我们的代码会更优秀。
动画效果的前提条件就是视图产生了变化,没有变化就不会有动画,所以变化是动画的前提。
什么样的变化可以被动画化?swiftUI里只有3种情况:ViewModifiers参数、shapes形状和View的存在状态。我们知道通过ViewModifier可以让视图产生变化,所以可以被动画,shape也是会产生变化的,最后的视图存在状态指的是当视图进入或者被移出时其实也是变化的一种。
注:很多时候我们期待的动画并没有发生的时候,大部分情况属于正在变化的数据与视图并未出现在屏幕的视图上,或者随着视图出现在屏幕上而发生的变化,因此它不会被动画化,下面的代码里将通过对数据的处理修复这样的情况。
要见到视图动画效果前提是它必须出现在屏幕上。SwiftUI内置了2种动画的编码方式,根据其特点分为:
1、隐式动画.animation(Animation)
隐式动画有时候也称之为自动动画,.animation类似上面的ViewModifier一样,用于单个视图动画的修饰效果,且不会被下面的显式动画覆盖效果(优先级更高)。其本质是为视图标记动画,处理动画ViewModifier的所有变化参数以动画化。
Text("⚙️")
.opacity(true ? 1 : 0)//透明度
.rotationEffect(Angle.degrees(true ? 0 : 180))//旋转
.animation(.easeInOut)//为上面的ViewModifier应用动画效果
.animation影响的作用域仅限于其上面的ViewModifier,如果写到了它的下面则动画效果会被忽略掉。如果要想根据数据变化产生动画效果则需要使用下面的显式动画。
注意:.animation不适用于容器(container),因为容器会将.animation传播到其子视图中(会被子视图继承)。这像font、foregroudColor,而不像padding。
2、显式动画 withAnimation(Animation){ }
通过监测代码块{ }里的数据变化影响到UI变化而实现的动画效果,这也意味着withAnimation可以在ViewModel里使用。当需要根据用户输入或者其它操作产生动画效果时,我们就需要使用到显式动画来完成。显示动画的原理是创建一个动画事务,并监测代码块{}里的数据发生改变后影响到的ViewModifiers和Shapes的所有符合条件的更改并应用动画。所以显式动画适合批量的、大范围的动画效果。
//持续2秒的均匀(linear)动画效果
withAnimation(.linear(duration: 2)){
isChoose = true //这里放入因数据修改而导致UI变化的数据
}
注:显式动画并不会覆盖隐式动画,因为隐式动画代码距离更近,拥有更高的优先权。而显式动画总结围绕着ViewModel里的Intent(s) 用户意图,因为我们定义的Intent函数们很可能会更改数据导致视图产生变化。
3、动画参数Animation
swiftUI内置的动画属性参数有:default、 ease
关于动画的详细说明请参考:http://www.55mx.com/ios/138.html
4、transitions 过渡效果
过渡和动画应用的场景是不一样的,它们的区别是动画是单个视图产生变化导致的动画效果,而过渡则是两个视图切换间的效果( A视图 -> 过渡效果 ->B视图),类似于视频剪辑里的2个画图衔接时使用的过渡效果。
ZStack{
if isFaceUp{ //通过改变isFaceUp的状态可以看到切换时的过渡效果
Rectangle().stroke()
Text("🐶").transition(AnyTransition.scale)//文本缩放过渡效果
}else{
Rectangle().transition(.identity)//回到原来的样子过渡效果
}
}
在后面的课程中会使用一个AnyTransition的参数应用相对比较复杂的过渡效果。AnyTransition是一个类型化的擦除过渡,AnyTransition.opacity,.scale,.iffset,.modifier(active: identity:)。
注意:与 .animation 不同的是 . transition 不会传播到子视图,只针对当前被修饰的视图与其它视图切换时产生过渡效果,所以上面的代码我们没有放到ZStack上面。
如果要为过渡设置一个过渡动画可以使用下面的代码:
//一个持续20秒的过渡动画效果(线形)
.transition(AnyTransition.opacity.animation(.linear(duration: 20)))
除了视图的显示与隐藏过渡,我们还可以使用.position(位置)四处移动时应用动画的解释视频第30分钟处,解释了不同容器之间怎样实现同一个动画效果(.matchedGeometryEffect(id: ID, in: Namespace))。
5、onAppear{ } 视图出现在屏幕上执行的操作(41:35)
前面我们说过了,动画只能应用在出现在屏幕上的视图,怎样检测视图已经出现在屏幕上就需要使用.onAppear{ }来检测了。其代码块里执行的是,当被修饰的视图出现时要执行的闭包代码。
将在后面几课里演示使用.onAppear实现自动发牌、自动倒计时等效果。
6、动画实际的原理(43:12)
前面讲解了怎么使用动画,和过渡效果,动画系统的本质是占用动画发生的持续时间,然后根据使用的动画时间曲线(ease
下面的演示中会使用Pie的startAngle和endAngle变化信息告诉系统需要被动画化的参数是什么。使用 var animatableData:Type(视频45:10) 实现Shape与ViewModifiers之间的通信(具体使用请看下面将要演示的代码),如果是两组数据导致的动画通信则需要使用AnimatablePair。
1、体验隐式动画.animation
上面对动画做了详细的介绍,我们将在游戏中开始使用动画效果,为了体验隐匿动画,现在我们准备将匹配成功后的卡片内容让其不停的旋转。
Text(card.content)
//匹配后让其旋转360度
.rotationEffect(Angle.degrees(card.isMatched ? 360 : 0))
//应用一个2秒的动画(渐进渐出)效果
.animation(Animation.easeInOut(duration: 2))
.font(font(in: geomery.size))
注意区别的是,上面参数里的Animation首字母是大写的,与animation是不同的参数,它们调用的子参数和功能也是不一样的。
上面代码应用后在切换横坚屏的时候会发现卡片里的内容会飞出卡片以外产生一个动画,并且只有一张卡片里的内容会旋转,现在我们要查找并解决这2个问题。
2、解决飞出屏幕的问题
出现这个问题的原因在于.font(font(in: geomery.size)),由于.font是不能使用动画的,所以我们在使用geomery获取到横坚屏尺寸时(上一课写了根据屏幕适配卡片数据的算法)卡片会稍微的改变大小,导致了字体也随之产生了变化(字体的位置也产生了大的变化)。
而需要解决这个问题的唯一办法就是让 geomery.size 动画化。但是font是不能被动画化的 ,我们需要使用.scaleEffect()来缩放内容大小。scaleEffect接受的是一个比例值,所以我们需要使用一个函数算出来需要缩放的比例是多少:
//字体设置为固定大小后原来的func font(...)-> Font就没用了
.font(Font.system(size: DrawingConstants.fontSize))
//使用scale算出需要缩放的比例
.scaleEffect(scale(thatFits:geomery.size))
/********************************/
//计算出当前字体需要缩放的比例
private func scale(thatFits size:CGSize) -> CGFloat{
min(size.width,size.height) / (DrawingConstants.fontSize / DrawingConstants.fontScale)
}
//字体参数控制
private struct DrawingConstants{
static let fontScale: CGFloat = 0.7//Emoji缩放比例
static let fontSize: CGFloat = 32//固定Emoji的字体大小
}
避开了font不能动画的问题,我们尝试旋转屏幕,已可以差不多显示正常了,还有一点,虽然我们缩放了字体大小,但在动画上并没有体现出来,是因为我们放到了.animation的下面,导致其后面的效果被忽略了,所以隐式动画的放置位置也是比较关键的问题(1:02:50)。现在我们要解决下一个问题:
3、解决只有一个动画问题(1:04:02)
首先找到导动画的原因是card.isMatched的值发生改变后卡片发生了rotationEffect。之所以后面的卡片不旋转,通过对.cardify的追踪发现是因为在屏幕上显示这个视图之前card.isMatched已发生了改变,这就要讲到上面说的动画黄金法则之一,必须是显示在屏幕上而发生的改变才会导致动画效果。
要解决这个问题我们首先要确保当card.isMatched发生的时候,这个Text在屏幕来才能解决这个问题。我们需要修改struct Cardify里的内容:
ZStack{
let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
if isFaceUp{
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: DrawingConstants.lineWidth)
//content //要使2张卡片的动画同样生效必须让其一直显示在屏幕上
}else{
shape.fill()
}
content.opacity(isFaceUp ? 1 : 0)//让其一直显示在屏幕上并根据条件隐藏起来
}
之所以这样实现,由于isFaceUP与isMatched是同时发生的,我们让其一直显示在屏幕上确保了isMatched能触发动画效果(虽然透明度为0,但动画是执行了的)。
本课先是演示了使用自己定义的ViewModifier来修改卡片内容,然后针对动画使用与原理做了大量的介绍,我们需要理解动画使用的各种功能,以达到更加符合用户体验的视觉效果。本课的重点主要是理解通过一步步分析溯源的方式找到动画显示问题及解决问题的思路。本课的代码都比较简单很容易理解,重点之重还是处理解决问题的思路!
除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/152
《CS193p2021学习笔记第七课:ViewModifier视图修改器与Animation动画》的网友评论(0)