Swift - GradientLayer 中的动画未出现在单元格中

Swift -Animation in GradientLayer not appearing in cell

我将不同食物的不同图像添加到 UIView(我选择使用 UIView 而不是 UIImageView)。图像的原始颜色是黑色,我使用 .alwaysTemplate 将它们更改为 .lightGray

// the imageWithColor function on the end turns it .lightGray: [
let pizzaImage = UIImage(named: "pizzaImage")?.withRenderingMode(.alwaysTemplate).imageWithColor(color1: UIColor.lightGray)
foodImages.append(pizzaImage) 

我将食物图像添加到 cellForRow

中的 UIView
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: foodCell, for: indexPath) as! FoodCell

    cell.myView.layer.contents = foodImages[indexPath.item].cgImage

    return cell
}

UIView 在一个单元格内,在单元格的 layoutSubviews 我添加了一个 gradientLayer 和一个动画效果,但是当单元格出现在屏幕上时,动画不会出现。

有什么问题?

class FoodCell: UICollectionViewCell {

let myView: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.layer.cornerRadius = 7
    view.layer.masksToBounds = true
    view.layer.contentsGravity = CALayerContentsGravity.center
    view.tintColor = .lightGray
    return view
}()

override init(frame: CGRect) {
    super.init(frame: frame)
    backgroundColor = .white

    setAnchors()
}

override func layoutSubviews() {
    super.layoutSubviews()

    let gradientLayer = CAGradientLayer()
    gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
    gradientLayer.locations = [0, 0.5, 1]
    gradientLayer.frame = myView.frame

    let angle = 45 * CGFloat.pi / 180
    gradientLayer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)

    let animation = CABasicAnimation(keyPath: "transform.translation.x")
    animation.duration = 2
    animation.fromValue = -self.frame.width
    animation.toValue = self.frame.width
    animation.repeatCount = .infinity

    gradientLayer.add(animation, forKey: "...")
}

fileprivate func setAnchors() {
    addSubview(myView)

    myView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true
    myView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true
    myView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
    myView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
}
}

你可以试试这个,将这段代码放在它自己的函数中:

func setUpGradient() {
let gradientLayer = CAGradientLayer()
    gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]    
    ...
    gradientLayer.add(animation, forKey: "...")
}

然后在你的初始化函数中调用它

override init(frame: CGRect) {
    super.init(frame: frame)
    setUpGradient()
}

看来你的问题可能是layoutSubviews可以调用很多,但是init函数只会在视图用框架初始化时被调用。此外,将设置代码放在其自己的函数中将使它更容易做其他事情,例如在框架发生变化时更新渐变图层的框架。

我成功了。

我在问题下的评论中采纳了@Matt 的建议,将 myView 添加到单元格的 contentView 属性 而不是直接添加到单元格中。我找不到 post 但我刚刚读到要使动画在单元格中工作,无论动画在哪个视图上都需要添加到单元格的 contentView

我从 layoutSubviews 中移动了 gradientLayer 并将其设为惰性 属性。

我也把动画移到了它自己的懒惰里属性。

我使用 并将 gradientLayer 的框架设置为单元格的 bounds 属性(我最初将其设置为单元格的 frame 属性)

我添加了一个函数,将 gradientLayer 添加到 myView 的图层 insertSublayer 属性 并在 cellForRow 中调用该函数。另外,根据@Matt 在我的回答下的评论,以防止 gradientLayer 不断被再次添加不同的原因)。如果没有我添加,如果有我不添加。

// I added both the animation and the gradientLayer here
func addAnimationAndGradientLayer() {

    if let _ = (myView.layer.sublayers?.compactMap { [=10=] as? CAGradientLayer })?.first {
        print("it's already in here so don't readd it")
    } else {

        gradientLayer.add(animation, forKey: "...") // 1. added animation
        myView.layer.insertSublayer(gradientLayer, at: 0) // 2. added the gradientLayer
        print("it's not in here so add it")
    }
}

调用函数将 gradientLayer 添加到它在 cellForRow

中调用的单元格
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: foodCell, for: indexPath) as! FoodCell

    cell.removeGradientLayer() // remove the gradientLayer due to going to the background and back issues

    cell.myView.layer.contents = foodImages[indexPath.item].cgImage

    cell.addAnimationAndGradientLayer() // I call it here

    return cell
}

单元格的更新代码

class FoodCell: UICollectionViewCell {

let myView: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.layer.cornerRadius = 7
    view.layer.masksToBounds = true
    view.layer.contentsGravity = CALayerContentsGravity.center
    view.tintColor = .lightGray
    return view
}()

lazy var gradientLayer: CAGradientLayer = {

    let gradientLayer = CAGradientLayer()
    gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
    gradientLayer.locations = [0, 0.5, 1]
    gradientLayer.frame = self.bounds

    let angle = 45 * CGFloat.pi / 180
    gradientLayer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)
    return gradientLayer
}()

lazy var animation: CABasicAnimation = {

    let animation = CABasicAnimation(keyPath: "transform.translation.x")
    animation.duration = 2
    animation.fromValue = -self.frame.width
    animation.toValue = self.frame.width
    animation.repeatCount = .infinity
    animation.fillMode = CAMediaTimingFillMode.forwards
    animation.isRemovedOnCompletion = false

    return animation
}()

override init(frame: CGRect) {
    super.init(frame: frame)
    backgroundColor = .white

    setAnchors()
}

func addAnimationAndGradientLayer() {

    // make sure the gradientLayer isn't already in myView's hierarchy before adding it
    if let _ = (myView.layer.sublayers?.compactMap { [=12=] as? CAGradientLayer })?.first {
        print("it's already in here so don't readd it")
    } else {

        gradientLayer.add(animation, forKey: "...") // 1. add animation
        myView.layer.insertSublayer(gradientLayer, at: 0) // 2. add gradientLayer
        print("it's not in here so add it")
    }
}

// this function is explained at the bottom of my answer and is necessary if you want the animation to not pause when coming from the background 
func removeGradientLayer() {

    myView.layer.sublayers?.removeAll()
    gradientLayer.removeFromSuperlayer()

    setNeedsDisplay() // these 2 might not be necessary but i called them anyway
    layoutIfNeeded()

    if let _ = (iconImageView.layer.sublayers?.compactMap { [=12=] as? CAGradientLayer })?.first {
        print("no man the gradientLayer is not removed")
    } else {
        print("yay the gradientLayer is removed")
    }
}

fileprivate func setAnchors() {

    self.contentView.addSubview(myView)

    myView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0).isActive = true
    myView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0).isActive = true
    myView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0).isActive = true
    myView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0).isActive = true
}
}

作为旁注,如果用户不能滚动单元格(占位符单元格),但如果他们可以确保在添加之前进行测试,因为它有错误,那么下面的内容非常有用

我 运行 遇到的另一个问题是,当我进入背景并返回时,动画不会移动。我按照 this answer (code below on how to use it) which works although in that same thread I amended that answer to use this answer 从头开始​​播放动画,效果很好 但是 有问题。

我注意到即使我从前台返回并且动画有时在我滚动时仍然有效但动画卡住了。为了解决这个问题,我在 cellForRow 中调用了 cell.removeGradientLayer(),然后再次调用,如下所述。然而,它在滚动时仍然卡住了,但通过调用上面的代码,它就被解开了。它可以满足我的需要,因为我只在加载实际单元格时显示这些单元格。无论如何,当动画出现时我会禁用滚动,所以我不必担心它。仅供参考,这个卡住的问题似乎只在从 background 返回然后 scrolling.

时发生

我还必须在应用程序进入后台时调用 cell.removeGradientLayer() 从单元格中删除 gradientLayer,然后当它返回前台时我不得不调用 cell.addAnimationAndGradientLayer() 来添加它再次。我通过在具有 collectionView 的 class 中添加 background/foreground 通知来做到这一点。在随附的通知功能中,我只是滚动浏览可见单元格并调用必要的单元格功能(代码也在下面)。

class PersistAnimationView: UIView {

    private var persistentAnimations: [String: CAAnimation] = [:]
    private var persistentSpeed: Float = 0.0

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
    }

    func commonInit() {

        NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: UIApplication.didEnterBackgroundNotification, object: nil)

        NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    func didBecomeActive() {
        self.restoreAnimations(withKeys: Array(self.persistentAnimations.keys))
        self.persistentAnimations.removeAll()
        if self.persistentSpeed == 1.0 { //if layer was plaiyng before backgorund, resume it
            self.layer.resume()
        }
    }

    func willResignActive() {
        self.persistentSpeed = self.layer.speed

        self.layer.speed = 1.0 //in case layer was paused from outside, set speed to 1.0 to get all animations
        self.persistAnimations(withKeys: self.layer.animationKeys())
        self.layer.speed = self.persistentSpeed //restore original speed

        self.layer.pause()
    }

    func persistAnimations(withKeys: [String]?) {
        withKeys?.forEach({ (key) in
            if let animation = self.layer.animation(forKey: key) {
                self.persistentAnimations[key] = animation
            }
        })
    }

    func restoreAnimations(withKeys: [String]?) {
        withKeys?.forEach { key in
            if let persistentAnimation = self.persistentAnimations[key] {
                self.layer.add(persistentAnimation, forKey: key)
            }
        }
    }
}

extension CALayer {
    func pause() {
        if self.isPaused() == false {
            let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
            self.speed = 0.0
            self.timeOffset = pausedTime
        }
    }

    func isPaused() -> Bool {
        return self.speed == 0.0
    }

    func resume() {
        let pausedTime: CFTimeInterval = self.timeOffset
        self.speed = 1.0
        self.timeOffset = 0.0
        self.beginTime = 0.0
        // as per the amended answer comment these 2 lines out to start the animation from the beginning when coming back from the background
        // let timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
        // self.beginTime = timeSincePause
    }
}

在单元格 class 中,我没有创建 MyViewUIView 的实例,而是像这样将其作为 PersistAnimationView 的实例:

class FoodCell: UICollectionViewCell {

    let MyView: PersistAnimationView = {
        let persistAnimationView = PersistAnimationView()
        persistAnimationView.translatesAutoresizingMaskIntoConstraints = false
        persistAnimationView.layer.cornerRadius = 7
        persistAnimationView.layer.masksToBounds = true
        persistAnimationView.layer.contentsGravity = CALayerContentsGravity.center
        persistAnimationView.tintColor = .lightGray
        return persistAnimationView
    }()

    // everything else in the cell class is the same

这里是 class 与 collectionView 的通知。动画也会停止 when the view disappears or ,因此您还必须在 viewWillAppear 和 viewDidDisappear 中进行管理。

class MyClass: UIViewController, UICollectionViewDatasource, UICollectionViewDelegateFlowLayout {

    var collectionView: UICollectionView!

    // MARK:- View Controller Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.addObserver(self, selector: #selector(appHasEnteredBackground), name: UIApplication.willResignActiveNotification, object: nil)

        NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        addAnimationAndGradientLayerInFoodCell()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        removeGradientLayerInFoodCell()
    }

    // MARK:- Functions for Notifications
    @objc func appHasEnteredBackground() {

        removeGradientLayerInFoodCell()
    }

    @objc func appWillEnterForeground() {

        addAnimationAndGradientLayerInFoodCell()
    }

    // MARK:- Supporting Functions
    func removeGradientLayerInFoodCell() {

        // if you are using a tabBar, switch tabs, then go to the background, comeback, then switch back to this tab, without this check the animation will get stuck
        if (self.view.window != nil) {

            collectionView.visibleCells.forEach { (cell) in

                if let cell = cell as? FoodCell {
                    cell.removeGradientLayer()
                }
            }
        }
    }

    func addAnimationAndGradientLayerInFoodCell() {

        // if you are using a tabBar, switch tabs, then go to the background, comeback, then switch back to this tab, without this check the animation will get stuck
        if (self.view.window != nil) {

            collectionView.visibleCells.forEach { (cell) in

                if let cell = cell as? FoodCell {
                    cell.addAnimationAndGradientLayer()
                }
            }
        }
    }
}