我有不同的食物的不同图像,我添加到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
中,我添加了一个带有动画效果的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: "...")
}
然后在你的init函数中调用它
override init(frame: CGRect) {
super.init(frame: frame)
setUpGradient()
}
看起来您的问题可能是layoutSubviews可以被调用很多,但只有在使用框架初始化视图时才会调用init函数。将设置代码放在自己的函数中也可以更容易地执行其他操作,例如在帧更改时更新渐变图层的帧。
我搞定了。
我在问题的评论中提到了@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
}
}
作为旁注,如果用户不能滚动单元格(占位符单元格),那么BELOW可以很好地工作但是如果他们可以确保在添加之前进行测试,因为它是错误的
我遇到的另一个问题是当我去背景并回来时动画不会移动。我按照this answer(下面的代码如何使用它)工作,虽然在同一个线程我修改了答案使用this answer从一开始就开始动画但是有问题。
我注意到即使我从前台回来并且有时当我滚动动画时动画仍然有效。为了解决这个问题,我在cell.removeGradientLayer()
打电话给cellForRow
然后再解释如下。但是在滚动时它仍然卡住了,但是通过调用上面的内容它会被取消。它适用于我需要它,因为我只在实际单元格加载时显示这些单元格。无论如何,当动画发生时我禁用滚动,所以我不必担心它。仅在从背景返回然后滚动时,这个问题才会发生。
当应用程序进入后台时,我还必须通过调用cell.removeGradientLayer()
从单元格中删除gradientLayer,然后当它返回到前台时,我不得不再次调用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
}
}
而在细胞类而不是制作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 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()
}
}
}
}
}