如何为Charts库的PieChart扇区实现高亮动画效果?
实现PieChart单个扇区高亮的过渡动画
我在使用PieChartView时,发现高亮扇区的时候,扇区会立刻切换成数据集的
highlightColor并应用selectionShift,我想给这个过渡加上动画效果。目前只找到ChartViewBase的animate方法,但它是对整个图表做动画,不是我要的。请问能不能只对单个选中的扇区执行动画?我已经写了初始代码,但没办法给entry或者highlight应用transform:func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) { let translation1 = CGAffineTransform(scaleX: 1.5, y: 1.5) let translation2 = CGAffineTransform(translationX: 1.0, y: 1.0) UIView.animate(withDuration: 0.4, animations: { in entry.transform = translation1 }) { didComplete in UIView.animate(withDuration: 0.4, animations: { entry.transform = translation2 }) } }
这个问题我之前也踩过坑——Charts库的ChartDataEntry本身不支持通过transform属性驱动UI动画,因为渲染逻辑根本不监听这个值的变化。要实现单个扇区的高亮过渡,得从渲染层或者图层入手,下面给你两个实用的方案:
方案一:自定义渲染器(推荐,兼容性强)
这个方法直接重写PieChart的渲染逻辑,完全掌控扇区的动画过程,不会受库版本更新的影响:
步骤1:创建自定义渲染器类
继承PieChartRenderer,在绘制时根据动画进度调整高亮扇区的偏移和颜色:
class AnimatedPieChartRenderer: PieChartRenderer { // 记录当前高亮的扇区索引 var highlightedIndex: Int? // 动画进度(0到1) var animationProgress: CGFloat = 0.0 override func drawDataSet(context: CGContext, dataSet: IChartDataSet) { guard let pieDataSet = dataSet as? PieChartDataSet else { super.drawDataSet(context: context, dataSet: dataSet) return } let entryCount = pieDataSet.entryCount guard entryCount > 0 else { return } let center = centerCircleBox let radius = min(viewPortHandler.contentWidth, viewPortHandler.contentHeight) / 2.0 let drawAngles = self.drawAngles let absoluteAngles = self.absoluteAngles for i in 0..<entryCount { let sliceAngle = drawAngles[i] let rotationAngle = absoluteAngles[i] let isHighlighted = i == highlightedIndex // 根据动画进度计算偏移量 var shift = pieDataSet.selectionShift if isHighlighted { shift *= animationProgress } // 绘制扇区路径 context.saveGState() context.translateBy(x: center.x, y: center.y) context.move(to: .zero) let shiftedRadius = radius + shift context.addArc(center: .zero, radius: shiftedRadius, startAngle: rotationAngle, endAngle: rotationAngle + sliceAngle, clockwise: false) context.closePath() // 过渡颜色:未高亮用原颜色,高亮时根据进度过渡到highlightColor let targetColor = isHighlighted ? pieDataSet.highlightColor ?? pieDataSet.color(atIndex: i) : pieDataSet.color(atIndex: i) let currentColor = pieDataSet.color(atIndex: i).blended(withFraction: animationProgress, of: targetColor) context.setFillColor(currentColor.cgColor) context.fillPath() context.restoreGState() } } }
步骤2:在ViewController中集成并触发动画
把自定义渲染器绑定到PieChartView,然后在代理方法中启动/停止动画:
class YourChartVC: UIViewController, ChartViewDelegate { let pieChart = PieChartView() var animatedRenderer: AnimatedPieChartRenderer! var currentHighlightedIndex: Int? override func viewDidLoad() { super.viewDidLoad() // 初始化图表布局、数据(这部分按你原来的逻辑来) setupPieChartData() // 替换默认渲染器 animatedRenderer = AnimatedPieChartRenderer(chart: pieChart, animator: pieChart.chartAnimator, viewPortHandler: pieChart.viewPortHandler) pieChart.renderer = animatedRenderer pieChart.delegate = self } func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) { guard let pieEntry = entry as? PieChartDataEntry else { return } // 获取选中扇区的索引 let selectedIndex = pieChart.data?.dataSets[0].entries.firstIndex(of: pieEntry) ?? -1 guard selectedIndex != -1 else { return } // 避免重复触发同一扇区的动画 if selectedIndex == currentHighlightedIndex { return } currentHighlightedIndex = selectedIndex animatedRenderer.highlightedIndex = selectedIndex // 执行高亮动画 UIView.animate(withDuration: 0.4) { self.animatedRenderer.animationProgress = 1.0 self.pieChart.setNeedsDisplay() } } func chartValueNothingSelected(_ chartView: ChartViewBase) { // 取消高亮时的反向动画 UIView.animate(withDuration: 0.4) { self.animatedRenderer.animationProgress = 0.0 self.pieChart.setNeedsDisplay() } _ in self.currentHighlightedIndex = nil self.animatedRenderer.highlightedIndex = nil } } }
方案二:直接操作CALayer(简洁但依赖内部实现)
如果不想写自定义渲染器,可以尝试直接获取扇区对应的图层,给图层加动画。注意:这个方法依赖Charts库的内部图层命名,升级版本时需要验证:
func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) { guard let pieChart = chartView as? PieChartView, let dataSet = pieChart.data?.dataSets.first as? PieChartDataSet else { return } let selectedIndex = dataSet.entries.firstIndex(of: entry) ?? -1 guard selectedIndex != -1 else { return } // 找到扇区对应的图层(Charts默认给扇区图层命名包含"slice") let sliceLayers = pieChart.layer.sublayers?.filter { $0.name?.contains("slice") ?? false } guard let targetLayer = sliceLayers?[selectedIndex] else { return } // 缩放动画:模拟selectionShift的偏移效果 let scaleAnim = CABasicAnimation(keyPath: "transform.scale") scaleAnim.fromValue = 1.0 scaleAnim.toValue = 1.0 + (dataSet.selectionShift / pieChart.frame.width * 2) scaleAnim.duration = 0.4 scaleAnim.fillMode = .forwards scaleAnim.isRemovedOnCompletion = false // 颜色过渡动画 let colorAnim = CABasicAnimation(keyPath: "fillColor") colorAnim.fromValue = dataSet.color(atIndex: selectedIndex).cgColor colorAnim.toValue = dataSet.highlightColor?.cgColor ?? dataSet.color(atIndex: selectedIndex).cgColor colorAnim.duration = 0.4 colorAnim.fillMode = .forwards colorAnim.isRemovedOnCompletion = false targetLayer.add(scaleAnim, forKey: "highlightScale") targetLayer.add(colorAnim, forKey: "highlightColor") } func chartValueNothingSelected(_ chartView: ChartViewBase) { guard let pieChart = chartView as? PieChartView else { return } let sliceLayers = pieChart.layer.sublayers?.filter { $0.name?.contains("slice") ?? false } sliceLayers?.forEach { layer in layer.removeAnimation(forKey: "highlightScale") layer.removeAnimation(forKey: "highlightColor") } }
小提示:
- 方案一的兼容性更好,适合长期维护的项目;
- 方案二更快上手,但如果Charts库更新时改了图层命名,就得跟着调整;
- 不管用哪个方案,都别忘了在
chartValueNothingSelected中处理取消高亮的动画,保证交互流畅。
内容的提问来源于stack exchange,提问作者nb07
相关产品推荐
相关产品推荐

