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

Posted

技术标签:

【中文标题】GradientLayer 中的 Swift -Animation 未出现在单元格中【英文标题】:Swift -Animation in GradientLayer not appearing in cell 【发布时间】:2019-04-27 15:13:24 【问题描述】:

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

// the imageWithColor function on the end turns it .lightGray: [https://***.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


【问题讨论】:

这不相关,但您在 layoutSubviews 实现中说 gradientLayer.add(animation, forKey: "...")。但是layoutSubviews 可以在您的应用程序的整个生命周期内被调用数千次。你真的想要每个单元格中都有数千个渐变层吗?这是正在酝酿中的火车残骸。 是的,我忽略了这一点,谢谢。我最初在 init 方法中添加了代码,当它不起作用时,我认为 UIView 的框架可能还没有准备好。这就是为什么我将它添加到 layoutSubviews 而不考虑其他任何事情。感谢您的洞察力! 另一个可能有用的见解:您不能为不在层层次结构中已经的层设置动画。这不仅仅是顺序问题。您不能为将其添加到层层次结构的层在相同的代码中(即相同的事务)制作动画。 你是说它们必须在不同的时间添加。比如调用func1添加渐变,然后调用func2添加动画? 这是另一个问题。你说的是addSubview(myView),意思是self. addSubview(myView)。但是selfFoodCell: UICollectionViewCell。禁止将视图直接添加到 UICollectionViewCell;您必须将其添加到单元格的contentView。如果您的目标是在其他所有内容后面显示视图,这就是单元格的 backgroundView 的用途。 【参考方案1】:

我搞定了。

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

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

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

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

我添加了一个函数,将 gradientLayer 添加到 myView 层的 insertSublayer 属性并在 cellForRow 中调用该函数。同样根据@Matt's cmets 在我的回答下,为了防止gradientLayer 不断被再次添加,我添加了一个检查以查看渐变是否在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


附带说明,如果用户无法滚动单元格(占位符单元格),但如果他们可以确保在添加之前进行测试,因为它有问题

我遇到的另一个问题是当我转到后台并返回时动画不会移动。我跟着this answer(下面的代码说明如何使用它)虽然在同一个线程中我修改了答案以使用this answer从头开始动画,但它可以工作但是有问题.

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

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

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
    

在单元格类中,我没有创建 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

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

【讨论】:

好的,但是你的代码仍然是错误的,原因和以前一样。 cellForItemAt 将多次收到同一个单元格,因为单元格被重用。因此,您将向同一个单元格添加许多渐变层。 你知道你的权利。这发生在我之前正在从事的一个项目中。让我很沮丧。我所做的是创建一个删除渐变的函数,然后在将图像添加到图层之前在 cellForRow 中调用该函数。所以 1.cell.removeGradient() 2.cell.myView.layer.contents = foodImages[indexPath.item].cgImage 和 3.cell.addGradientLayer()。我不确定这是最优雅的方法,但它阻止了问题的发生。我也可以在 prepareForReuse 中删除它,但你的书和很多帖子都说不要这样做。 但我的书也告诉你正确的答案。 有条件地添加渐变层。只需查看查看该单元格中是否已经存在渐变层,如果存在,则不要再次添加。 @matt 我更新了代码以检查 gradientLayer 是否在 myView 的层次结构中。有用。谢谢 @matt 不确定您是否有时间查看此内容,但我必须做大量工作才能使这些动画正常工作。转到后台并返回它们停止,切换它们停止的选项卡,切换选项卡,转到后台,然后返回,然后返回到它们停止的此选项卡。动画是很多工作的人。我不得不不断地添加和删除它们。【参考方案2】:

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

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

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

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

看来您的问题可能是 layoutSubviews 可以调用很多,但只有在使用框架初始化视图时才会调用 init 函数。此外,将设置代码放在它自己的函数中可以更轻松地执行其他操作,例如在框架发生变化时更新渐变层的框架。

【讨论】:

嘿,谢谢,我之前确实尝试过,但我做的有点不同。我创建了该函数,然后从 cellForRow 调用它。它没有用。感谢您的帮助 我只是按照您的方式尝试过,但没有。不过谢谢 没问题 :) 对不起,我帮不上忙! 我刚刚开始使用它,如果可以,请检查一下。干杯! 太棒了,这是一个了不起的解决方案!

以上是关于GradientLayer 中的 Swift -Animation 未出现在单元格中的主要内容,如果未能解决你的问题,请参考以下文章

swift中的CAGradientLayer相当于啥?

GradientLayer 显示在按钮外

GradientLayer显示在按钮外部

Swift - 重用渐变的代码效率最高的方法?

在三个按钮之间同时添加/删除 GradientLayer

UILabel 添加 GradientLayer [重复]