WatchOS中SpriteKit复刻Apple Workout Rings的技术难题求助
我之前在做类似的WatchOS SpriteKit项目时也踩过这些坑,给你分享几个实用的解决方案:
一、解决SKShader导致线帽失效的问题
当你给SKShapeNode附加SKShader后,默认的lineCap和lineJoin设置会被Shader的自定义渲染覆盖——因为Shader直接操作片段着色器,不会继承ShapeNode的这些属性。这里有两个可行的思路:
1. 在Shader中手动实现圆角线帽
你可以在Shader里计算像素距离,给线条的两端和转角添加圆角效果。下面是一个针对环形进度的片段Shader示例,同时实现渐变和圆角:
void main() { // 获取当前点到圆心的相对坐标 vec2 center = vec2(0.5, 0.5); float distanceFromCenter = length(v_tex_coord - center); // 定义环形的内外半径 float innerRadius = 0.3; float outerRadius = 0.45; float halfLineWidth = (outerRadius - innerRadius) / 2.0; float midRadius = innerRadius + halfLineWidth; // 计算进度遮罩(u_progress为外部传入的0-1进度值) float angle = atan(v_tex_coord.y - center.y, v_tex_coord.x - center.x); float normalizedAngle = (angle + M_PI) / (2.0 * M_PI); float progressMask = step(normalizedAngle, u_progress); // 实现圆角线帽:判断像素到环形中心线的距离,平滑过渡 float distanceToMidline = abs(distanceFromCenter - midRadius); float roundedMask = smoothstep(halfLineWidth, halfLineWidth - 0.02, distanceToMidline); // 组合渐变颜色(这里用角度控制从红到蓝的渐变) vec3 color = mix(vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0), normalizedAngle); gl_FragColor = vec4(color, progressMask * roundedMask); }
Swift侧代码配置:
let ringShader = SKShader(fileNamed: "RingShader.fsh") ringShader.uniforms = [SKUniform(name: "u_progress", float: 0.0)] let ringNode = SKShapeNode(ellipseOf: CGSize(width: 200, height: 200)) ringNode.fillColor = .clear ringNode.strokeColor = .white // 颜色会被Shader覆盖,仅作占位 ringNode.lineWidth = 30 ringNode.shader = ringShader
这个方法完全用Shader控制渲染,同时实现渐变和圆角,不依赖ShapeNode的默认线帽属性。
2. 用SKCropNode结合双节点实现
如果你不想写复杂的Shader,可以用「基础圆角环+遮罩裁剪」的方式:
- 第一个
SKShapeNode:绘制完整的圆角环形(提前设置lineCap = .round),通过SKEffectNode添加渐变滤镜。 - 第二个
SKShapeNode:绘制扇形遮罩,用来裁剪出进度部分。 - 将基础环放入
SKCropNode,用扇形节点作为遮罩。
示例代码:
// 1. 创建带圆角的基础环形 let baseRing = SKShapeNode(ellipseOf: CGSize(width: 200, height: 200)) baseRing.fillColor = .clear baseRing.strokeColor = .red baseRing.lineWidth = 30 baseRing.lineCap = .round baseRing.lineJoin = .round // 2. 给基础环添加渐变效果 let gradientFilter = CIFilter(name: "CILinearGradient")! gradientFilter.setValue(CIVector(x: 0, y: 0), forKey: "inputPoint0") gradientFilter.setValue(CIVector(x: 200, y: 200), forKey: "inputPoint1") gradientFilter.setValue(CIColor(red: 1, green: 0, blue: 0), forKey: "inputColor0") gradientFilter.setValue(CIColor(red: 0, green: 1, blue: 0), forKey: "inputColor1") let effectNode = SKEffectNode() effectNode.filter = gradientFilter effectNode.addChild(baseRing) // 3. 创建进度遮罩(扇形路径) let progress: CGFloat = 0.7 let path = UIBezierPath() path.move(to: CGPoint(x: 100, y: 100)) path.addArc(withCenter: CGPoint(x: 100, y: 100), radius: 115, startAngle: -CGFloat.pi/2, endAngle: -CGFloat.pi/2 + CGFloat.pi * 2 * progress, clockwise: false) path.close() let maskNode = SKShapeNode(path: path.cgPath) maskNode.fillColor = .white maskNode.strokeColor = .clear // 4. 裁剪出进度环 let cropNode = SKCropNode() cropNode.maskNode = maskNode cropNode.addChild(effectNode)
这个方法保留了ShapeNode的原生圆角线帽,同时通过滤镜实现渐变,适合不想深入Shader的场景。
二、实现重叠环效果(复刻Apple Workout Rings)
Apple的三个运动环是独立的环形节点,重叠部分通过渲染层级自动覆盖。你只需要给每个环设置不同的zPosition,按顺序添加到场景中即可:
// 封装创建环形节点的函数 func createRingNode(progress: CGFloat) -> SKNode { // 这里放入你前面实现的带渐变+圆角的环形节点逻辑 // 返回对应的SKNode(比如CropNode或带Shader的ShapeNode) } // 创建三个环,设置不同层级 let moveRing = createRingNode(progress: 0.7) moveRing.zPosition = 1 scene.addChild(moveRing) let exerciseRing = createRingNode(progress: 0.5) exerciseRing.zPosition = 2 scene.addChild(exerciseRing) let standRing = createRingNode(progress: 0.9) standRing.zPosition = 3 scene.addChild(standRing)
SpriteKit会按zPosition从低到高渲染,上层的环会自动覆盖下层的重叠区域,完美复刻官方效果。
三、替代方案:用UIKit/CoreAnimation实现
如果SpriteKit的Shader和ShapeNode限制太多,你也可以考虑用WatchOS支持的CAShapeLayer实现——CoreAnimation对环形进度和渐变的支持更成熟,无需处理SpriteKit的兼容问题:
// 创建环形Layer let ringLayer = CAShapeLayer() ringLayer.path = UIBezierPath(arcCenter: CGPoint(x: 100, y: 100), radius: 85, startAngle: -CGFloat.pi/2, endAngle: CGFloat.pi * 3/2, clockwise: true).cgPath ringLayer.lineWidth = 30 ringLayer.lineCap = .round ringLayer.fillColor = UIColor.clear.cgColor ringLayer.strokeEnd = 0.8 // 设置进度 // 添加渐变效果 let gradient = CAGradientLayer() gradient.frame = CGRect(x: 0, y: 0, width: 200, height: 200) gradient.colors = [UIColor.red.cgColor, UIColor.orange.cgColor] gradient.mask = ringLayer // 用环形Layer作为遮罩 // 添加到视图 let ringView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) ringView.layer.addSublayer(gradient)
内容的提问来源于stack exchange,提问作者Poornima Sapkal
相关产品推荐
相关产品推荐

