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

时间:2019-04-27 15:13:25

标签: ios swift uicollectionviewcell cabasicanimation cagradientlayer

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

// the imageWithColor function on the end turns it .lightGray: [https://stackoverflow.com/a/24545102/4833705][1]
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中添加了带有动画效果的渐变层,该动画具有微光效果,但是当单元格出现在屏幕上时,动画不会发生。

出了什么问题?

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
}
}

enter image description here

2 个答案:

答案 0 :(得分:0)

您可以尝试将其放在自己的函数中:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>React Training Project</title>
    <base href="/">
</head>
<body>
    <div id="app"></div>
    <script src="./bundle.js"></script>    
</body>
</html>

然后在您的init函数中调用它

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

似乎您的问题可能是layoutSubviews可以被调用很多,但是只有在使用框架初始化视图时才调用init函数。另外,将设置代码置于其自身的功能中将使它更容易执行其他操作,例如在帧改变时更新渐变层的帧。

答案 1 :(得分:0)

我知道了。

我在问题下的注释中接受了@Matt的建议,并将myView添加到了单元格的contentView属性中,而不是直接添加到该单元格中。我找不到该帖子,但我只是读到要使动画在单元格中起作用,无论动画显示在哪个视图,都需要将其添加到单元格的contentView

我从layoutSubviews中移了gradientLayer,取而代之的是使它成为惰性属性。

我也将动画移到了它自己的惰性属性中。

我使用了this answer并将gradientLayer的帧设置为单元格的bounds属性(我最初将其设置为单元格的frame属性)

我添加了一个函数,该函数将gradientLayer添加到myView图层的insertSublayer属性中,并在cellForRow中调用该函数。另外,根据我回答下的@Matt的注释,以防止渐变图层不断添加,我添加了一个检查以查看渐变是否在UIView图层的层次结构中(尽管from here是用于{不同的原因)。如果不存在,则添加,如果不添加,则不添加。

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

    if let _ = (myView.layer.sublayers?.compactMap { $0 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 { $0 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 { $0 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
}
}

enter image description here

请注意,如果用户无法滚动单元格(占位符单元格),但如果由于存在问题,他们可以确保在添加前进行测试,则此BELOW效果很好

我遇到的另一个问题是当我回到背景并返回动画时不会移动。我遵循了this answer(下面的代码使用方法),尽管在同一线程中,我修改了答案,以使用this answer从一开始就开始制作动画,但但是有问题。

即使从前台回来,我仍然注意到动画有时会起作用,但滚动动画时会卡住。为了解决这个问题,我在cell.removeGradientLayer()中调用了cellForRow,然后再次按如下说明进行了调用。但是,滚动时它仍然卡住,但是通过调用上面的内容,它就被卡住了。它可以满足我的需要,因为我仅在实际单元加载时显示这些单元。无论如何,我都会在动画发生时禁用滚动,所以我不必担心。仅供参考,似乎只有从背景回到然后再进行滚动时,才会出现此问题。

我还不得不通过在应用程序进入后台时调用cell.removeGradientLayer()来从单元格中删除gradientLayer,然后当它返回到前台时我必须调用cell.addAnimationAndGradientLayer()来再次添加它。我是通过在具有collectionView的类中添加background / foreground Notifications来实现的。在随附的Notification函数中,我只需滚动显示可见的单元格,然后调用该单元格所需的功能(代码也在下面)。

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
    }
}

在单元格类中,而不是制作 MyView UIView的实例,而是改为使其成为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

这是带有collectionView的类的通知。动画还会停止when the view disappearsreappears,因此您也必须在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()
                }
            }
        }
    }
}