Swift自定义视图CABasicAnimation的strokeEnd动画不生效
问题根因
CABasicAnimation描边动画不生效是几个代码细节错误叠加导致的,和你检查过的视图frame配置无关,核心是layer坐标系、属性初始值、布局时机的问题:
- subLayer的frame赋值错误:你写的
subLayer.frame = layer.frame逻辑有误,sublayer的frame是相对于父layer(即ToastView的根layer)的bounds坐标系,而layer.frame是根layer相对于它父视图的坐标,直接赋值会导致shapeLayer偏移出可视区域,根本看不到绘制的描边。 - 没有设置shapeLayer的model层属性:CABasicAnimation只改动渲染层的临时展示值,动画执行过程中看到的效果都是渲染层输出的,如果你不给
subLayer.strokeEnd设置最终值1,动画开始前/结束后渲染层会回到model层的默认值0,很容易出现完全看不到动画的情况。 - 布局时机错误:如果调用
startAnimation的时候ToastView还没完成约束布局,此时self.bounds是不准确的,用这个bounds画出来的贝塞尔路径尺寸、位置都是错的。 - 重复添加sublayer:每次调用startAnimation都会执行一次
insertSublayer,多次调用会堆叠大量重复的shapeLayer在视图层级里,引发渲染异常。
修复代码
调整layer的初始化和布局逻辑,把sublayer添加提前到初始化阶段,布局更新放到layoutSubviews里保证尺寸准确,动画部分补全model层赋值即可:
class ToastView: UIView { lazy var label: UILabel = { let label = UILabel() label.textColor = .white label.font = UIFont.systemFont(ofSize: 14, weight: .regular) return label }() lazy var subLayer: CAShapeLayer = { let shapeLayer = CAShapeLayer() shapeLayer.fillColor = nil shapeLayer.strokeColor = UIColor.green.cgColor shapeLayer.lineCap = .round shapeLayer.lineWidth = 2 // 初始strokeEnd设为0,默认不显示描边 shapeLayer.strokeEnd = 0 return shapeLayer }() override init(frame: CGRect) { super.init(frame: frame) customInit() } required init?(coder: NSCoder) { super.init(coder: coder) customInit() } override func layoutSubviews() { super.layoutSubviews() // 布局完成后更新subLayer的frame和path,保证尺寸准确 subLayer.frame = layer.bounds subLayer.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 4).cgPath } private func customInit() { backgroundColor = .black self.addSubview(label) label.translatesAutoresizingMaskIntoConstraints = false let constraints = [ label.topAnchor.constraint(equalTo: self.topAnchor, constant: 2), label.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 3), label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 4), label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 4) ] NSLayoutConstraint.activate(constraints) layer.borderWidth = 1 layer.borderColor = UIColor.white.cgColor layer.cornerRadius = 4 layer.masksToBounds = true // 只添加一次subLayer,不要每次动画都重复加 layer.insertSublayer(subLayer, at: 0) } func startAnimation(toastMessage: String) { self.label.text = toastMessage // 强制更新布局,保证此时bounds是准确的 self.layoutIfNeeded() // 先给model层设置最终值,避免动画跳变 subLayer.strokeEnd = 1 let strokeAnimation = CABasicAnimation(keyPath: "strokeEnd") strokeAnimation.fromValue = 0 strokeAnimation.toValue = 1 strokeAnimation.duration = 1.5 strokeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) subLayer.add(strokeAnimation, forKey: "strokeEndAnimation") // 后续写弹出动画逻辑即可 } }
额外说明
- 不需要手动设置
beginTime = 0,这个是动画的默认值,写了没有实际作用。 - 给动画设置非空的key,后续如果需要提前终止动画可以通过key找到并移除。
- 只要你在添加动画前设置了
subLayer.strokeEnd = 1,就不需要设置isRemovedOnCompletion = false和fillMode = .forwards来强行保留动画最后一帧,那种写法是不规范的,本质是让动画留在渲染层不更新model层,会导致后续属性读写异常。
内容的提问来源于stack exchange,提问作者H1rouge
相关产品推荐
相关产品推荐

