collectionView 中的 CAShapelayer 在 reloadData() 上添加奇怪的动画
CAShapelayer in collectionView adding odd animation on reloadData()
我构建了一个 collectionView,它在每个单元格中显示一个圆形图(以展示百分比)。为此,我在 CustomCell 中投射的子类 (CircularGraph) 中绘制 CAShapelayer。
我遇到的问题是,每当我重新加载我的 collectionView 时,CAShapelayers 都会重新绘制到它们的 strokeEnd 位置,并在这样做时添加不需要的动画。看起来他们是根据上一层的 strokeEnd 值绘制的。这让我相信这与我的子类无法转换图形的离散版本有关。
为了演示这个问题,我构建了一个小型演示项目,可用 here。只需点击重新加载按钮即可查看我指的是什么。每当加载视图时也会发生这种情况,例如切换选项卡时(我想这是有道理的,因为 collectionView 会再次加载它的数据)。
这是集合视图:
class CellsController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let graphValues:[CGFloat] = [0.12, 0.35, 0.14, 1, 0.89]
override func viewDidLoad() {
super.viewDidLoad()
print("Showing cellsController")
collectionView?.backgroundColor = .lightGray
navigationItem.title = "Cells"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Reload", style: .plain, target: self, action: #selector(didPressReload))
collectionView?.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.graph.progressLayerStrokeEnd = graphValues[indexPath.row]
return cell
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return graphValues.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 156)
}
@objc func didPressReload() {
collectionView?.reloadData()
}
}
这是正在投射图形的 CustomCell:
class CustomCell: UICollectionViewCell {
let graph: CircularGraph = {
let graph = CircularGraph()
graph.translatesAutoresizingMaskIntoConstraints = false
return graph
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
addSubview(graph)
graph.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
graph.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
graph.heightAnchor.constraint(equalToConstant: 100).isActive = true
graph.widthAnchor.constraint(equalToConstant: 100).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
这是图表的子类:
class CircularGraph: UIView {
//All layers
let trackLayer = CAShapeLayer()
let progressLayer = CAShapeLayer()
//Animation values
var percentageValue = CGFloat()
//Line width
var lineWidth: CGFloat = 15 { didSet { updatePath() } }
//Fill colors
var trackLayerFillColor: UIColor = .clear { didSet { trackLayer.fillColor = trackLayerFillColor.cgColor } }
var progressLayerFillColor: UIColor = .clear { didSet { progressLayer.fillColor = progressLayerFillColor.cgColor } }
//Stroke colors
var trackStrokeColor: UIColor = UIColor.lightGray { didSet { trackLayer.strokeColor = trackStrokeColor.cgColor } }
var progressLayerStrokeColor: UIColor = UIColor.green { didSet { progressLayer.strokeColor = progressLayerStrokeColor.cgColor } }
//Stroke start and end
var trackLayerStrokeStart: CGFloat = 0 { didSet { trackLayer.strokeStart = trackLayerStrokeStart } }
var progressLayerStrokeStart: CGFloat = 0 { didSet { progressLayer.strokeStart = progressLayerStrokeStart } }
var trackLayerStrokeEnd: CGFloat = 1 { didSet { trackLayer.strokeEnd = trackLayerStrokeEnd } }
var progressLayerStrokeEnd: CGFloat = 1 { didSet { progressLayer.strokeEnd = progressLayerStrokeEnd } }
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
func configure() {
trackLayer.strokeColor = trackStrokeColor.cgColor
trackLayer.fillColor = trackLayerFillColor.cgColor
trackLayer.strokeStart = trackLayerStrokeStart
trackLayer.strokeEnd = trackLayerStrokeEnd
progressLayer.strokeColor = progressLayerStrokeColor.cgColor
progressLayer.fillColor = progressLayerFillColor.cgColor
progressLayer.strokeStart = progressLayerStrokeStart
progressLayer.strokeEnd = progressLayerStrokeEnd
layer.addSublayer(trackLayer)
layer.addSublayer(progressLayer)
}
func updatePath() {
//The actual calculation for the circular graph
let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = (min(bounds.width, bounds.height) - lineWidth) / 2
let circularPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.lineWidth = lineWidth
progressLayer.path = circularPath.cgPath
progressLayer.lineWidth = lineWidth
progressLayer.lineCap = kCALineCapRound
//Set the frame in order to rotate the outer circular paths to start at 12 o'clock
trackLayer.transform = CATransform3DIdentity
trackLayer.frame = bounds
trackLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
progressLayer.transform = CATransform3DIdentity
progressLayer.frame = bounds
progressLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
}
}
我找到了一个基于 CATransaction
的修复程序,您可以遵循这个想法:
var progressLayerStrokeEnd: CGFloat = 1 {
didSet {
CATransaction.begin()
CATransaction.setDisableActions(true)
progressLayer.strokeEnd = progressLayerStrokeEnd
CATransaction.commit()
}
}
基本上,每次修改CAShapeLayer
,都应该禁用隐式CAShapeLayer 动画。因此,您可以根据需要调整 setDisableActions
的标志,例如:在调用 reloadData
时将其设置为 true
我构建了一个 collectionView,它在每个单元格中显示一个圆形图(以展示百分比)。为此,我在 CustomCell 中投射的子类 (CircularGraph) 中绘制 CAShapelayer。
我遇到的问题是,每当我重新加载我的 collectionView 时,CAShapelayers 都会重新绘制到它们的 strokeEnd 位置,并在这样做时添加不需要的动画。看起来他们是根据上一层的 strokeEnd 值绘制的。这让我相信这与我的子类无法转换图形的离散版本有关。
为了演示这个问题,我构建了一个小型演示项目,可用 here。只需点击重新加载按钮即可查看我指的是什么。每当加载视图时也会发生这种情况,例如切换选项卡时(我想这是有道理的,因为 collectionView 会再次加载它的数据)。
这是集合视图:
class CellsController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let graphValues:[CGFloat] = [0.12, 0.35, 0.14, 1, 0.89]
override func viewDidLoad() {
super.viewDidLoad()
print("Showing cellsController")
collectionView?.backgroundColor = .lightGray
navigationItem.title = "Cells"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Reload", style: .plain, target: self, action: #selector(didPressReload))
collectionView?.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.graph.progressLayerStrokeEnd = graphValues[indexPath.row]
return cell
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return graphValues.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 156)
}
@objc func didPressReload() {
collectionView?.reloadData()
}
}
这是正在投射图形的 CustomCell:
class CustomCell: UICollectionViewCell {
let graph: CircularGraph = {
let graph = CircularGraph()
graph.translatesAutoresizingMaskIntoConstraints = false
return graph
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
addSubview(graph)
graph.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
graph.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
graph.heightAnchor.constraint(equalToConstant: 100).isActive = true
graph.widthAnchor.constraint(equalToConstant: 100).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
这是图表的子类:
class CircularGraph: UIView {
//All layers
let trackLayer = CAShapeLayer()
let progressLayer = CAShapeLayer()
//Animation values
var percentageValue = CGFloat()
//Line width
var lineWidth: CGFloat = 15 { didSet { updatePath() } }
//Fill colors
var trackLayerFillColor: UIColor = .clear { didSet { trackLayer.fillColor = trackLayerFillColor.cgColor } }
var progressLayerFillColor: UIColor = .clear { didSet { progressLayer.fillColor = progressLayerFillColor.cgColor } }
//Stroke colors
var trackStrokeColor: UIColor = UIColor.lightGray { didSet { trackLayer.strokeColor = trackStrokeColor.cgColor } }
var progressLayerStrokeColor: UIColor = UIColor.green { didSet { progressLayer.strokeColor = progressLayerStrokeColor.cgColor } }
//Stroke start and end
var trackLayerStrokeStart: CGFloat = 0 { didSet { trackLayer.strokeStart = trackLayerStrokeStart } }
var progressLayerStrokeStart: CGFloat = 0 { didSet { progressLayer.strokeStart = progressLayerStrokeStart } }
var trackLayerStrokeEnd: CGFloat = 1 { didSet { trackLayer.strokeEnd = trackLayerStrokeEnd } }
var progressLayerStrokeEnd: CGFloat = 1 { didSet { progressLayer.strokeEnd = progressLayerStrokeEnd } }
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
func configure() {
trackLayer.strokeColor = trackStrokeColor.cgColor
trackLayer.fillColor = trackLayerFillColor.cgColor
trackLayer.strokeStart = trackLayerStrokeStart
trackLayer.strokeEnd = trackLayerStrokeEnd
progressLayer.strokeColor = progressLayerStrokeColor.cgColor
progressLayer.fillColor = progressLayerFillColor.cgColor
progressLayer.strokeStart = progressLayerStrokeStart
progressLayer.strokeEnd = progressLayerStrokeEnd
layer.addSublayer(trackLayer)
layer.addSublayer(progressLayer)
}
func updatePath() {
//The actual calculation for the circular graph
let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = (min(bounds.width, bounds.height) - lineWidth) / 2
let circularPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.lineWidth = lineWidth
progressLayer.path = circularPath.cgPath
progressLayer.lineWidth = lineWidth
progressLayer.lineCap = kCALineCapRound
//Set the frame in order to rotate the outer circular paths to start at 12 o'clock
trackLayer.transform = CATransform3DIdentity
trackLayer.frame = bounds
trackLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
progressLayer.transform = CATransform3DIdentity
progressLayer.frame = bounds
progressLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
}
}
我找到了一个基于 CATransaction
的修复程序,您可以遵循这个想法:
var progressLayerStrokeEnd: CGFloat = 1 {
didSet {
CATransaction.begin()
CATransaction.setDisableActions(true)
progressLayer.strokeEnd = progressLayerStrokeEnd
CATransaction.commit()
}
}
基本上,每次修改CAShapeLayer
,都应该禁用隐式CAShapeLayer 动画。因此,您可以根据需要调整 setDisableActions
的标志,例如:在调用 reloadData
true