图表封装
1. 业务
- 可滚动的平滑曲线。
- 选中状态: 位置在中心,点/标题/y轴字体是黑体加粗,其他为未选中状态,颜色字体淡一些。
- 滚动到中间位置的点显示为选中状态。
- 可以点击某个点成为选中状态,并滚动到试图中间。
2.思路
- 滚动视图用UIScrollView横向滚动
- 使用 CAShapeLayer + UIBezierPath 绘制曲线图表
- 监听滚动结束后的位置,使最近的点成为选中状态。
- 监听点击的位置,使最近的点成为选中状态。
3.细节
- 监听UIScrollView滚动结束
滚动结束的状态有3种,1.滚动减速停止 2.滚动按压停止 3.滚动上下滑动停止。
根据 tracking、dragging、decelerating这3个属性监听停止。func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { // 拖拽停止 if !decelerate { let dragToDragStop = scrollView.isTracking && !scrollView.isDragging && !scrollView.isDecelerating if dragToDragStop { self.cgo_scrollViewDidEndScroll(scrollView) } } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { /// 快速滑动自由停止/按压停止 let scrollToScrollStop = !scrollView.isTracking && !scrollView.isDragging && !scrollView.isDecelerating if scrollToScrollStop { self.cgo_scrollViewDidEndScroll(scrollView) } }
- UIScrollView点击事件
UIScrollView对于touch事件的接收处理原理:UIScrollView重载hitTest 方法,并总会返回itself 。所以所有的touch 事件都会进入到它自己里面去了,所以UIScrollView及其子视图是不响应touchBegan事件的
这里有两种方式,第一种给UIScrollView添加点击手势。在手势结束是处理点击事件let tap = UITapGestureRecognizer(target: self, action: #selector(tapEvent(_:))) self.chartView.addGestureRecognizer(tap)
@objc func tapEvent(_ gestureRecognizer: UIGestureRecognizer) { switch gestureRecognizer.state { case .ended: do { let point = gestureRecognizer.location(in: self.chartView) let index = self.calculatePosition(offset: point.x) guard let pointIndex = index else { return } debugPrint("ponitIndex === \(pointIndex)") self.updateRow(index: pointIndex) // self.lastRow = ponitIndex self.adustMidPosition(isDrag: false) self.delegate?.chartView(self, didSelectedRowWithIndex: pointIndex) } break default: break } }
第二种是重写touchbegan一系列事件,单独处理点击事件和移动事件,点击事件自己处理,移动事件交给UIScrollView滚动结束处理,但是这里长按也会被统计为移动事件,导致一个小问题。
class CGOChartScrollView: UIScrollView { public var isDrag : Bool = false public var tapEvent : ((Set<UITouch>)-> Void)? override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesBegan(touches, with: event) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { self.isDrag = true super.touchesMoved(touches, with: event) } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if self.isDrag { super.touchesEnded(touches, with: event) } else { if let event = self.tapEvent { event(touches) } } self.isDrag = false } }
- UIScrollView setContentOffset:animated与contentOffset的区别
setContentOffset有两种方法:setContentOffset:和setContentOffset:animated:
但是两者还是有点差异的:
setContentOffset:animated: 这种方法,无论animated为YES还是NO, 都会等待scrollView的滚动结束以后才会执行,也就是当isDragging和isDecelerating为YES的时候,会等待滚动完成才执行上面的方法。
setContentOffset:这种方法则不受scrollView是否正在滚动的限制。
所以我在滚动结束后和点击事件使用的动画是不同的,需要判断是否是拖拽手势isDrag
设置setContentOffset的动画if isDrag { self.chartView.setContentOffset(CGPoint(x: m_offset, y: 0), animated: true) } else { UIView.animate(withDuration: 0.3, delay: 0, options: UIView.AnimationOptions.curveEaseInOut, animations: { self.chartView.contentOffset = CGPoint(x: m_offset, y: 0) }, completion: nil) }
- 滑动或者点击后选中状态的绘制。
- 第一次进入时只绘制可见部分,后面滚动到什么位置就绘制到哪里。
4.接口
- 数据接口设计
避免数据的耦合,可以使用多种方式,比如UITableVIew的dataSource,我这里使用强制类型CGOChartData
class CGOChartDataSet : NSObject { public var title : String = "" public var prefix : String = "R$" public var yAixsText : String = "" public var isSelected : Bool = false } class CGOChartData : NSObject { public var dataSet : CGOChartDataSet? public var point : CGPoint = CGPoint.zero }
dataSet是对数据的设置
使用的时候需要进行转换var data : Array<CGOChartData> = Array<CGOChartData>() for (index,value) in a.enumerated() { let d = CGOChartData() let set = CGOChartDataSet() d.point = self.chartView.getPoint(index: index, value: value.consumption, count: 6) set.title = "\(value.consumption)" set.isSelected = value.isSelected set.yAixsText = "\(value.month)" d.dataSet = set data.append(d) } self.chartView.points = data
- 对外接口设计
选中状态完成时的接口,这里包括点击选中和滚动后选中
// 接口 protocol CGOLineChartViewProtocol : NSObjectProtocol { // 选中结束事件 func chartView(_ chartView: CGOLineChartView, didSelectedRowWithIndex: NSInteger) -> Void }