如何实现圆角矩形路径蛇形动画?解决路径闭合点跳转异常
蛇形圆角边框动画实现问题及优化建议
我要实现一个蛇形流动的圆角矩形边框动画(边框上有一段高亮线条循环流动的效果),用Core Graphics、Core Animation结合UIBezierPath尝试实现时遇到了核心问题:由于strokeStart必须小于strokeEnd,动画无法平滑经过路径的闭合点,导致循环时出现断层。调试许久后觉得可能工具用法不对,求可行的实现方案。
以下是我尝试的代码:
func animate() { let centerRectInRect = {(rect: CGRect, bounds: CGRect) -> CGRect in return CGRect(x: bounds.origin.x + ((bounds.width - rect.width) / 2.0), y: bounds.origin.y + ((bounds.height - rect.height) / 2.0), width: rect.width, height: rect.height) } let shapeLayer = CAShapeLayer() shapeLayer.frame = centerRectInRect(CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0), self.view.bounds) self.view.layer.addSublayer(shapeLayer) shapeLayer.strokeStart = 0.0 shapeLayer.strokeEnd = 1.0 shapeLayer.fillColor = UIColor.clear.cgColor shapeLayer.strokeColor = UIColor.red.cgColor shapeLayer.fillColor = UIColor.orange.withAlphaComponent(0.2).cgColor shapeLayer.lineWidth = 12.0 let rect = shapeLayer.bounds let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 16, height: 16)) path.append(UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 16, height: 16))) shapeLayer.path = path.cgPath let strokeStartAnim = CAKeyframeAnimation(keyPath: "strokeStart") strokeStartAnim.values = [0, 1] strokeStartAnim.keyTimes = [0, 1] strokeStartAnim.duration = 12.0 strokeStartAnim.beginTime = 1.0 strokeStartAnim.repeatCount = .infinity strokeStartAnim.calculationMode = .paced let strokeEndAnim = CAKeyframeAnimation(keyPath: "strokeEnd") strokeEndAnim.values = [0, 1] strokeEndAnim.keyTimes = [0, 1] strokeEndAnim.duration = 12.0 strokeEndAnim.repeatCount = .infinity strokeEndAnim.calculationMode = .paced let groupAnim = CAAnimationGroup() groupAnim.animations = [strokeStartAnim, strokeEndAnim] groupAnim.isRemovedOnCompletion = false groupAnim.fillMode = .forwards groupAnim.duration = .greatestFiniteMagnitude shapeLayer.add(groupAnim, forKey: "AnimateSnake") }
可行实现方案建议
方案1:使用虚线偏移动画(最简洁)
这种方式不需要处理strokeStart/strokeEnd的限制,通过移动虚线的偏移量来实现流动效果,适配闭合路径更自然:
func animateWithDashPhase() { let centerRectInRect = {(rect: CGRect, bounds: CGRect) -> CGRect in return CGRect(x: bounds.origin.x + ((bounds.width - rect.width) / 2.0), y: bounds.origin.y + ((bounds.height - rect.height) / 2.0), width: rect.width, height: rect.height) } let shapeLayer = CAShapeLayer() shapeLayer.frame = centerRectInRect(CGRect(x: 0, y: 0, width: 200, height: 200), self.view.bounds) self.view.layer.addSublayer(shapeLayer) shapeLayer.fillColor = UIColor.orange.withAlphaComponent(0.2).cgColor shapeLayer.strokeColor = UIColor.red.cgColor shapeLayer.lineWidth = 12.0 // 修正路径:让边框不超出layer范围 let rect = shapeLayer.bounds.insetBy(dx: shapeLayer.lineWidth/2, dy: shapeLayer.lineWidth/2) let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 16, height: 16)) shapeLayer.path = path.cgPath // 计算路径总长度(也可以用CGPath的精确计算方法,这里是近似值) let pathLength = CGFloat(path.cgPath.boundingBox.width * 2 + path.cgPath.boundingBox.height * 2) let highlightLength: CGFloat = 80 // 设置虚线模式:[高亮段长度, 空白段长度] shapeLayer.lineDashPattern = [NSNumber(value: Float(highlightLength)), NSNumber(value: Float(pathLength - highlightLength))] // 动画虚线偏移量,实现循环流动 let dashAnim = CABasicAnimation(keyPath: "lineDashPhase") dashAnim.fromValue = 0 dashAnim.toValue = -pathLength dashAnim.duration = 12.0 dashAnim.repeatCount = .infinity dashAnim.timingFunction = CAMediaTimingFunction(name: .linear) shapeLayer.add(dashAnim, forKey: "DashAnimation") }
方案2:修复原路径与动画逻辑
如果坚持用strokeStart/strokeEnd,需要修改路径为非闭合且首尾重叠的连续路径,同时调整动画数值逻辑:
- 不要重复添加路径,而是将原闭合路径改成开放路径后,额外绘制一段和开头重叠的线段(比如20pt),让路径总长度包含重叠部分
- 调整
strokeStart和strokeEnd的动画范围,保持两者的差值固定(对应高亮段的路径比例),确保循环时重叠段覆盖闭合点的断层
示例路径修改:
let rect = shapeLayer.bounds.insetBy(dx: shapeLayer.lineWidth/2, dy: shapeLayer.lineWidth/2) let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 16, height: 16)) // 取消闭合,添加一段和起点重叠的线段 path.removeAllPoints() path.move(to: rect.origin) path.addArc(withCenter: CGPoint(x: rect.maxX - 16, y: rect.maxY - 16), radius: 16, startAngle: .pi, endAngle: .pi/2, clockwise: false) path.addLine(to: CGPoint(x: rect.minX + 16, y: rect.maxY)) path.addArc(withCenter: CGPoint(x: rect.minX + 16, y: rect.maxY - 16), radius: 16, startAngle: .pi/2, endAngle: 0, clockwise: false) path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + 16)) path.addArc(withCenter: CGPoint(x: rect.minX + 16, y: rect.minY + 16), radius: 16, startAngle: 0, endAngle: -.pi/2, clockwise: false) path.addLine(to: CGPoint(x: rect.maxX - 16, y: rect.minY)) path.addArc(withCenter: CGPoint(x: rect.maxX - 16, y: rect.minY + 16), radius: 16, startAngle: -.pi/2, endAngle: -.pi, clockwise: false) // 添加重叠段 path.addLine(to: CGPoint(x: rect.maxX - 36, y: rect.minY)) shapeLayer.path = path.cgPath
然后调整动画参数,让strokeStart和strokeEnd的差值固定,动画周期匹配路径总长度,实现无缝循环。
内容的提问来源于stack exchange,提问作者lawicko
相关产品推荐
相关产品推荐

