visionOS空间计算实战开发教程Day 8 附属视图和动画

本节我们开始初探在visionOS中添加动画效果,我们的入口文件和​​ContentView​​​和​​Day 6​​​中并没有什么区别,所以重点来看​​ViewModel​​​和​​ImmersiveView​​。

首先是​​ViewModel.swift​​文件:

复制代码
import SwiftUI
import RealityKit

class ViewModel: ObservableObject {
    private var contentEntity = Entity()
    
    func setupContentEntity() -> Entity {
        return contentEntity
    }
    
    func getTargetEntity(name: String) -> Entity? {
        return contentEntity.children.first { $0.name == name }
    }
    
    func addCube(name: String, position: SIMD3<Float>, color: UIColor) -> ModelEntity {
        let entity = ModelEntity(
            mesh: .generateBox(size: 0.5, cornerRadius: 0),
            materials: [SimpleMaterial(color: color, isMetallic: false)],
            collisionShape: .generateBox(size: SIMD3<Float>(repeating: 0.5)),
            mass: 0.0
        )
        
        entity.name = name
        entity.position = position
        entity.components.set(InputTargetComponent(allowedInputTypes: .indirect))
        entity.components.set(CollisionComponent(shapes: [ShapeResource.generateBox(size: SIMD3<Float>(repeating: 0.5))], isStatic: true))
        entity.components.set(HoverEffectComponent())
        
        contentEntity.addChild(entity)
        
        return entity
    }
    
    func playAnimation(entity: Entity) {
        let goUp = FromToByAnimation<Transform>(
            name: "goUp",
            from: .init(scale: .init(repeating: 1), translation: entity.position),
            to: .init(scale: .init(repeating: 1), translation: entity.position + .init(x:0 ,y: 0.4, z: 0)),
            duration: 0.2,
            timing: .easeOut,
            bindTarget: .transform
        )
        
        let pause = FromToByAnimation<Transform>(
            name: "pause",
            from: .init(scale: .init(repeating: 1), translation: entity.position + .init(x:0 ,y: 0.4, z: 0)),
            to: .init(scale: .init(repeating: 1), translation: entity.position + .init(x:0 ,y: 0.4, z: 0)),
            duration: 0.1,
            bindTarget: .transform
        )
        
        let goDown = FromToByAnimation<Transform>(
            name: "goDown",
            from: .init(scale: .init(repeating: 1), translation: entity.position + .init(x:0 ,y: 0.4, z: 0)),
            to: .init(scale: .init(repeating: 1), translation: entity.position),
            duration: 0.2,
            timing: .easeOut,
            bindTarget: .transform
        )
        
        let goUpAnimation = try! AnimationResource.generate(with: goUp)
        
        let pauseAnimation = try! AnimationResource.generate(with: pause)
        
        let goDownAnimation = try! AnimationResource.generate(with: goDown)
        
        let animation = try! AnimationResource.sequence(with: [goUpAnimation, pauseAnimation, goDownAnimation])
        
        entity.playAnimation(animation, transitionDuration: 0.5)
    }
}

通过前面的学习我们已经知道​​setupContentEntity​​​用于初始化实体对象,​​getTargetEntity​​​用于根据模型的名称查找到指定模型。​​addCube(name: String, position: SIMD3<Float>, color: UIColor)​​​中包含三个参数,​​name​​​用于指定所创建盒子的名称,​​position​​​用于指定盒子所处的位置,同样是按人所处位置为参照坐标,​​color​​用于指定盒体的颜色。

接下来就是本节的一个重要的知识点了,​​playAnimation​​​用于创建动画效果,传入的参数​​entity​​就是要实现动画效果的模型。

结构体​​FromToByAnimation​​​可用于实现实体对象和场景的动画,方式为逐渐修改参数值。​​from​​​值表示动画属性的起始值,​​to​​​表示动画结束时的属性值,也可以通过​​by​​​值让框架来计算动画结束时的值。​​duration​​​顾名思义是指动画的时长,​​timing​​​参数通过​​AnimationTimingFunction​​结构体中的定时函数进行指定,包含的值有:

  • default:生成默认过渡曲线的定时函数,不指定时此即为默认值。
  • linear:生成线性过渡效果的定时函数。
  • easeIn:生成淡入效果的定时函数。
  • easeOut:生成淡出效果的定时函数。
  • easeInOut:生成淡入淡出效果的定时函数。

​bindTarget​​​参数指定为​​tranform​​时表示动画作用于对象本身。

我们定义了三个动画函数,分别为​​goUp​​​、​​pause​​​和​​goDown​​​,实现在纵坐标上完成上下移动0.4个单位的动画效果。接下来通过这三个动画定义来生成动画资源并指定动画的顺序,最后对实体对象调用​​playAnimation​​​来播放动画,这里的​​transitionDuration​​参数与三个动画函数中用时的总和相同。

接下来是我们的​​ImmersiveView​​:

复制代码
import SwiftUI
import RealityKit

struct ImmersiveView: View {
    @State var model = ViewModel()
    @State var cube1 = ModelEntity()
    @State var cube2 = ModelEntity()
        
    var body: some View {
        RealityView { content, attachments in
            content.add(model.setupContentEntity())
            cube1 = model.addCube(name: "Cube1", position: SIMD3<Float>(x: 1, y: 1, z: -2), color: .red)
            cube2 = model.addCube(name: "Cube2", position: SIMD3<Float>(x: -1, y: 1, z: -2), color: .blue)
            
            if let attachment = attachments.entity(for: "cube1_label") {
                attachment.position = [0, -0.35, 0]
                cube1.addChild(attachment)
            }
            
            if let attachment = attachments.entity(for: "cube2_label") {
                attachment.position = [0, -0.35, 0]
                cube2.addChild(attachment)
            }
        } attachments: {
            Attachment(id: "cube1_label") {
                Text("Cube1")
                    .font(.system(size: 48))
            }
            Attachment(id: "cube2_label") {
                Text("Cube2")
                    .font(.system(size: 48))
            }
        }
        .gesture(
            SpatialTapGesture()
                .targetedToEntity(cube1)
                .onEnded { value in
                    print(value)
                    model.playAnimation(entity: cube1)
                }
        )
        .gesture(
            SpatialTapGesture()
                .targetedToEntity(cube2)
                .onEnded { value in
                    print(value)
                    model.playAnimation(entity: cube2)
                }
        )
    }
}

​cube1​​​为红色,位于我们的右侧,​​cube2​​​为蓝色,位于我们的左侧,为方便标识,我们通过​​RealityView​​​的​​AttachmentContentBuilder​​​(即​​attachments​​​参数)来创建附属视图,但附属视图并不会自动添加到​​RealityView​​​的对象上。我们需要显式地进行指定,这里​​attachments.entity​​​中的​​for​​​与​​Attachment​​​的​​id​​​相对应,我们只添加了两个文本视图,并通过​​position​​指定了相对于父对象的位置。

接着通过​​SpatialTapGesture​​​指定在点击相应盒子时执行前面所定义的​​playAnimation​​。

运行应用,点击​​Show ImmersiveSpace​​会显示两个盒子,盒子下方分别显示一个附属视图,点击盒体会执行上下移动的动画。

示例代码:​​GitHub仓库​

其它相关内容请见​​虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记​

相关推荐
HarderCoder12 小时前
iOS 知识积累第一弹:从 struct 到 APP 生命周期的全景复盘
ios
叽哥1 天前
Flutter Riverpod上手指南
android·flutter·ios
用户092 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan2 天前
iOS26适配指南之UIColor
ios·swift
权咚3 天前
阿权的开发经验小集
git·ios·xcode
用户093 天前
TipKit与CloudKit同步完全指南
ios·swift
法的空间3 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918413 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张3 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview
东坡肘子3 天前
完成 Liquid Glass 的适配了吗?| 肘子的 Swift 周报 #0102
swiftui·swift·apple