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

动画时CAGradientLayer无法跟随UIView bounds平滑变化的问题

问题描述

在UIViewController中,给一个UIView添加了CAGradientLayer作为子层,通过修改该视图的高度约束做动画时,gradientLayer没有跟随视图的过渡尺寸变化,而是直接跳转到最终bounds,动画过程不同步。

代码示例:

class ViewController: UIViewController {
    
    @IBOutlet weak var headerView: UIView!
    @IBOutlet weak var headerViewHeightConstraint: NSLayoutConstraint!

    var gradientLayer: CAGradientLayer?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        applyGradient()
    }

    func applyGradient() {
        let colors: [UIColor] = [.red, .black]
        let gradientLayer = CAGradientLayer()
        gradientLayer.colors = colors.map { $0.cgColor}
        gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
        gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
        
        // headerView bounds here is (0, 0, 393, 200)
        gradientLayer.frame = headerView.bounds
        
        headerView.layer.addSublayer(gradientLayer)
        self.gradientLayer = gradientLayer
    }
    
    func animate() {
        headerViewHeightConstraint.constant = 100
        UIView.animate(withDuration: 0.3, delay: 0, animations: {
            self.view.layoutIfNeeded()
            // headerView bounds here is always (0, 0, 393, 100)
            self.gradientLayer?.frame = self.headerView.bounds
        }, completion: { _ in
            // headerView bounds here is always (0, 0, 393, 100)
            self.gradientLayer?.frame = self.headerView.bounds
        })
    }
    
    @IBAction func didTapAnimateButton() {
        animate()
    }
}

尝试过viewDidLayoutSubviews捕获过渡值、KVO观察frame变化,都只有动画结束时才触发,无法获取过渡过程中的值。

解决方案

方法1:自定义UIView,让其layer为CAGradientLayer

这是最省心的方案,系统会自动处理layer的布局跟随,不需要手动维护frame:

class GradientView: UIView {
    override class var layerClass: AnyClass {
        return CAGradientLayer.self
    }
    
    func configureGradient(colors: [UIColor], startPoint: CGPoint, endPoint: CGPoint) {
        guard let gradientLayer = layer as? CAGradientLayer else { return }
        gradientLayer.colors = colors.map { $0.cgColor }
        gradientLayer.startPoint = startPoint
        gradientLayer.endPoint = endPoint
    }
}

使用时,把storyboard中的headerView类改成GradientView,然后在ViewController中:

@IBOutlet weak var headerView: GradientView!

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    headerView.configureGradient(colors: [.red, .black], startPoint: CGPoint(x:0.5,y:0), endPoint: CGPoint(x:0.5,y:1))
}

func animate() {
    headerViewHeightConstraint.constant = 100
    UIView.animate(withDuration: 0.3) {
        self.view.layoutIfNeeded()
    }
}

这样动画时,gradientLayer会自动跟随headerView的尺寸变化,完全同步。

方法2:利用CALayer的mask属性

如果不想自定义View,可以把gradientLayer作为headerView的mask,这样mask会自动跟随视图的bounds变化:

修改applyGradient方法:

func applyGradient() {
    let colors: [UIColor] = [.red, .black]
    let gradientLayer = CAGradientLayer()
    gradientLayer.colors = colors.map { $0.cgColor}
    gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
    gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
    
    gradientLayer.frame = headerView.bounds
    // 把gradientLayer设为mask,而不是子层
    headerView.layer.mask = gradientLayer
    self.gradientLayer = gradientLayer
}

动画代码不需要修改,mask会自动跟随headerView的尺寸动画。

方法3:同步Core Animation动画

如果必须用子层的方式,需要手动给gradientLayer添加动画,和UIView动画同步:

func animate() {
    headerViewHeightConstraint.constant = 100
    // 先布局一次获取最终bounds
    self.view.layoutIfNeeded()
    let finalBounds = headerView.bounds
    
    // 还原约束,再触发动画
    headerViewHeightConstraint.constant = 200
    self.view.layoutIfNeeded()
    
    // 修改约束为目标值
    headerViewHeightConstraint.constant = 100
    
    // 同步UIView和Core Animation的动画
    UIView.animate(withDuration: 0.3, delay: 0, animations: {
        self.view.layoutIfNeeded()
    })
    
    // 给gradientLayer添加frame动画
    let frameAnimation = CABasicAnimation(keyPath: "bounds")
    frameAnimation.fromValue = headerView.bounds
    frameAnimation.toValue = finalBounds
    frameAnimation.duration = 0.3
    gradientLayer?.add(frameAnimation, forKey: "boundsAnimation")
    // 设置最终值,避免动画结束后回弹
    gradientLayer?.bounds = finalBounds
}

这种方法需要手动管理动画,不如前两种优雅,推荐优先用前两种方案。

内容的提问来源于stack exchange,提问作者Alex Sh.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 05:10:40