iOS自定义绘图:如何在两条折线间正确填充颜色?
问题描述
我拥有三个数组xValues、yMinValues和yMaxValues,想要绘制一张图表,需基于yMinValues和yMaxValues数组绘制两条折线,并在两条折线之间填充颜色。目前已完成折线绘制,但填充区域不符合预期,期望实现的效果是仅在两条折线之间填充颜色,不要填充外部区域。
原实现代码如下:
class CustomView: UIView { let xValues: [CGFloat] = [0, 2, 4, 6, 8, 10, 12, 14] let yMaxValues: [CGFloat] = [4, 6, 9, 10, 10, 11, 10, 11] let yMinValues: [CGFloat] = [2, 4, 5, 6, 7, 8, 9, 11] override func draw(_ rect: CGRect) { super.draw(rect) guard let context = UIGraphicsGetCurrentContext() else { return } // Arka planı beyaz yapın UIColor.white.setFill() context.fill(rect) // Grafik boyutlarını alın let graphWidth = rect.width let graphHeight = rect.height // Min ve max değerleri belirleyin let minValue = min(yMinValues.min() ?? 0.0, yMinValues.max() ?? 0.0) let maxValue = max(yMaxValues.min() ?? 0.0, yMaxValues.max() ?? 0.0) // Y değerlerini ölçeklendirin let yScaleFactor = graphHeight / (maxValue - minValue) // X değerlerini ölçeklendirin let xScaleFactor = graphWidth / CGFloat(xValues.count - 1) // Grafik alanını doldurun let yFillValuesPath = UIBezierPath() // YFillValues için line çizin for i in 0..<xValues.count { let x = CGFloat(i) * xScaleFactor let yFill = graphHeight - (yMinValues[i] - minValue) * yScaleFactor // Burada yMinValues kullanılmalı if i == 0 { yFillValuesPath.move(to: CGPoint(x: x, y: yFill)) } else { yFillValuesPath.addLine(to: CGPoint(x: x, y: yFill)) } // Noktalara yFillValues değerlerini yaz let pointLabel = UILabel(frame: CGRect(x: x - 15, y: yFill - 30, width: 30, height: 30)) pointLabel.text = "\(yMinValues[i])" pointLabel.textColor = UIColor.black pointLabel.textAlignment = .center pointLabel.font = UIFont.systemFont(ofSize: 10) addSubview(pointLabel) } // Grafik alanını doldurun let fillColor = UIColor(red: 0.5, green: 0.5, blue: 1.0, alpha: 0.5) fillColor.setFill() yFillValuesPath.addLine(to: CGPoint(x: yMaxValues.last ?? 0, y: graphHeight - (yMaxValues.last ?? 0 - minValue) * yScaleFactor)) // yMinValues kullanılmalı yFillValuesPath.addLine(to: CGPoint(x: yMinValues.first ?? 0, y: graphHeight - (yMinValues.first ?? 0 - maxValue) * yScaleFactor)) // yMinValues kullanılmalı yFillValuesPath.fill() yFillValuesPath.close() // YValues için line çizin let yValuesPath = UIBezierPath() for i in 0..<xValues.count { let x = CGFloat(i) * xScaleFactor let y = graphHeight - (yMaxValues[i] - minValue) * yScaleFactor if i == 0 { yValuesPath.move(to: CGPoint(x: x, y: y)) } else { yValuesPath.addLine(to: CGPoint(x: x, y: y)) } // Noktalara yValues değerlerini yaz let pointLabel = UILabel(frame: CGRect(x: x - 15, y: y - 30, width: 30, height: 30)) pointLabel.text = "\(yMaxValues[i])" pointLabel.textColor = UIColor.black pointLabel.textAlignment = .center pointLabel.font = UIFont.systemFont(ofSize: 10) addSubview(pointLabel) } // Çizgileri çizin let lineColor = UIColor.black.cgColor context.setStrokeColor(lineColor) yValuesPath.stroke() yFillValuesPath.stroke() // X ekseni çizin context.move(to: CGPoint(x: 0, y: graphHeight)) context.addLine(to: CGPoint(x: graphWidth, y: graphHeight)) context.strokePath() // Y ekseni çizin context.move(to: CGPoint(x: 0, y: graphHeight)) context.addLine(to: CGPoint(x: 0, y: 0)) context.strokePath() // X değerlerini gösterin for i in 0..<xValues.count { let x = CGFloat(i) * xScaleFactor let label = UILabel(frame: CGRect(x: x - 15, y: graphHeight + 5, width: 30, height: 20)) label.text = "\(xValues[i])" label.textColor = UIColor.black label.textAlignment = .center label.font = UIFont.systemFont(ofSize: 12) addSubview(label) } // Y değerlerini gösterin for i in 0..<yMaxValues.count { let y = graphHeight - (yMaxValues[i] - minValue) * yScaleFactor let label = UILabel(frame: CGRect(x: -35, y: y - 10, width: 30, height: 20)) label.text = "\(yMaxValues[i])" label.textColor = UIColor.black label.textAlignment = .right label.font = UIFont.systemFont(ofSize: 12) addSubview(label) } } }
解决方案
问题出在填充路径的构建逻辑上,当前路径没有正确闭合两条折线的端点。正确的做法是创建一个包含yMin折线所有点 + 反向的yMax折线所有点的闭合路径,这样就能精准填充两条线之间的区域。
修正后的完整代码如下:
class CustomView: UIView { let xValues: [CGFloat] = [0, 2, 4, 6, 8, 10, 12, 14] let yMaxValues: [CGFloat] = [4, 6, 9, 10, 10, 11, 10, 11] let yMinValues: [CGFloat] = [2, 4, 5, 6, 7, 8, 9, 11] override func draw(_ rect: CGRect) { super.draw(rect) guard let context = UIGraphicsGetCurrentContext() else { return } // 填充白色背景 UIColor.white.setFill() context.fill(rect) // 获取图表尺寸 let graphWidth = rect.width let graphHeight = rect.height // 计算全局的最小和最大Y值(确保覆盖所有数据) let allYValues = yMinValues + yMaxValues let minValue = allYValues.min() ?? 0.0 let maxValue = allYValues.max() ?? 0.0 // 计算缩放因子 let yScaleFactor = graphHeight / (maxValue - minValue) let xScaleFactor = graphWidth / CGFloat(xValues.count - 1) // 构建填充路径:先画yMin折线,再反向画yMax折线,最后闭合 let fillPath = UIBezierPath() // 添加yMin折线的所有点 for i in 0..<xValues.count { let x = CGFloat(i) * xScaleFactor let y = graphHeight - (yMinValues[i] - minValue) * yScaleFactor if i == 0 { fillPath.move(to: CGPoint(x: x, y: y)) } else { fillPath.addLine(to: CGPoint(x: x, y: y)) } } // 反向添加yMax折线的所有点(从最后一个到第一个) for i in (0..<xValues.count).reversed() { let x = CGFloat(i) * xScaleFactor let y = graphHeight - (yMaxValues[i] - minValue) * yScaleFactor fillPath.addLine(to: CGPoint(x: x, y: y)) } // 闭合路径并填充 fillPath.close() let fillColor = UIColor(red: 0.5, green: 0.5, blue: 1.0, alpha: 0.5) fillColor.setFill() fillPath.fill() // 绘制yMin折线 let yMinPath = UIBezierPath() for i in 0..<xValues.count { let x = CGFloat(i) * xScaleFactor let y = graphHeight - (yMinValues[i] - minValue) * yScaleFactor if i == 0 { yMinPath.move(to: CGPoint(x: x, y: y)) } else { yMinPath.addLine(to: CGPoint(x: x, y: y)) } // 添加yMin数值标签 let pointLabel = UILabel(frame: CGRect(x: x - 15, y: y - 30, width: 30, height: 30)) pointLabel.text = "\(yMinValues[i])" pointLabel.textColor = .black pointLabel.textAlignment = .center pointLabel.font = .systemFont(ofSize: 10) addSubview(pointLabel) } // 绘制yMax折线 let yMaxPath = UIBezierPath() for i in 0..<xValues.count { let x = CGFloat(i) * xScaleFactor let y = graphHeight - (yMaxValues[i] - minValue) * yScaleFactor if i == 0 { yMaxPath.move(to: CGPoint(x: x, y: y)) } else { yMaxPath.addLine(to: CGPoint(x: x, y: y)) } // 添加yMax数值标签 let pointLabel = UILabel(frame: CGRect(x: x - 15, y: y - 30, width: 30, height: 30)) pointLabel.text = "\(yMaxValues[i])" pointLabel.textColor = .black pointLabel.textAlignment = .center pointLabel.font = .systemFont(ofSize: 10) addSubview(pointLabel) } // 设置折线颜色并绘制 let lineColor = UIColor.black.cgColor context.setStrokeColor(lineColor) yMinPath.stroke() yMaxPath.stroke() // 绘制X轴 context.move(to: CGPoint(x: 0, y: graphHeight)) context.addLine(to: CGPoint(x: graphWidth, y: graphHeight)) context.strokePath() // 绘制Y轴 context.move(to: CGPoint(x: 0, y: graphHeight)) context.addLine(to: CGPoint(x: 0, y: 0)) context.strokePath() // 添加X轴数值标签 for i in 0..<xValues.count { let x = CGFloat(i) * xScaleFactor let label = UILabel(frame: CGRect(x: x - 15, y: graphHeight + 5, width: 30, height: 20)) label.text = "\(xValues[i])" label.textColor = .black label.textAlignment = .center label.font = .systemFont(ofSize: 12) addSubview(label) } // 添加Y轴数值标签(显示全局的min和max,避免重复) let yMinLabel = UILabel(frame: CGRect(x: -35, y: graphHeight - 10, width: 30, height: 20)) yMinLabel.text = "\(minValue)" yMinLabel.textColor = .black yMinLabel.textAlignment = .right yMinLabel.font = .systemFont(ofSize: 12) addSubview(yMinLabel) let yMaxLabel = UILabel(frame: CGRect(x: -35, y: 0 - 10, width: 30, height: 20)) yMaxLabel.text = "\(maxValue)" yMaxLabel.textColor = .black yMaxLabel.textAlignment = .right yMaxLabel.font = .systemFont(ofSize: 12) addSubview(yMaxLabel) } }
关键修改点
- 填充路径构建:先按顺序添加
yMinValues的所有点,再反向添加yMaxValues的所有点,最后调用close()闭合路径,形成的区域就是两条折线之间的部分。 - 全局Y值计算:合并
yMinValues和yMaxValues计算全局的最小/最大值,确保所有数据点都能正确映射到图表范围内。 - Y轴标签优化:改为显示全局的最小和最大值,避免重复添加相同标签。
内容的提问来源于stack exchange,提问作者esra didem
相关产品推荐
相关产品推荐

