You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:42:20