Numeric Property:动画的生成方式

在上一节中,我们通过使用 .animate() 修饰符和修改 changed 变量来动态改变小球的「偏移量」,从而创造出动画效果。实际上,SwiftUI 提供了众多类似的数值型属性,它们可以被用来控制和自定义各种动态交互和视觉表现。本节将详细介绍一些常见且极具实用性的属性,以帮助开发者充分利用 SwiftUI 的强大功能,创造出更加丰富和流畅的用户体验。

偏移量(Offset)

怎么「理解」偏移量?

每个 SwiftUI 视图在其父视图中都有一个「原始位置」,这个位置是根据布局系统(如堆栈、网格、对齐等)决定的。

当应用一个偏移量到一个视图上时,相当于是在告诉 SwiftUI:在渲染这个视图时,应该在其「原始位置的基础上」,在屏幕上向「指定方向」移动「指定的距离」。例如:

swift 复制代码
Text("Hello, World!")
    .offset(x: 20, y: 50)

在这个例子中,无论文本的原始位置在哪里,都会在屏幕上向右移动 20 个单位,向下移动 50 个单位。这种移动是视觉上的,不影响文本在布局系统中的「逻辑」位置。

重要的是要理解,偏移量的应用不会改变视图在其父视图中的布局属性。也就是说,尽管视觉上文本被移动了,它仍然「占据」着原来的空间位置。这意味着其他视图的布局不会因为这个视图的偏移而受到影响。

理解了这点后,再使用偏移动画时,可以放心大胆的使用,因为无论当前视图怎么偏移都不会影响到其它的视图布局。

除了offset(x:y:)的方法外,还有一种offset(_ offset:CGSize)方法可实现位置偏移。

swift 复制代码
 Circle()
	.fill(.blue)
	.frame(width: 100)
	.offset(CGSize(width: changed ? 125 : -125, height: changed ? 0 : -500))
	.animation(.easeIn, value: changed)

这两者在使用上是等价的,「区别」就是CGSize可以作为一个单一的数据结构处理,而offset(x:y:) 更直观。

框架(Frame)

在 SwiftUI 中,frame 修饰符用来指定视图的「尺寸」和子视图的「对齐」。所以它在动画中的使用,也分为两种场景:

1. 设置尺寸

frame 最常见的用途是定义视图的「宽度」和「高度」。当你为视图设置一个 frame,你可以指定宽度(width)、高度(height),或者两者都指定。如果不指定,视图将尽可能地适应其内容或填满可用空间,当然这取决于其父视图的布局特性。

如下例,通过改变高和宽能够实现一个放大和缩小的动画效果:

swift 复制代码
 Rectangle()
	.fill(.blue)
	.frame(width: changed ? 300 : 100, height: changed ? 400 : 150)
	.animation(.easeIn, value: changed)

2. 对齐方式

frame当用来设置容器视图的尺寸时,其中的子视图默认居中展示,一般子视图的尺寸会小于父容器的尺寸,所以改变子视图在其中的「对齐方式」可以间接实现视图的「移动」效果。

如下例所示:

swift 复制代码
VStack {
	Circle()
		.fill(.blue)
		.frame(width: 100)
		.animation(.easeIn, value: changed)
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: changed ? .topLeading : .center)

位置(Position)

position指定当前视图「中心」的横纵坐标(x,y)时,是基于其所在的「父容器」进行计算的,如下例圆心的位置正位于父容器(红色矩形)的左上角(x:0,y:0)

offset类似,它同样可用于移动视图,而且移动视图后不会影响到其它「兄弟」视图的布局,如下例:

如果只需要移动当前视图,用offset最为简单直观;如果需要依据父容器的大小或在父容器中的位置来判断移动带来的影响,那最好用position,如下例:

整个浅蓝色背景是「父容器」,当将小球向下移动,如果纵坐标「超过 400」,则小球变为红色并放大 3 倍;否则当纵坐标小于 400,小球变为绿色并恢复原始大小。

GeometryReader

很多时候我们并不关注具体的坐标值,因为在不同的平台(iPhone、iPad 或 Mac)上,由于设备尺寸的不同,所以更需要一个相对的位置或坐标,比「居中」、「左下角」、「底部」等。

SwiftUI 提供了一个容器视图 GeometryReader,它的构造器参数 「GeometryProxy」 可以用来读取其父视图的「尺寸」和「位置」信息。这里面方法很多,不一一列举,只说一下与position配合常用的两个方法:

  • size用来获取父容器的尺寸,比如宽(width)或高(height)。
  • frame用来获取父容器的「边界」的坐标,比如minXmidXmaxY等。

两者某种情况下是等价的,比如「宽」等价于「maxX」,「宽」的一半就是「midX」等等。 如下例所示,两种方法都可以获取到父容器中心位置的坐标,黑球和白球都位于父容器「ZStack」(橘色背景)的中心位置:

swift 复制代码
 ZStack {
	GeometryReader { geometry in
		Circle().fill(.black)
			.frame(width: geometry.size.width / 2, height: 75)
			.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
	}
	GeometryReader { geometry in
		Circle().fill(.white)
			.frame(width: geometry.frame(in: .local).midX, height: 50)
			.position(x: geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY)
	}
}.background(.orange)

利用这种方式,前例中当小球纵坐标大于「400」(预估的一个中间值),就可以使用「midY」来代替,这样在不同设备、不同方向(横屏或竖屏)都能完美兼容。

颜色(Color)

Color在内部使用数值来定义颜色的各个组成部分,如红、绿、蓝和透明度(RGBA)或其他颜色模型。这些数值可以在动画中平滑地过渡,使得 Color 可以被动画化。 比如「红色」转变为「绿色」时,是穿插了一些列的「插值」,然后在一定「间隔」时间内,缓缓从起始切换到终止(起始 -> 插值 1 -> 插值 2 -> ...插值n -> 终止)。

如下例,当点击「红色」时,背景由「红色」转变为「绿色」,产生一个渐变的动画效果:

swift 复制代码
View
	.foregroundStyle(changed ? .green : .red)
	.animation(.easeIn, value: changed)	

应用颜色的修饰符,最常用的就是前景色(foregroundStyle)、背景色(background)、主题色(tint),如下例:

swift 复制代码
Text("foregroundStyle")
	.foregroundStyle(changed ? .green : .red)

Text("background")
	.foregroundStyle(.white)
	.padding(5)
	.background(changed ? .green : .red)

Toggle(isOn: .constant(true), label: {
	Text("tint")
}).tint(changed ? .green : .red)

「前景色」、「背景色」都可以响应颜色渐变的动画,「主题色」则不支持动画,所以如果想要设置某个视图的主题色,比如「按钮」、「开关」等,可以通过重写其Style方法来实现,如下例:

swift 复制代码
Toggle(isOn: $changed, label: {
	Text("开关")
})
.toggleStyle(MyToggle()) //重写 ToggleStyle

struct MyToggle: ToggleStyle {
    func makeBody(configuration: Configuration) -> some View {
        Circle()
	        .fill(configuration.isOn ? .green : .red)//改变颜色
            .frame(width: 100)
            .overlay(content: {
                configuration.label.foregroundStyle(.white)
            })
        .animation(.easeIn, value: configuration.isOn)//增加动画
    }
}

透明度(Opacity)

opacity 控制视图的透明度,取值范围从 0.0(完全透明)到 1.0(完全不透明)。在动画中,可以用其来实现视图的渐进渐出效果,如下例:

swift 复制代码
View.opacity(changed ? 1 : 0)

虽然视觉上不可见了,但其依然保持空间的占用,不会影响到「兄弟」视图的布局,比如下例中,「红色」不会因为「绿色」的消失而「浮动」(上浮)。

所以,如果想要实现类似于弹窗、模态窗、提示窗口等视图,可以使用ZStack容器或者.overlay修饰符来实现。

旋转(RotationEffect)

.rotationEffect 修饰符允许你对视图进行旋转变换,比如实现一个旋转的按钮:

swift 复制代码
View
	.rotationEffect(.degrees(tapped ? 45 : 0))
	.animation(.easeIn, value: tapped)

再结合「偏移量」、「颜色」、「透明度」实现一个多功能按钮:

swift 复制代码
ZStack {
	//「图片按钮」
	Circle().stroke(.blue, lineWidth: 2.5).frame(width: 70)
		.overlay {
			Image(systemName: "photo")
				.font(.system(size: 36))
				.bold()
				.foregroundStyle(.blue)
		}
		//向左移动 100
		.offset(x: tapped ? -100 : 0)
		//逆时针旋转 90 度
		.rotationEffect(.degrees(tapped ? 0 : -90))
		//渐进
		.opacity(tapped ? 1 : 0)
	
	
	//「视频按钮」
	// ...
		//向左移动 100,向上移动 100
		.offset(x: tapped ? -100 : 0, y: tapped ? -100 : 0)
		//渐进
		.opacity(tapped ? 1 : 0)
	
	
	//「语音按钮」
	// ...
		//向上移动 100
		.offset(y: tapped ? -100 : 0)
		//顺时针旋转 90 度
		.rotationEffect(.degrees(tapped ? 0 : 90))
		//渐进
		.opacity(tapped ? 1 : 0)
	
	//「多功能按钮」
	Button(action: {
		// tapped false => true
		tapped.toggle()
	}, label: {
		//红色变为蓝色
		Circle().fill(tapped ? .red : .blue).frame(width: 75)
			.overlay {
				Image(systemName: "plus")
					.font(.system(size: 48))
					.bold()
					.foregroundStyle(.white)
			}
	})
	//顺时针旋转 45 度
	.rotationEffect(.degrees(tapped ? 45 : 0))
}

三维旋转(Rotation3DEffect)

使用 rotation3DEffect 可以为视图添加立体旋转效果,这种效果特别适合制作翻转动画。如下例:

swift 复制代码
View
	.rotation3DEffect(
		.degrees(changed ? 0 : 180),axis: (x: 0.0, y: 1.0, z: 0.0)
	)
	.animation(.easeIn, value: changed)

缩放(ScaleEffect)

scaleEffect 用于改变视图的尺寸,当结合动画使用时,可以创造出视图缩放进入或退出屏幕的动态效果。如下例:

swift 复制代码
View
	.scaleEffect(changed ? 1 : 0)
	.animation(.easeIn, value: changed)

scaleEffectframe都可以实现视图的缩放效果,区别在于frame缩放后会影响兄弟视图的布局,如下例:

字体(Font)

动画化字体大小变化是一个很好的方式来吸引用户的注意或者提供动态的视觉反馈。如下例:

swift 复制代码
Text("Hello, SwiftUI")
	.font(changed ? .largeTitle : .title3)
	.animation(.easeIn, value: changed)

仔细观察会发现,字体在放大的过程中有些诡异的左右抖动,这是因为字体的缩放过程中,也会缩放它占用的空间,换句话说,它的缩放也会影响到其它兄弟视图的布局,所以解决的办法就是预先设置好它的frame尺寸(能装得下最大的字体)。

swift 复制代码
Text("Hello, SwiftUI")
	.font(changed ? .largeTitle : .title3)
	//提前告知 SwiftUI 它的尺寸
	.frame(width: 200, height: 100)
	.animation(.easeIn, value: changed)

字体权重也可以动画化,如下例:

swift 复制代码
Text("Hello, SwiftUI")
	.fontWeight(changed ? .black : .ultraLight)

字体类型

swift 复制代码
Text("Hello, SwiftUI")
	.font(.custom(changed ? "Academy Engraved LET" : "Papyrus", size: 36))

字体风格

swift 复制代码
Text("Hello, SwiftUI")
	.fontDesign(changed ? .rounded : .monospaced)

裁剪(Trim) trim 修饰符通常与 Shape 结构体一起使用,比如 Circle, Rectangle, Path 等,它的功能是根据起始和结束点来截取形状的一部分。所以可以动画化这些起始和结束点的参数来创造出动态的效果,例如一个进度条。

swift 复制代码
Circle()
	.trim(from: 0, to: progress)
	.stroke(.blue, lineWidth: 25)
	.animation(.linear(duration: 2), value: progress)

网格布局(Grid)

当在网格布局中,根据特定条件显示或隐藏某个网格视图,也可以增加一些动画效果,例如实现一个帷幕。

swift 复制代码
Grid {
	GridRow {
		if changed {
			Color.blue
		}
		Color.green
		if changed {
			Color.red
		}
	}
}.animation(.easeIn, value: changed)
相关推荐
万兴丶5 分钟前
Unnity IOS安卓启动黑屏加图(底图+Logo gif也行)
android·unity·ios
2401_852403554 小时前
探索iPhone一键删除重复照片的方法
ios·iphone
pf_data1 天前
手机换新,怎么把旧iPhone手机数据传输至新iPhone16手机
ios·智能手机·iphone
键盘敲没电1 天前
【iOS】KVC
ios·objective-c·xcode
吾吾伊伊,野鸭惊啼1 天前
2024最新!!!iOS高级面试题,全!(二)
ios
吾吾伊伊,野鸭惊啼1 天前
2024最新!!!iOS高级面试题,全!(一)
ios
不会敲代码的VanGogh1 天前
【iOS】——应用启动流程
macos·ios·objective-c·cocoa
Swift社区2 天前
Apple 新品发布会亮点有哪些 | Swift 周报 issue 61
ios·swiftui·swift
逻辑克2 天前
使用 MultipeerConnectivity 在 iOS 中实现近场无线数据传输
ios
dnekmihfbnmv2 天前
好用的电容笔有哪些推荐一下?年度最值得推荐五款电容笔分享!
ios·电脑·ipad·平板