自定义ScrollView用UIPanGestureRecognizer滚动时上下反复抖动怎么办
问题描述
我尝试参考矢量插画软件的设计思路,用UIBezier和UILabel实现类似日历应用的时间线效果,再通过UIPanGestureRecognizer实现上下滚动功能。但在模拟器中滚动时,内容没有按预期正常移动,反而反复叠加渲染(我在scrollValue变化时调用了setNeedsDisplay方法重绘整个视图),大概率是代码某处的小问题导致的。
我知道可以直接用UIScrollView或者UITableView实现这个效果,但我平时习惯用CAD绘图和矢量插画工具,想尝试自定义实现表格作为小挑战,我觉得直接用现成组件限制太多。
模拟器中的问题效果如下图:
我用的实现代码如下:
import UIKit class ViewController: UIViewController { var tlobject = TimelineView() let gesto = UIPanGestureRecognizer() override func viewDidLoad() { super.viewDidLoad() // ===== 把TimelineView添加到父视图 let TLObjectFrame = CGRect(x: 0, y: 40, width: 100, height: 100) tlobject = TimelineView(frame: TLObjectFrame) view.addSubview(tlobject) // ===== 添加拖动手势 gesto.addTarget(self, action: #selector(touchinput)) view.addGestureRecognizer(gesto) } var touchStartLocation: Int = 0 var scrollDistance: Int = 0 var lastScrollDistance: Int = 0 // 以下方法计算Y轴上的拖动距离,把结果值scrollDistance传给时间线组件,用来调整每个UIBezier的Y坐标。感谢Youtube上Mitchell Hudson的教程"06 11 touches value"提供的实现思路 @objc func touchinput (sender: UIPanGestureRecognizer) { if sender.state == UIGestureRecognizer.State.began { touchStartLocation = Int(sender.location(in: view).y) lastScrollDistance = scrollDistance } if sender.state == UIGestureRecognizer.State.changed { let touchEndLocation = Int(sender.location(in: view).y) let currentScrollDistance = touchEndLocation - touchStartLocation print("deltaY", currentScrollDistance) var newScrollDistance = lastScrollDistance + currentScrollDistance scrollDistance = newScrollDistance tlobject.totalScrollDistance = scrollDistance //把滚动值传给TimelineView } if sender.state == UIGestureRecognizer.State.ended { print("lastScrollDistance", lastScrollDistance) print("scroll Distance", scrollDistance) } } } // 自定义时间线视图 class TimelineView: UIView { var totalScrollDistance: Int = 0 { didSet{ setNeedsDisplay() //每次手势位置变化都会触发重绘 } } override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = UIColor.clear } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func draw(_ rect: CGRect) { timelinemechanism() } func timelinemechanism() { let lineElements: Array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] let spacing: Int = 30 let scrollDistance: Int = totalScrollDistance let totalElements: Int = lineElements.count for n in 1...totalElements { // 计算UILabel/分割线的屏幕Y坐标 = 数组下标 * 间距 + 手势滚动距离 let yPosition = lineElements[n - 1] * spacing + scrollDistance let linepath = UIBezierPath() linepath.move(to: CGPoint(x: 60, y: yPosition)) linepath.addLine(to: CGPoint(x: 300, y: yPosition)) let lineshape = CAShapeLayer() lineshape.path = linepath.cgPath lineshape.strokeColor = UIColor.blue.cgColor self.layer.addSublayer(lineshape) let hourlabel = UILabel() hourlabel.frame = CGRect(x: 5, y: yPosition - 20, width: 45, height: 40) hourlabel.text = "\(n):00" hourlabel.textColor = UIColor.blue hourlabel.textAlignment = NSTextAlignment.right self.addSubview(hourlabel) } } }
问题原因
- 每次滚动触发
setNeedsDisplay()后,timelinemechanism()方法都会新建10个CAShapeLayer和10个UILabel添加到视图上,旧的图层和标签没有被移除,多次调用后就会出现内容叠加的问题 - 额外小问题:初始化
TimelineView时设置的宽度只有100,但是分割线的终点x坐标是300,超出视图范围的内容会显示异常。
修复方案
- 每次执行
timelinemechanism()前,先移除之前添加的所有自定义子视图和自定义图层 - 调整
TimelineView的初始化宽度,适配内容显示范围 - 可选:开启视图的
clipsToBounds,避免超出视图范围的内容显示
修改后完整代码
import UIKit class ViewController: UIViewController { var tlobject = TimelineView() let gesto = UIPanGestureRecognizer() override func viewDidLoad() { super.viewDidLoad() // 调整TimelineView的宽高适配屏幕和内容 let TLObjectFrame = CGRect(x: 0, y: 40, width: 350, height: UIScreen.main.bounds.height - 80) tlobject = TimelineView(frame: TLObjectFrame) // 开启裁剪,超出视图范围的内容不显示 tlobject.clipsToBounds = true view.addSubview(tlobject) gesto.addTarget(self, action: #selector(touchinput)) view.addGestureRecognizer(gesto) } var touchStartLocation: Int = 0 var scrollDistance: Int = 0 var lastScrollDistance: Int = 0 @objc func touchinput (sender: UIPanGestureRecognizer) { if sender.state == .began { touchStartLocation = Int(sender.location(in: view).y) lastScrollDistance = scrollDistance } if sender.state == .changed { let touchEndLocation = Int(sender.location(in: view).y) let currentScrollDistance = touchEndLocation - touchStartLocation let newScrollDistance = lastScrollDistance + currentScrollDistance scrollDistance = newScrollDistance tlobject.totalScrollDistance = scrollDistance } } } class TimelineView: UIView { var totalScrollDistance: Int = 0 { didSet{ setNeedsDisplay() } } override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = .clear } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func draw(_ rect: CGRect) { timelinemechanism() } func timelinemechanism() { // 清空之前添加的所有子标签 subviews.forEach { $0.removeFromSuperview() } // 清空之前添加的所有分割线图层 layer.sublayers?.filter { $0 is CAShapeLayer }.forEach { $0.removeFromSuperlayer() } let lineElements = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] let spacing = 30 let totalElements = lineElements.count for n in 1...totalElements { let yPosition = lineElements[n - 1] * spacing + totalScrollDistance let linepath = UIBezierPath() linepath.move(to: CGPoint(x: 60, y: yPosition)) linepath.addLine(to: CGPoint(x: 300, y: yPosition)) let lineshape = CAShapeLayer() lineshape.path = linepath.cgPath lineshape.strokeColor = UIColor.blue.cgColor layer.addSublayer(lineshape) let hourlabel = UILabel() hourlabel.frame = CGRect(x: 5, y: yPosition - 20, width: 45, height: 40) hourlabel.text = "\(n):00" hourlabel.textColor = .blue hourlabel.textAlignment = .right addSubview(hourlabel) } } }
内容的提问来源于stack exchange,提问作者KikoPerdido
相关产品推荐
相关产品推荐

