动画时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.
相关产品推荐
相关产品推荐

