Swift - GradientLayer 中的动画未出现在单元格中 [英] Swift -Animation in GradientLayer not appearing in cell

查看:23
本文介绍了Swift - GradientLayer 中的动画未出现在单元格中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

//最后的imageWithColor函数变成.lightGray:[https://stackoverflow.com/a/24545102/4833705][1]让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!食物细胞cell.myView.layer.contents = foodImages[indexPath.item].cgImage返回单元格}

UIView 位于单元格内,在单元格的 layoutSubviews 中,我添加了一个带有动画的渐变层,可提供微光效果,但当单元格出现在屏幕上时,动画不会发生.

有什么问题?

class FoodCell: UICollectionViewCell {让我的视图:UIView = {让视图 = UIView()view.translatesAutoresizingMaskIntoConstraints = falseview.layer.cornerRadius = 7view.layer.masksToBounds = trueview.layer.contentsGravity = CALayerContentsGravity.centerview.tintColor = .lightGray返回视图}()覆盖初始化(框架:CGRect){super.init(框架:框架)背景颜色 = .white设置锚点()}覆盖 func layoutSubviews() {super.layoutSubviews()让梯度层 = CAGradientLayer()gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]gradientLayer.locations = [0, 0.5, 1]渐变层.frame = myView.frame让角度 = 45 * CGFloat.pi/180gradientLayer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)让动画 = CABasicAnimation(keyPath: "transform.translation.x")动画.持续时间 = 2animation.fromValue = -self.frame.widthanimation.toValue = self.frame.width动画.repeatCount = .infinitygradientLayer.add(animation, forKey: "...")}文件私有函数 setAnchors() {添加子视图(我的视图)myView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = truemyView.trailingAnchor.constraint(equalTo: self.trailingAnchor, 常量: 0).isActive = truemyView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = truemyView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true}}

解决方案

我搞定了.

我在问题下的评论中接受了@Matt 的建议,并将 myView 添加到单元格的 contentView 属性中,而不是直接添加到单元格中.我找不到那个帖子,但我只是读到要让动画在单元格中工作,需要将动画所在的视图添加到单元格的 contentView

我从 layoutSubviews 中移动了 gradientLayer,而是将其设为惰性属性.

我还将动画移动到它自己的惰性属性中.

我使用了

作为旁注,如果用户不能滚动单元格(占位符单元格),但如果他们可以确保在添加之前进行测试,因为它有问题

我遇到的另一个问题是,当我转到后台并返回时,动画不会移动.我遵循了 这个答案(下面关于如何使用它的代码)虽然在同一个线程中我修改了该答案以使用它,但它仍然有效这个答案从头开始播放动画但是存在问题.

我注意到即使我从前台返回并且动画有时会在我滚动时卡住.为了解决这个问题,我在 cellForRow 中调用了 cell.removeGradientLayer() ,然后再次调用,如下所述.但是,滚动时它仍然卡住了,但是通过调用上面的代码,它被卡住了.它适用于我需要的东西,因为我只在加载实际单元格时显示这些单元格.无论如何,我在动画发生时禁用滚动,所以我不必担心.仅供参考,这个卡住的问题似乎只有在从背景返回然后滚动时才会发生.

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

class PersistAnimationView: UIView {私有变量持久动画:[字符串:CAAnimation] = [:]私有变量persistentSpeed:浮点数= 0.0覆盖初始化(框架:CGRect){super.init(框架:框架)self.commonInit()}需要初始化?(编码器aDecoder:NSCoder){super.init(编码器: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)}去初始化{NotificationCenter.default.removeObserver(self)}func didBecomeActive() {self.restoreAnimations(withKeys: Array(self.persistentAnimations.keys))self.persistentAnimations.removeAll()if self.persistentSpeed == 1.0 {//如果图层在backgorund之前是播放,则恢复它self.layer.resume()}}func willResignActive() {self.persistentSpeed = self.layer.speedself.layer.speed = 1.0//如果图层从外部暂停,将速度设置为 1.0 以获取所有动画self.persistAnimations(withKeys: self.layer.animationKeys())self.layer.speed = self.persistentSpeed//恢复原速度self.layer.pause()}func persistAnimations(withKeys: [String]?) {withKeys?.forEach({ (key) in如果让动画 = self.layer.animation(forKey: key) {self.persistentAnimations[key] = 动画}})}func restoreAnimations(withKeys: [String]?) {withKeys?.forEach { 输入如果让persistentAnimation = self.persistentAnimations[key] {self.layer.add(persistentAnimation, forKey: key)}}}}扩展 CALayer {功能暂停(){如果 self.isPaused() == false {让 pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)self.speed = 0.0self.timeOffset = pausedTime}}func isPaused() ->布尔{返回 self.speed == 0.0}功能恢复(){让 pausedTime: CFTimeInterval = self.timeOffsetself.speed = 1.0self.timeOffset = 0.0self.beginTime = 0.0//根据修改后的答案注释这 2 行,以便从后台返回时从头开始动画//让 timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime//self.beginTime = timeSincePause}}

在单元格类中,我没有制作 MyViewUIView 的实例,而是将它设为 PersistAnimationView 的实例,如下所示:

class FoodCell: UICollectionViewCell {让我的视图:PersistAnimationView = {让persistAnimationView = PersistAnimationView()persistAnimationView.translatesAutoresizingMaskIntoConstraints = falsepersistAnimationView.layer.cornerRadius = 7persistAnimationView.layer.masksToBounds = truepersistAnimationView.layer.contentsGravity = CALayerContentsGravity.centerpersistAnimationView.tintColor = .lightGray返回持久动画视图}()//单元格类中的其他所有内容都相同

这是带有 collectionView 的类的通知.当视图消失重新出现,因此您也必须在 viewWillAppear 和 viewDidDisappear 中管理它.

class MyClass: UIViewController, UICollectionViewDatasource, UICollectionViewDelegateFlowLayout {var collectionView:UICollectionView!//MARK:- 视图控制器生命周期覆盖 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)}覆盖 func viewWillAppear(_ 动画:布尔){super.viewWillAppear(动画)addAnimationAndGradientLayerInFoodCell()}覆盖 func viewDidDisappear(_ 动画:布尔){super.viewDidDisappear(动画)removeGradientLayerInFoodCell()}//MARK:- 通知函数@objc func appHasEnteredBackground() {removeGradientLayerInFoodCell()}@objc func appWillEnterForeground() {addAnimationAndGradientLayerInFoodCell()}//MARK:- 支持函数func removeGradientLayerInFoodCell() {//如果你使用的是tabBar,切换tabs,然后到后台,回来,然后切换回这个tab,不勾选动画会卡住如果(self.view.window != nil){collectionView.visibleCells.forEach {(单元格)在如果让单元格 = 单元格为?食物细胞{cell.removeGradientLayer()}}}}func addAnimationAndGradientLayerInFoodCell() {//如果你使用的是tabBar,切换tabs,然后到后台,回来,然后切换回这个tab,不勾选动画会卡住如果(self.view.window != nil){collectionView.visibleCells.forEach {(单元格)在如果让单元格 = 单元格为?食物细胞{cell.addAnimationAndGradientLayer()}}}}}

I have different images of different foods that I add to a UIView (I choose to use an UIView instead of an UIImageView). The original color of the images are black and I change them to .lightGray using .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) 

I add the food images to the UIView in cellForRow

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
}

The UIView is inside a cell and in the cell's layoutSubviews I add a gradientLayer with an animation that gives a shimmer effect but when the cells appear on screen the animation doesn't occur.

What's the issue?

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

解决方案

I got it working.

I took @Matt's advice in the comments under the question and added myView to the cell's contentView property instead of the cell directly. I can't find the post but I just read that for animations to work in a cell, whichever views the animations are on needs to be added to the cell's contentView

I moved the gradientLayer from layoutSubviews and instead made it a lazy property.

I also moved the animation into it's own lazy property.

I used this answer and set the gradientLayer's frame to the cell's bounds property (I initially had it set to the cell's frame property)

I added a function that adds the gradientLayer to myView's layer's insertSublayer property and call that function in cellForRow. Also as per @Matt's comments under my answer to prevent the gradientLayer from constantly getting added over again I add a check to see if the gradient is in the UIView's layer's hierarchy (I got the idea from here even though it's used for a different reason). If it isn't there I add and if is I don't add it.

// 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")
    }
}

To call the function to add the gradientLayer to the cell it's called in 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
}

Updated code for the 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
}
}

As a side note this BELOW works great if the users CAN’T scroll the cells (placeholder cells) but if they CAN make sure to test before adding because it's buggy

Another problem I ran into was when I would go to the background and come back the animation wouldn't move. I followed this answer (code below on how to use it) which works although in that same thread I amended that answer to use this answer to start the animation from the beginning which works BUT there are issues.

I noticed even though I came back from the foreground and the animation worked sometimes when I scrolled the animation got stuck. To get around it I called cell.removeGradientLayer() in cellForRow and then again as explained below. However it still got stuck when scrolling but by calling the above it got unstuck. It works for what I need it for because I only show these cells while the actual cells are loading. I'm disabling scrolling when the animation occurs anyway so I don't have to worry about it. FYI this stuck issue only seems to happen when coming back from the background and then scrolling.

I also had to remove the gradientLayer from the cell by calling cell.removeGradientLayer() when the app went to the background and then when it came back to the foreground I had to call cell.addAnimationAndGradientLayer() to add it again. I did that by adding background/foreground Notifications in the class that has the collectionView. In the accompanying Notification functions I just scroll through the visible cells and call the cell's functions that are necessary (code is also below).

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

And in the cell class instead of making MyView and instance of UIView I instead made it an instance of PersistAnimationView like this:

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

Here are the Notifications for the class with the collectionView. The animations also stop when the view disappears or reappears so you’ll have to manage this in viewWillAppear and viewDidDisappear too.

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

这篇关于Swift - GradientLayer 中的动画未出现在单元格中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆