|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
扫描动画效果是现代iOS应用中常见的UI元素,它可以用于表示数据加载、内容刷新、搜索过程或仅仅作为视觉装饰。这种动画效果能够吸引用户的注意力,提供视觉反馈,并增强用户体验。在本教程中,我们将学习如何使用Swift编程语言从基础到进阶实现各种炫酷的扫描动画效果。
1. 基础知识
在开始实现扫描动画之前,我们需要了解一些基础知识:
SwiftUI和UIKit
SwiftUI是Apple推出的现代UI框架,它使用声明式语法来构建用户界面。而UIKit是iOS开发的传统UI框架,它使用命令式编程方式。在本教程中,我们将主要使用SwiftUI,因为它更适合创建动画效果,但也会介绍一些UIKit的实现方式。
动画基础
在Swift中,动画通常通过以下方式实现:
• 使用withAnimation函数包装状态变化
• 使用animation修饰符指定动画类型
• 使用Animatable协议创建自定义动画
2. 简单扫描动画实现
让我们从最基础的扫描动画开始。我们将创建一个水平移动的扫描线效果。
使用SwiftUI创建基础扫描效果
- import SwiftUI
- struct BasicScanView: View {
- @State private var scanPosition: CGFloat = -100
- @State private var isScanning = false
-
- var body: some View {
- ZStack {
- // 背景内容
- Rectangle()
- .fill(Color.gray.opacity(0.2))
- .frame(height: 200)
- .cornerRadius(10)
-
- // 扫描线
- Rectangle()
- .fill(
- LinearGradient(
- gradient: Gradient(colors: [Color.clear, Color.blue, Color.clear]),
- startPoint: .leading,
- endPoint: .trailing
- )
- )
- .frame(width: 100, height: 200)
- .offset(x: scanPosition)
- .opacity(isScanning ? 1 : 0)
- }
- .onAppear {
- startScanning()
- }
- .padding()
- }
-
- private func startScanning() {
- isScanning = true
- withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
- scanPosition = UIScreen.main.bounds.width + 100
- }
- }
- }
- struct BasicScanView_Previews: PreviewProvider {
- static var previews: some View {
- BasicScanView()
- }
- }
复制代码
这段代码创建了一个简单的扫描动画效果。我们使用ZStack将背景和扫描线叠加在一起,然后通过动画改变扫描线的offset值,使其从左向右移动。扫描线使用渐变填充,使其看起来更加自然。
添加扫描效果
现在,让我们为扫描线添加一个发光效果,使其看起来更加炫酷:
- import SwiftUI
- struct GlowingScanView: View {
- @State private var scanPosition: CGFloat = -100
- @State private var isScanning = false
-
- var body: some View {
- ZStack {
- // 背景内容
- Rectangle()
- .fill(Color.gray.opacity(0.2))
- .frame(height: 200)
- .cornerRadius(10)
-
- // 扫描线
- Rectangle()
- .fill(
- LinearGradient(
- gradient: Gradient(colors: [Color.clear, Color.blue, Color.clear]),
- startPoint: .leading,
- endPoint: .trailing
- )
- )
- .frame(width: 100, height: 200)
- .offset(x: scanPosition)
- .opacity(isScanning ? 1 : 0)
- .shadow(color: .blue, radius: 20, x: 0, y: 0)
- }
- .onAppear {
- startScanning()
- }
- .padding()
- }
-
- private func startScanning() {
- isScanning = true
- withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
- scanPosition = UIScreen.main.bounds.width + 100
- }
- }
- }
- struct GlowingScanView_Previews: PreviewProvider {
- static var previews: some View {
- GlowingScanView()
- }
- }
复制代码
通过添加.shadow(color: .blue, radius: 20, x: 0, y: 0),我们为扫描线添加了一个蓝色发光效果,使其看起来更加炫酷。
3. 进阶扫描动画
现在我们已经掌握了基础的扫描动画,让我们尝试一些更复杂的效果。
圆形扫描动画
- import SwiftUI
- struct CircularScanView: View {
- @State private var scanProgress: Double = 0
- @State private var isScanning = false
-
- var body: some View {
- ZStack {
- // 背景圆
- Circle()
- .stroke(Color.gray.opacity(0.2), lineWidth: 20)
- .frame(width: 200, height: 200)
-
- // 扫描圆弧
- Circle()
- .trim(from: 0, to: scanProgress)
- .stroke(
- LinearGradient(
- gradient: Gradient(colors: [Color.blue, Color.purple]),
- startPoint: .topLeading,
- endPoint: .bottomTrailing
- ),
- lineWidth: 20
- )
- .frame(width: 200, height: 200)
- .rotationEffect(.degrees(-90))
- .shadow(color: .blue, radius: 10, x: 0, y: 0)
- }
- .onAppear {
- startScanning()
- }
- .padding()
- }
-
- private func startScanning() {
- isScanning = true
- withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
- scanProgress = 1.0
- }
- }
- }
- struct CircularScanView_Previews: PreviewProvider {
- static var previews: some View {
- CircularScanView()
- }
- }
复制代码
这段代码创建了一个圆形扫描动画,通过trim方法控制圆弧的显示进度,并使用渐变色和阴影效果增强视觉效果。
雷达扫描动画
- import SwiftUI
- struct RadarScanView: View {
- @State private var scanAngle: Double = 0
- @State private var isScanning = false
-
- var body: some View {
- ZStack {
- // 背景圆
- Circle()
- .stroke(Color.green.opacity(0.3), lineWidth: 1)
- .frame(width: 300, height: 300)
-
- Circle()
- .stroke(Color.green.opacity(0.3), lineWidth: 1)
- .frame(width: 200, height: 200)
-
- Circle()
- .stroke(Color.green.opacity(0.3), lineWidth: 1)
- .frame(width: 100, height: 100)
-
- // 扫描线
- Rectangle()
- .fill(
- LinearGradient(
- gradient: Gradient(colors: [Color.clear, Color.green.opacity(0.8), Color.clear]),
- startPoint: .leading,
- endPoint: .trailing
- )
- )
- .frame(width: 300, height: 2)
- .rotationEffect(.degrees(scanAngle))
- .shadow(color: .green, radius: 5, x: 0, y: 0)
- }
- .onAppear {
- startScanning()
- }
- .padding()
- }
-
- private func startScanning() {
- isScanning = true
- withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
- scanAngle = 360
- }
- }
- }
- struct RadarScanView_Previews: PreviewProvider {
- static var previews: some View {
- RadarScanView()
- }
- }
复制代码
这段代码创建了一个雷达扫描动画,通过旋转一个带有渐变效果的矩形来模拟雷达扫描的效果。我们还添加了几个同心圆作为背景,增强雷达的感觉。
多方向扫描动画
- import SwiftUI
- struct MultiDirectionalScanView: View {
- @State private var horizontalScanPosition: CGFloat = -100
- @State private var verticalScanPosition: CGFloat = -100
- @State private var isScanning = false
-
- var body: some View {
- ZStack {
- // 背景内容
- Rectangle()
- .fill(Color.gray.opacity(0.2))
- .frame(width: 300, height: 300)
- .cornerRadius(10)
-
- // 水平扫描线
- Rectangle()
- .fill(
- LinearGradient(
- gradient: Gradient(colors: [Color.clear, Color.blue, Color.clear]),
- startPoint: .leading,
- endPoint: .trailing
- )
- )
- .frame(width: 100, height: 300)
- .offset(x: horizontalScanPosition)
- .opacity(isScanning ? 1 : 0)
- .shadow(color: .blue, radius: 10, x: 0, y: 0)
-
- // 垂直扫描线
- Rectangle()
- .fill(
- LinearGradient(
- gradient: Gradient(colors: [Color.clear, Color.red, Color.clear]),
- startPoint: .top,
- endPoint: .bottom
- )
- )
- .frame(width: 300, height: 100)
- .offset(y: verticalScanPosition)
- .opacity(isScanning ? 1 : 0)
- .shadow(color: .red, radius: 10, x: 0, y: 0)
- }
- .onAppear {
- startScanning()
- }
- .padding()
- }
-
- private func startScanning() {
- isScanning = true
-
- // 水平扫描
- withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
- horizontalScanPosition = UIScreen.main.bounds.width + 100
- }
-
- // 垂直扫描
- withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false).delay(1.5)) {
- verticalScanPosition = UIScreen.main.bounds.height + 100
- }
- }
- }
- struct MultiDirectionalScanView_Previews: PreviewProvider {
- static var previews: some View {
- MultiDirectionalScanView()
- }
- }
复制代码
这段代码创建了一个多方向扫描动画,包括水平和垂直两个方向的扫描线。通过使用不同的颜色和延迟启动,我们创建了一个更加复杂的扫描效果。
4. 自定义扫描动画
现在,让我们尝试创建一些独特的自定义扫描动画。
网格扫描动画
- import SwiftUI
- struct GridScanView: View {
- @State private var scanProgress: Double = 0
- @State private var isScanning = false
-
- let gridSize = 10
- let cellSize: CGFloat = 30
-
- var body: some View {
- VStack(spacing: 0) {
- ForEach(0..<gridSize, id: \.self) { row in
- HStack(spacing: 0) {
- ForEach(0..<gridSize, id: \.self) { column in
- Rectangle()
- .fill(cellColor(for: row, column: column))
- .frame(width: cellSize, height: cellSize)
- .border(Color.gray.opacity(0.3), width: 0.5)
- }
- }
- }
- }
- .onAppear {
- startScanning()
- }
- .padding()
- }
-
- private func cellColor(for row: Int, column: Int) -> Color {
- let distance = sqrt(pow(Double(row - gridSize/2), 2) + pow(Double(column - gridSize/2), 2))
- let maxDistance = sqrt(pow(Double(gridSize/2), 2) + pow(Double(gridSize/2), 2))
- let normalizedDistance = distance / maxDistance
-
- if abs(normalizedDistance - scanProgress) < 0.1 {
- return Color.blue
- } else if normalizedDistance < scanProgress {
- return Color.blue.opacity(0.3)
- } else {
- return Color.gray.opacity(0.2)
- }
- }
-
- private func startScanning() {
- isScanning = true
- withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
- scanProgress = 1.0
- }
- }
- }
- struct GridScanView_Previews: PreviewProvider {
- static var previews: some View {
- GridScanView()
- }
- }
复制代码
这段代码创建了一个网格扫描动画,从网格中心向外扩散。每个单元格的颜色根据其与中心的距离和扫描进度来确定,创建了一个波纹扩散的效果。
粒子扫描动画
- import SwiftUI
- struct ParticleScanView: View {
- @State private var particles: [Particle] = []
- @State private var scanPosition: CGFloat = -100
- @State private var isScanning = false
-
- struct Particle: Identifiable {
- let id = UUID()
- var x: CGFloat
- var y: CGFloat
- var size: CGFloat
- var opacity: Double
- var color: Color
- }
-
- let particleCount = 50
-
- var body: some View {
- ZStack {
- // 背景内容
- Rectangle()
- .fill(Color.black)
- .frame(height: 300)
- .cornerRadius(10)
-
- // 粒子
- ForEach(particles) { particle in
- Circle()
- .fill(particle.color)
- .frame(width: particle.size, height: particle.size)
- .position(x: particle.x, y: particle.y)
- .opacity(particle.opacity)
- }
-
- // 扫描线
- Rectangle()
- .fill(
- LinearGradient(
- gradient: Gradient(colors: [Color.clear, Color.white, Color.clear]),
- startPoint: .leading,
- endPoint: .trailing
- )
- )
- .frame(width: 5, height: 300)
- .offset(x: scanPosition)
- .opacity(isScanning ? 1 : 0)
- .shadow(color: .white, radius: 10, x: 0, y: 0)
- }
- .onAppear {
- initializeParticles()
- startScanning()
- }
- .padding()
- }
-
- private func initializeParticles() {
- for _ in 0..<particleCount {
- let particle = Particle(
- x: CGFloat.random(in: 0...UIScreen.main.bounds.width),
- y: CGFloat.random(in: 0...300),
- size: CGFloat.random(in: 2...6),
- opacity: 0.3,
- color: [Color.blue, Color.green, Color.yellow, Color.purple].randomElement() ?? .blue
- )
- particles.append(particle)
- }
- }
-
- private func startScanning() {
- isScanning = true
-
- // 扫描线动画
- withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
- scanPosition = UIScreen.main.bounds.width + 100
- }
-
- // 粒子动画
- Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
- updateParticles()
- }
- }
-
- private func updateParticles() {
- for i in particles.indices {
- // 检查粒子是否在扫描线附近
- if abs(particles[i].x - scanPosition) < 20 {
- // 激活粒子
- particles[i].opacity = 1.0
- particles[i].size = particles[i].size * 1.5
-
- // 添加随机运动
- particles[i].x += CGFloat.random(in: -2...2)
- particles[i].y += CGFloat.random(in: -2...2)
- } else {
- // 逐渐恢复原始状态
- if particles[i].opacity > 0.3 {
- particles[i].opacity -= 0.01
- }
- if particles[i].size > 2 {
- particles[i].size -= 0.05
- }
- }
- }
- }
- }
- struct ParticleScanView_Previews: PreviewProvider {
- static var previews: some View {
- ParticleScanView()
- }
- }
复制代码
这段代码创建了一个粒子扫描动画,当扫描线经过粒子时,粒子会被激活并产生反应。这种效果可以用于表示数据扫描、安全检查等场景。
3D扫描动画
- import SwiftUI
- import SceneKit
- struct ThreeDScanView: UIViewRepresentable {
- @Binding var scanProgress: Double
-
- func makeUIView(context: Context) -> SCNView {
- let scnView = SCNView()
- scnView.scene = createScene()
- scnView.autoenablesDefaultLighting = true
- scnView.allowsCameraControl = true
- scnView.backgroundColor = UIColor.black
-
- return scnView
- }
-
- func updateUIView(_ uiView: SCNView, context: Context) {
- // 更新扫描进度
- if let material = uiView.scene?.rootNode.childNodes.first?.geometry?.firstMaterial {
- material.setValue(scanProgress, forKey: "scanProgress")
- }
- }
-
- private func createScene() -> SCNScene {
- let scene = SCNScene()
-
- // 创建3D对象
- let box = SCNBox(width: 2, height: 2, length: 2, chamferRadius: 0)
-
- // 创建自定义着色器材质
- let material = SCNMaterial()
- material.diffuse.contents = UIColor.blue
- material.specular.contents = UIColor.white
-
- // 添加自定义着色器
- let shader = """
- uniform float scanProgress;
-
- #pragma transparent
- #pragma body
-
- vec2 uv = _surface.diffuseTexcoord;
-
- // 计算扫描位置
- float scanPos = scanProgress * 2.0 - 1.0;
-
- // 计算当前像素与扫描位置的距离
- float dist = abs(uv.y - scanPos);
-
- // 创建扫描线效果
- if (dist < 0.05) {
- _output.color.rgb = mix(_output.color.rgb, vec3(0.0, 1.0, 1.0), 1.0 - dist / 0.05);
- _output.color.a = 1.0;
- }
-
- // 创建扫描过的区域效果
- if (uv.y < scanPos) {
- _output.color.rgb = mix(_output.color.rgb, vec3(0.0, 0.5, 1.0), 0.5);
- _output.color.a = 0.8;
- }
- """
-
- material.shaderModifiers = [.surface: shader]
- material.setValue(0.0, forKey: "scanProgress")
-
- box.materials = [material]
-
- let boxNode = SCNNode(geometry: box)
- scene.rootNode.addChildNode(boxNode)
-
- // 添加相机
- let cameraNode = SCNNode()
- cameraNode.camera = SCNCamera()
- cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
- scene.rootNode.addChildNode(cameraNode)
-
- // 添加旋转动画
- let rotation = SCNAction.rotateBy(x: 0, y: CGFloat.pi * 2, z: 0, duration: 10)
- let repeatRotation = SCNAction.repeatForever(rotation)
- boxNode.runAction(repeatRotation)
-
- return scene
- }
- }
- struct ThreeDScanContainerView: View {
- @State private var scanProgress: Double = 0
- @State private var isScanning = false
-
- var body: some View {
- VStack {
- Text("3D扫描动画")
- .font(.title)
- .padding()
-
- ThreeDScanView(scanProgress: $scanProgress)
- .frame(height: 400)
- .onAppear {
- startScanning()
- }
- }
- }
-
- private func startScanning() {
- isScanning = true
-
- Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
- scanProgress += 0.01
- if scanProgress >= 1.0 {
- scanProgress = 0.0
- }
- }
- }
- }
- struct ThreeDScanView_Previews: PreviewProvider {
- static var previews: some View {
- ThreeDScanContainerView()
- }
- }
复制代码
这段代码创建了一个3D扫描动画,使用SceneKit和自定义着色器来实现。扫描线从上到下移动,扫描过的区域会改变颜色,同时3D对象会缓慢旋转,展示全方位的扫描效果。
5. 性能优化
在实现复杂的扫描动画时,性能优化是非常重要的。以下是一些优化技巧:
使用CADisplayLink而不是Timer
对于需要频繁更新的动画,使用CADisplayLink比Timer更高效:
- import SwiftUI
- struct OptimizedScanView: View {
- @State private var scanPosition: CGFloat = -100
- @State private var displayLink: CADisplayLink?
-
- var body: some View {
- ZStack {
- // 背景内容
- Rectangle()
- .fill(Color.gray.opacity(0.2))
- .frame(height: 200)
- .cornerRadius(10)
-
- // 扫描线
- Rectangle()
- .fill(
- LinearGradient(
- gradient: Gradient(colors: [Color.clear, Color.blue, Color.clear]),
- startPoint: .leading,
- endPoint: .trailing
- )
- )
- .frame(width: 100, height: 200)
- .offset(x: scanPosition)
- .shadow(color: .blue, radius: 10, x: 0, y: 0)
- }
- .onAppear {
- startScanning()
- }
- .onDisappear {
- stopScanning()
- }
- .padding()
- }
-
- private func startScanning() {
- displayLink = CADisplayLink(target: self, selector: #selector(updateScanPosition))
- displayLink?.add(to: .current, forMode: .common)
- }
-
- private func stopScanning() {
- displayLink?.invalidate()
- displayLink = nil
- }
-
- @objc private func updateScanPosition() {
- scanPosition += 5
-
- if scanPosition > UIScreen.main.bounds.width + 100 {
- scanPosition = -100
- }
- }
- }
- struct OptimizedScanView_Previews: PreviewProvider {
- static var previews: some View {
- OptimizedScanView()
- }
- }
复制代码
使用Metal着色器
对于更复杂的动画效果,使用Metal着色器可以显著提高性能:
- import SwiftUI
- import MetalKit
- struct MetalScanView: UIViewRepresentable {
- func makeUIView(context: Context) -> MTKView {
- let mtkView = MTKView()
- mtkView.device = MTLCreateSystemDefaultDevice()
- mtkView.delegate = context.coordinator
- mtkView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)
- mtkView.enableSetNeedsDisplay = true
-
- if let device = mtkView.device {
- context.coordinator.setup(device: device)
- }
-
- return mtkView
- }
-
- func updateUIView(_ uiView: MTKView, context: Context) {
- // 更新视图
- }
-
- func makeCoordinator() -> Coordinator {
- Coordinator()
- }
-
- class Coordinator: NSObject, MTKViewDelegate {
- private var device: MTLDevice!
- private var commandQueue: MTLCommandQueue!
- private var pipelineState: MTLRenderPipelineState!
- private var vertices: [Float] = []
- private var vertexBuffer: MTLBuffer!
- private var scanPosition: Float = -1.0
- private var time: Float = 0.0
-
- func setup(device: MTLDevice) {
- self.device = device
- commandQueue = device.makeCommandQueue()
-
- // 创建顶点数据
- vertices = [
- -1.0, -1.0, 0.0, 1.0,
- 1.0, -1.0, 1.0, 1.0,
- -1.0, 1.0, 0.0, 0.0,
- 1.0, 1.0, 1.0, 0.0
- ]
-
- vertexBuffer = device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Float>.size, options: [])
-
- // 创建渲染管线
- let library = device.makeDefaultLibrary()
- let vertexFunction = library?.makeFunction(name: "vertexShader")
- let fragmentFunction = library?.makeFunction(name: "fragmentShader")
-
- let pipelineDescriptor = MTLRenderPipelineDescriptor()
- pipelineDescriptor.vertexFunction = vertexFunction
- pipelineDescriptor.fragmentFunction = fragmentFunction
- pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
-
- do {
- pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
- } catch {
- print("Failed to create pipeline state: \(error)")
- }
- }
-
- func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
- // 视图大小改变时调用
- }
-
- func draw(in view: MTKView) {
- guard let drawable = view.currentDrawable,
- let renderPassDescriptor = view.currentRenderPassDescriptor,
- let pipelineState = pipelineState else {
- return
- }
-
- time += 0.01
- scanPosition = sin(time) * 0.8
-
- let commandBuffer = commandQueue.makeCommandBuffer()
- let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
-
- renderEncoder?.setRenderPipelineState(pipelineState)
- renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
-
- // 传递扫描位置和时间到着色器
- var scanPos = scanPosition
- var timeVal = time
- renderEncoder?.setFragmentBytes(&scanPos, length: MemoryLayout<Float>.size, index: 0)
- renderEncoder?.setFragmentBytes(&timeVal, length: MemoryLayout<Float>.size, index: 1)
-
- renderEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
- renderEncoder?.endEncoding()
-
- commandBuffer?.present(drawable)
- commandBuffer?.commit()
- }
- }
- }
- // Metal着色器代码
- #if os(iOS)
- #include <metal_stdlib>
- using namespace metal;
- struct VertexOut {
- float4 position [[position]];
- float2 texCoord;
- };
- vertex VertexOut vertexShader(const device packed_float4 *vertexArray [[buffer(0)], uint vertexId [[vertex_id]]]) {
- VertexOut out;
- out.position = float4(vertexArray[vertexId].xy, 0.0, 1.0);
- out.texCoord = vertexArray[vertexId].zw;
- return out;
- }
- fragment float4 fragmentShader(VertexOut in [[stage_in]], const device float &scanPosition [[buffer(0)]], const device float &time [[buffer(1)]]) {
- float2 uv = in.texCoord;
-
- // 创建扫描线效果
- float scanLine = smoothstep(0.02, 0.0, abs(uv.y - scanPosition));
-
- // 创建扫描过的区域效果
- float scannedArea = smoothstep(scanPosition - 0.1, scanPosition, uv.y);
-
- // 创建网格效果
- float grid = 0.0;
- float2 gridUV = uv * 20.0;
- grid += smoothstep(0.98, 1.0, abs(fract(gridUV.x) - 0.5));
- grid += smoothstep(0.98, 1.0, abs(fract(gridUV.y) - 0.5));
-
- // 组合效果
- float3 color = float3(0.0, 0.0, 0.0);
- color = mix(color, float3(0.0, 0.5, 1.0), scannedArea * 0.5);
- color = mix(color, float3(0.0, 1.0, 1.0), scanLine);
- color += float3(0.0, 1.0, 0.5) * grid * 0.2;
-
- return float4(color, 1.0);
- }
- #endif
- struct MetalScanContainerView: View {
- var body: some View {
- VStack {
- Text("Metal扫描动画")
- .font(.title)
- .padding()
-
- MetalScanView()
- .frame(height: 400)
- }
- }
- }
- struct MetalScanView_Previews: PreviewProvider {
- static var previews: some View {
- MetalScanContainerView()
- }
- }
复制代码
这段代码使用Metal和着色器创建了一个高效的扫描动画效果。通过在GPU上执行渲染逻辑,我们可以实现更复杂的视觉效果,同时保持高性能。
6. 实际应用案例
现在,让我们看看如何将这些扫描动画应用到实际的应用场景中。
文件扫描应用
- import SwiftUI
- struct DocumentScannerView: View {
- @State private var scanProgress: Double = 0
- @State private var isScanning = false
- @State private var scannedDocument: UIImage?
- @State private var showResults = false
-
- var body: some View {
- VStack {
- Text("文档扫描")
- .font(.largeTitle)
- .padding()
-
- ZStack {
- // 相机预览区域
- Rectangle()
- .fill(Color.black)
- .frame(height: 400)
- .cornerRadius(10)
- .overlay(
- RoundedRectangle(cornerRadius: 10)
- .stroke(Color.white, lineWidth: 2)
- )
-
- // 扫描线
- if isScanning {
- Rectangle()
- .fill(
- LinearGradient(
- gradient: Gradient(colors: [Color.clear, Color.green, Color.clear]),
- startPoint: .leading,
- endPoint: .trailing
- )
- )
- .frame(width: UIScreen.main.bounds.width, height: 2)
- .offset(y: scanPosition)
- .shadow(color: .green, radius: 10, x: 0, y: 0)
-
- // 扫描进度指示器
- VStack {
- Spacer()
- ProgressView(value: scanProgress)
- .progressViewStyle(LinearProgressViewStyle(tint: .green))
- .padding()
- }
- }
-
- // 扫描结果
- if showResults, let image = scannedDocument {
- Image(uiImage: image)
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(height: 400)
- .cornerRadius(10)
- }
- }
- .padding()
-
- HStack {
- Button(action: {
- if isScanning {
- stopScanning()
- } else {
- startScanning()
- }
- }) {
- Text(isScanning ? "停止扫描" : "开始扫描")
- .font(.headline)
- .padding()
- .background(isScanning ? Color.red : Color.green)
- .foregroundColor(.white)
- .cornerRadius(10)
- }
-
- Spacer()
-
- Button(action: {
- saveDocument()
- }) {
- Text("保存文档")
- .font(.headline)
- .padding()
- .background(Color.blue)
- .foregroundColor(.white)
- .cornerRadius(10)
- }
- .disabled(scannedDocument == nil)
- }
- .padding()
- }
- }
-
- private var scanPosition: CGFloat {
- let progress = CGFloat(scanProgress)
- return -200 + (400 * progress)
- }
-
- private func startScanning() {
- isScanning = true
- showResults = false
-
- withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
- scanProgress = 1.0
- }
-
- // 模拟扫描完成
- DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
- completeScan()
- }
- }
-
- private func stopScanning() {
- isScanning = false
- withAnimation {
- scanProgress = 0
- }
- }
-
- private func completeScan() {
- isScanning = false
-
- // 在实际应用中,这里会处理扫描的图像
- // 这里我们使用一个示例图像
- scannedDocument = UIImage(systemName: "doc.fill")
-
- withAnimation {
- showResults = true
- }
- }
-
- private func saveDocument() {
- // 在实际应用中,这里会保存扫描的文档
- print("文档已保存")
- }
- }
- struct DocumentScannerView_Previews: PreviewProvider {
- static var previews: some View {
- DocumentScannerView()
- }
- }
复制代码
这段代码创建了一个文档扫描应用的界面,包含扫描动画、进度指示器和结果展示。这种界面可以用于实际的文档扫描应用,如扫描纸质文件、收据等。
安全验证应用
- import SwiftUI
- import LocalAuthentication
- struct SecurityScanView: View {
- @State private var scanProgress: Double = 0
- @State private var isScanning = false
- @State private var isAuthenticated = false
- @State private var showAlert = false
- @State private var alertMessage = ""
-
- var body: some View {
- VStack {
- Text("安全验证")
- .font(.largeTitle)
- .padding()
-
- ZStack {
- // 指纹图标
- Image(systemName: "touchid")
- .font(.system(size: 100))
- .foregroundColor(isAuthenticated ? .green : .gray)
- .scaleEffect(isAuthenticated ? 1.2 : 1.0)
- .animation(.spring())
-
- // 扫描圆环
- if isScanning {
- Circle()
- .trim(from: 0, to: scanProgress)
- .stroke(
- LinearGradient(
- gradient: Gradient(colors: [Color.blue, Color.purple]),
- startPoint: .topLeading,
- endPoint: .bottomTrailing
- ),
- lineWidth: 5
- )
- .frame(width: 200, height: 200)
- .rotationEffect(.degrees(-90))
- .shadow(color: .blue, radius: 10, x: 0, y: 0)
- }
- }
- .frame(height: 300)
- .padding()
-
- Text(isAuthenticated ? "验证成功" : "请进行身份验证")
- .font(.title2)
- .foregroundColor(isAuthenticated ? .green : .primary)
-
- Spacer()
-
- Button(action: {
- authenticateUser()
- }) {
- Text("开始验证")
- .font(.headline)
- .padding()
- .frame(maxWidth: .infinity)
- .background(Color.blue)
- .foregroundColor(.white)
- .cornerRadius(10)
- }
- .padding()
- .disabled(isScanning || isAuthenticated)
- }
- .alert(isPresented: $showAlert) {
- Alert(title: Text("验证结果"), message: Text(alertMessage), dismissButton: .default(Text("确定")))
- }
- }
-
- private func authenticateUser() {
- isScanning = true
-
- // 启动扫描动画
- withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
- scanProgress = 1.0
- }
-
- // 使用生物识别验证
- let context = LAContext()
- var error: NSError?
-
- if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
- let reason = "请使用Touch ID进行身份验证"
-
- context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
- DispatchQueue.main.async {
- self.isScanning = false
-
- if success {
- self.isAuthenticated = true
- self.alertMessage = "身份验证成功!"
- self.showAlert = true
- } else {
- self.alertMessage = "身份验证失败,请重试。"
- self.showAlert = true
- }
-
- // 重置扫描进度
- self.scanProgress = 0
- }
- }
- } else {
- // 无法使用生物识别
- DispatchQueue.main.async {
- self.isScanning = false
- self.scanProgress = 0
- self.alertMessage = "此设备不支持生物识别。"
- self.showAlert = true
- }
- }
- }
- }
- struct SecurityScanView_Previews: PreviewProvider {
- static var previews: some View {
- SecurityScanView()
- }
- }
复制代码
这段代码创建了一个安全验证应用的界面,使用扫描动画来表示生物识别验证的过程。这种界面可以用于需要安全验证的应用,如银行应用、密码管理器等。
数据加载应用
- import SwiftUI
- struct DataLoadingScanView: View {
- @State private var scanProgress: Double = 0
- @State private var isLoading = false
- @State private var dataItems: [DataItem] = []
- @State private var showData = false
-
- struct DataItem: Identifiable {
- let id = UUID()
- let title: String
- let subtitle: String
- let icon: String
- }
-
- var body: some View {
- VStack {
- Text("数据加载")
- .font(.largeTitle)
- .padding()
-
- ZStack {
- // 数据加载区域
- RoundedRectangle(cornerRadius: 10)
- .fill(Color.gray.opacity(0.2))
- .frame(height: 400)
-
- // 扫描线
- if isLoading {
- VStack(spacing: 0) {
- ForEach(0..<5, id: \.self) { index in
- Rectangle()
- .fill(
- LinearGradient(
- gradient: Gradient(colors: [Color.clear, Color.blue, Color.clear]),
- startPoint: .leading,
- endPoint: .trailing
- )
- )
- .frame(height: 80)
- .offset(x: scanPosition)
- .shadow(color: .blue, radius: 5, x: 0, y: 0)
- .opacity(scanOpacity(for: index))
- }
- }
- }
-
- // 加载的数据
- if showData {
- ScrollView {
- LazyVStack(spacing: 15) {
- ForEach(dataItems) { item in
- HStack {
- Image(systemName: item.icon)
- .font(.title)
- .foregroundColor(.blue)
- .frame(width: 40)
-
- VStack(alignment: .leading) {
- Text(item.title)
- .font(.headline)
-
- Text(item.subtitle)
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
-
- Spacer()
- }
- .padding()
- .background(Color.white)
- .cornerRadius(10)
- .shadow(radius: 2)
- }
- }
- .padding()
- }
- }
- }
- .padding()
-
- HStack {
- Button(action: {
- if isLoading {
- stopLoading()
- } else {
- startLoading()
- }
- }) {
- Text(isLoading ? "停止加载" : "开始加载")
- .font(.headline)
- .padding()
- .background(isLoading ? Color.red : Color.green)
- .foregroundColor(.white)
- .cornerRadius(10)
- }
-
- Spacer()
-
- Button(action: {
- refreshData()
- }) {
- Text("刷新数据")
- .font(.headline)
- .padding()
- .background(Color.blue)
- .foregroundColor(.white)
- .cornerRadius(10)
- }
- .disabled(isLoading || dataItems.isEmpty)
- }
- .padding()
- }
- }
-
- private var scanPosition: CGFloat {
- let progress = CGFloat(scanProgress)
- return -UIScreen.main.bounds.width + (UIScreen.main.bounds.width * 2 * progress)
- }
-
- private func scanOpacity(for index: Int) -> Double {
- let progress = scanProgress
- let segmentProgress = progress * 5
- let segmentIndex = Int(segmentProgress)
- let segmentOffset = segmentProgress - Double(segmentIndex)
-
- if segmentIndex == index {
- return 1.0 - segmentOffset
- } else if segmentIndex == index - 1 {
- return segmentOffset
- } else {
- return 0.0
- }
- }
-
- private func startLoading() {
- isLoading = true
- showData = false
- dataItems = []
-
- withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
- scanProgress = 1.0
- }
-
- // 模拟数据加载
- DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
- completeLoading()
- }
- }
-
- private func stopLoading() {
- isLoading = false
- withAnimation {
- scanProgress = 0
- }
- }
-
- private func completeLoading() {
- isLoading = false
-
- // 生成示例数据
- dataItems = [
- DataItem(title: "项目 1", subtitle: "这是第一个项目的描述", icon: "doc.fill"),
- DataItem(title: "项目 2", subtitle: "这是第二个项目的描述", icon: "folder.fill"),
- DataItem(title: "项目 3", subtitle: "这是第三个项目的描述", icon: "star.fill"),
- DataItem(title: "项目 4", subtitle: "这是第四个项目的描述", icon: "heart.fill"),
- DataItem(title: "项目 5", subtitle: "这是第五个项目的描述", icon: "flag.fill")
- ]
-
- withAnimation {
- showData = true
- }
- }
-
- private func refreshData() {
- showData = false
-
- // 模拟数据刷新
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
- // 重新生成示例数据
- dataItems = [
- DataItem(title: "新项目 1", subtitle: "这是第一个新项目的描述", icon: "doc.fill"),
- DataItem(title: "新项目 2", subtitle: "这是第二个新项目的描述", icon: "folder.fill"),
- DataItem(title: "新项目 3", subtitle: "这是第三个新项目的描述", icon: "star.fill"),
- DataItem(title: "新项目 4", subtitle: "这是第四个新项目的描述", icon: "heart.fill"),
- DataItem(title: "新项目 5", subtitle: "这是第五个新项目的描述", icon: "flag.fill")
- ]
-
- withAnimation {
- showData = true
- }
- }
- }
- }
- struct DataLoadingScanView_Previews: PreviewProvider {
- static var previews: some View {
- DataLoadingScanView()
- }
- }
复制代码
这段代码创建了一个数据加载应用的界面,使用多条扫描线来表示数据加载的过程。这种界面可以用于需要从网络或数据库加载数据的应用,如新闻应用、社交媒体应用等。
7. 总结与展望
在本教程中,我们学习了如何使用Swift编程语言实现各种炫酷的扫描动画效果,从基础的线性扫描到复杂的3D扫描。我们探讨了如何使用SwiftUI、UIKit、SceneKit和Metal等技术来创建这些效果,并讨论了性能优化的方法。
扫描动画效果不仅可以增强应用的视觉吸引力,还可以提供有用的反馈,表示系统正在处理数据或执行任务。通过将这些动画应用到实际的应用场景中,如文档扫描、安全验证和数据加载,我们可以创建更加生动和直观的用户界面。
随着iOS开发技术的不断发展,我们可以期待更多新的动画技术和工具的出现。例如,SwiftUI的不断更新可能会带来更多简洁而强大的动画API,而Metal的持续发展将使我们能够创建更加复杂和高效的图形效果。
无论你是初学者还是有经验的开发者,掌握这些扫描动画技术都将有助于你创建更加吸引人的iOS应用。希望本教程能够为你提供有用的指导和灵感,让你在iOS开发的道路上取得更大的成功。
现在,你已经掌握了使用Swift实现炫酷扫描动画效果的知识,可以开始将这些技术应用到自己的项目中,创造出令人印象深刻的用户界面了!
版权声明
1、转载或引用本网站内容(使用Swift编程语言实现炫酷扫描动画效果的完整教程从基础到进阶让你的iOS应用界面更具吸引力)须注明原网址及作者(威震华夏关云长),并标明本网站网址(https://pixtech.org/)。
2、对于不当转载或引用本网站内容而引起的民事纷争、行政处理或其他损失,本网站不承担责任。
3、对不遵守本声明或其他违法、恶意使用本网站内容者,本网站保留追究其法律责任的权利。
本文地址: https://pixtech.org/thread-36719-1-1.html
|
|