You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift实现沿圆角矩形路径的等长无限线条动画方法咨询

没问题,我来给你梳理一个完美解决这个需求的方案——你要的是一段固定长度的线条沿着圆角矩形路径无限循环滑动,而且全程保持长度不变对吧?之前用虚线动画之类的方案确实容易踩坑(比如圆角部分线条视觉长度会变形),下面这个自定义View的实现能精准控制效果:

实现沿圆角矩形路径的固定长度无限循环线条动画

核心思路是:先生成完整的圆角矩形路径,然后通过CADisplayLink驱动动画,实时截取路径上固定长度的子段进行绘制;当线条的起点走到路径终点时,自动从路径开头衔接,实现无缝循环。

步骤1:自定义动画承载View

先创建一个专门的UIView子类,用来管理路径、动画逻辑和线条绘制:

import UIKit

class AnimatedFixedLengthLineView: UIView {
    // 可自定义的动画参数
    var cornerRadius: CGFloat = 20 {
        didSet { refreshFullPath() }
    }
    var fixedLineLength: CGFloat = 80 {
        didSet { 
            // 避免线条长度超过路径总长度
            if fixedLineLength > fullPathLength {
                fixedLineLength = fullPathLength
            }
            updateCurrentLinePath()
        }
    }
    var lineWidth: CGFloat = 4 {
        didSet { shapeLayer.lineWidth = lineWidth }
    }
    var lineColor: UIColor = .systemBlue {
        didSet { shapeLayer.strokeColor = lineColor.cgColor }
    }
    
    // 内部核心组件
    private let shapeLayer = CAShapeLayer()
    private var fullRoundedRectPath: UIBezierPath!
    private var fullPathLength: CGFloat = 0
    private var currentProgress: CGFloat = 0 // 0~1,代表起点在路径上的位置比例
    private let displayLink = CADisplayLink(target: self, selector: #selector(updateAnimationFrame))
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupBaseConfig()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupBaseConfig()
    }
    
    private func setupBaseConfig() {
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeEnd = 1.0
        layer.addSublayer(shapeLayer)
        refreshFullPath()
        // 绑定屏幕刷新率驱动动画
        displayLink.add(to: .main, forMode: .common)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        // 视图尺寸变化时重新生成路径
        refreshFullPath()
    }
}

步骤2:生成圆角矩形路径并计算总长度

我们需要确保路径预留线条宽度的边距,避免线条被视图裁切,同时计算路径总长度作为动画的基准:

extension AnimatedFixedLengthLineView {
    private func refreshFullPath() {
        // 留出线条宽度的边距,防止线条被裁切
        let insetRect = bounds.insetBy(dx: lineWidth/2, dy: lineWidth/2)
        fullRoundedRectPath = UIBezierPath(roundedRect: insetRect, cornerRadius: cornerRadius)
        fullPathLength = fullRoundedRectPath.cgPath.length
        
        updateCurrentLinePath()
    }
}

步骤3:核心动画逻辑——实时更新线条路径

这里通过CADisplayLink逐帧计算线条的起止位置,截取路径的固定长度子段绘制,实现循环滑动效果:

extension AnimatedFixedLengthLineView {
    @objc private func updateAnimationFrame() {
        // 控制动画速度:每秒移动40pt,可根据需求调整
        let moveSpeed: CGFloat = 40 / 60 // 60对应屏幕刷新率
        currentProgress += moveSpeed / fullPathLength
        
        // 超过1则重置进度,实现无限循环
        if currentProgress > 1 {
            currentProgress -= 1
        }
        
        updateCurrentLinePath()
    }
    
    private func updateCurrentLinePath() {
        let startDistance = currentProgress * fullPathLength
        var endDistance = startDistance + fixedLineLength
        
        // 处理跨路径首尾的情况
        if endDistance > fullPathLength {
            // 分两段截取路径:从起点到路径末尾 + 从路径开头到剩余长度
            let firstSegment = fullRoundedRectPath.cgPath.copy(dashingWithPhase: startDistance, lengths: [fullPathLength - startDistance, fullPathLength])
            let secondSegment = fullRoundedRectPath.cgPath.copy(dashingWithPhase: 0, lengths: [endDistance - fullPathLength, fullPathLength])
            
            let combinedPath = UIBezierPath()
            combinedPath.append(UIBezierPath(cgPath: firstSegment))
            combinedPath.append(UIBezierPath(cgPath: secondSegment))
            
            shapeLayer.path = combinedPath.cgPath
        } else {
            // 正常情况:直接截取固定长度的子路径
            let targetPath = fullRoundedRectPath.cgPath.copy(dashingWithPhase: startDistance, lengths: [fixedLineLength, fullPathLength])
            shapeLayer.path = targetPath
        }
    }
}

步骤4:在项目中使用这个View

在ViewController里直接添加配置即可:

class DemoViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        let animatedLineView = AnimatedFixedLengthLineView(frame: CGRect(x: 50, y: 200, width: 300, height: 200))
        animatedLineView.cornerRadius = 30
        animatedLineView.fixedLineLength = 100
        animatedLineView.lineWidth = 5
        animatedLineView.lineColor = .systemOrange
        view.addSubview(animatedLineView)
    }
}

关键细节说明

  • 固定长度保障:通过CGPath的copy(dashingWithPhase:)方法直接截取路径上的固定弧长段,不管是直线还是圆角曲线,线条的实际长度始终保持你设置的fixedLineLength值。
  • 无缝循环:当线条终点超过路径总长度时,自动拆分路径为两段拼接,完全没有断点感。
  • 性能优化:用CADisplayLink和屏幕刷新率同步,动画流畅无卡顿,路径计算开销极低,不会影响主线程性能。

内容的提问来源于stack exchange,提问作者martinzly

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 03:37:00