导读:本期聚焦于松本一香创作的《iOS股票分时图成交量柱状图如何只给顶部绘制圆角、底部保持直角?》,敬请观看详情。分时图下方的成交量柱状图是股票类App的标配组件,但很多绘制方案直接用UIBezierPath统一加圆角,导致柱子底部也出现弧度,与坐标轴贴合时显得不自然。本文围绕Core Graphics的路径绘制原理,详细讲解如何只为柱状图顶部两角添加圆角,而底部保持平角的实现方法,包括move与addLine手动构建路径、addArc精确控制圆弧角度、根据涨跌颜色区分红绿柱的处理技巧,以及批量绘制时的性能优化思路,帮助你绘制出与专业行情软件一致的成交量柱状效果。

在iOS股票分时图的开发中,上方的价格曲线通常只占整个视图的一部分,下方还需要配合一个成交量副图区域。成交量以一根根细密的柱状图呈现,红色代表当前分钟上涨,绿色代表下跌。仔细观察专业的行情软件会发现一个细节:这些成交量柱的顶部两个角是圆角,而底部紧贴横坐标轴的位置是直角。如果简单地使用UIBezierPath(roundedRect:)统一设置圆角,底部也会出现弧度,视觉上与坐标轴之间产生缝隙。本文将详细讲解如何用Core Graphics精确控制圆角的位置,只对顶部做圆角处理。

iOS股票分时图成交量柱状图如何只给顶部绘制圆角、底部保持直角?

为什么统一圆角方案不可行

最直观的思路是使用UIKit提供的便捷方法,对每根柱子创建一个带圆角的矩形路径然后填充:

let path = UIBezierPath(roundedRect: barRect,
                        byRoundingCorners: [.topLeft, .topRight],
                        cornerRadii: CGSize(width: 2, height: 2))
UIColor.red.setFill()
path.fill()</code>

这个方法其实已经支持byRoundingCorners参数,可以指定只圆化顶部两角,UIKit内部会调用Core Graphics完成绘制。它的优点是代码简洁、可读性好,对于几十根柱子的场景完全够用。

但它的局限在于灵活性不足。分时图中每根柱子的宽度往往只有2到4个点,圆角半径一旦接近柱宽的一半,路径的形状会开始失真;另外某些设计稿要求顶部的红柱和绿柱圆角半径不同,或者需要在柱体内部再做渐变填充,此时便捷方法就难以满足需求。更底层的做法是直接操作Core Graphics的路径API,手动构建这个“顶部圆角、底部平角”的形状,这也是本文的重点。

用Core Graphics手动构建顶部圆角路径

Core Graphics中构建自定义路径的核心是CGMutablePath。绘制一个顶部圆角、底部直角的矩形,本质上是从左下角出发,沿逆时针或顺时针方向依次连接四条边,其中顶部的两个角用圆弧代替直角转折。圆弧使用addArc方法添加,需要明确圆心坐标、半径、起始角度和结束角度。

这里以顺时针方向为例,从左下角开始绘制。左下角是直角,直接move到该点;向上画到左侧圆弧的起点,也就是左上角圆角圆心向下偏移一个半径的位置;接着用一段四分之一圆弧过渡到顶部;沿顶部直线到右侧圆弧起点;再用一段圆弧过渡到右侧边;最后向下画回底部,closeSubpath闭合路径。具体代码如下:

func roundedTopBarPath(rect: CGRect, radius: CGFloat) -> CGPath {
    let path = CGMutablePath()
    // 半径不能超过柱宽和柱高各自的一半,否则形状会异常
    let r = min(radius, rect.width / 2, rect.height / 2)
    
    // 从左下角起点,柱子底部保持直角
    path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
    // 左侧直边向上,到圆弧起点
    path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + r))
    // 左上角圆弧:圆心为 (minX + r, minY + r),从180度转到270度
    path.addArc(center: CGPoint(x: rect.minX + r, y: rect.minY + r),
                radius: r,
                startAngle: .pi,
                endAngle: .pi * 1.5,
                clockwise: false)
    // 顶部直线
    path.addLine(to: CGPoint(x: rect.maxX - r, y: rect.minY))
    // 右上角圆弧:从270度转到360度(即0度)
    path.addArc(center: CGPoint(x: rect.maxX - r, y: rect.minY + r),
                radius: r,
                startAngle: .pi * 1.5,
                endAngle: .pi * 2,
                clockwise: false)
    // 右侧直边向下,回到右下角(直角)
    path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
    // 闭合路径,底部两个角均为直角
    path.closeSubpath()
    return path
}

需要特别注意的是Core Graphics的坐标系问题。UIView的draw(_ rect: CGRect)方法中,系统已经将坐标系翻转为我们熟悉的原点在左上角的形式,角度的约定是:0度指向正右方,.pi指向左方,.pi * 1.5指向上方。上面代码中左上角圆弧从.pi(正左)逆时针参数设置为clockwise: false,转到.pi * 1.5(正上),正好是四分之一圆。如果角度算错,绘制出来的圆弧会凸向柱子内部,形成“凹陷”的怪异形状,这是初学者最常踩的坑。

另一个容易忽略的细节是对半径的钳制处理。分时图中成交量柱的高度差异很大,开盘初期某一分钟的成交量可能极小,柱高甚至只有一两个点。如果传入的圆角半径大于柱高的一半,圆弧的圆心会落到柱体外部,路径自相交后填充结果不可预期。所以代码第一行用min函数对半径做了保护,这是保证绘制稳定性的关键一步。

结合涨跌颜色绘制完整的成交量副图

解决了单根柱子的路径问题,接下来把它放入完整的绘制流程。分时图的数据源通常是一个包含时间、价格、成交量的数组,红绿颜色的判定规则是:当前分钟价格大于上一分钟则为红,小于则为绿,相等时沿用前一根的颜色(不同券商的规则略有差异,需与产品确认)。

draw(_ rect: CGRect)中遍历数据源,为每根柱子计算矩形区域并调用上面的路径函数。计算X坐标时,用总宽度除以柱子数量得到间距,柱子实际宽度取间距的一定比例(如60%),这样柱子之间能留出呼吸感:

override func draw(_ rect: CGRect) {
    guard let context = UIGraphicsGetCurrentContext(), volumes.count > 0 else { return }
    
    let count = max(volumes.count, 240)
    let step = rect.width / CGFloat(count)
    let barWidth = step * 0.6
    let maxVolume = volumes.map { $0 }.max() ?? 1
    
    for (index, volume) in volumes.enumerated() {
        // 根据涨跌确定颜色
        let isUp = prices[index] >= prices[max(0, index - 1)]
        let color = isUp ? UIColor(red: 0.85, green: 0.2, blue: 0.24, alpha: 1)
                         : UIColor(red: 0.13, green: 0.63, blue: 0.33, alpha: 1)
        
        let barHeight = CGFloat(volume) / CGFloat(maxVolume) * rect.height
        let x = CGFloat(index) * step + (step - barWidth) / 2
        let barRect = CGRect(x: x, y: rect.height - barHeight,
                             width: barWidth, height: barHeight)
        
        context.addPath(roundedTopBarPath(rect: barRect, radius: barWidth / 2))
        context.setFillColor(color.cgColor)
        context.fillPath()
    }
}

注意柱子的Y坐标计算:分时图坐标原点在左上角,柱子要从底部向上生长,所以y值是rect.height - barHeight。成交量归一化时使用当前已显示数据中的最大值作为基准,这样开盘初期数据量少时,柱子不会被过度放大;等数据积累到全天240根后,整体比例会自动趋于稳定。

性能优化与高频刷新的处理

分时图的特点是需要定时刷新,盘中通常每几秒钟追加或更新一根柱子,如果每次都完整重绘240根柱子,在低端设备上可能带来不必要的开销。优化的方向有两个层面。

第一个层面是避免不必要的setNeedsDisplay调用。可以将成交量柱分层:已经确定的历史柱子绘制到一张CGLayer或离屏位图中缓存起来,每次刷新时只把缓存位图贴回视图,再增量绘制最新的几根柱子。当新的一分钟开始时才重建整张缓存,这样大部分刷新周期内的绘制量从240根降到1根左右。

第二个层面是路径合并。Core Graphics每次fillPath都有上下文状态切换的成本,可以把相同颜色的柱子路径全部addPath到同一个CGMutablePath里,最后分别用红色和绿色各执行一次fillPath,绘制调用次数从240次降到2次。实测这种合并策略在中端机型上可以将单帧绘制耗时降低约一半,对于滚动流畅度的提升相当明显。

最后提一点兼容性建议:如果项目最低支持iOS 13以上,也可以考虑用CAShapeLayer承载柱状图,把路径生成逻辑复用过去,配合CADisplayLink做柱子生长动画会更容易。但对于纯静态的高密度柱状图,直接在draw(_ rect: CGRect)中用Core Graphics绘制仍然是最轻量、最可控的方案,这也是主流行情软件普遍采用的方式。

Core Graphics分时图圆角绘制修改时间:2026-08-31 16:17:14

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。