Bài viết này sẽ giới thiệu cho các bạn cách tạo một biểu đồ đơn giản mà không cần sử dụng bất kỳ thư viện nào.
Bài viết sử dụng Swift 4.2 và XCode 10.1 cho dự án mẫu.
Tất nhiên, có khá nhiều thư viện để triển khai biểu đồ nhanh chóng và bạn có thể cân nhắc sử dụng một trong số chúng trước khi làm một cái biểu đồ theo ý thích.
Tuy nhiên, tự làm cũng không khó và chắc chắn sẽ giúp bạn học được nhiều hơn và nó cũng có một số ưu điểm như:
Thiết kế linh hoạt: Bạn có thể làm nó trông giống như bất kì thứ gì bạn muốn. Thư viện của bên thứ ba có thể không có biểu đồ trông giống như thiết kế mà công ty bạn muốn.
Không có mã dự phòng: Bởi vì bạn chỉ cần xây dựng những gì bạn cần.
Mục tiêu
Bài viết sẽ hướng dẫn bạn cách xây dựng một biểu đồ thanh đẹp như thế này:
Những gì bạn cần biết và các thành phần được sử dụng để xây dựng biểu đồ trên:
UIScrollView: để cho phép cuộn sang trái và phải
UIBezierPath: để tạo đường dẫn cho hình dạng
CALayer: để xây dựng các thanh hình chữ nhật
CAShapeLayer: để xây dựng các hình dạng giống như các thanh trong Biểu đồ 1: Biểu đồ thanh đẹp. Đây là một lớp rất tuyệt vời giúp bạn xây dựng tất cả các loại hình dạng 2D. Nó cũng có các chức năng bổ sung như màu tô và nét, mũ dòng, hoa văn và nhiều hơn nữa.
CATextLayer: để hiển thị văn bản
Bắt đầu nào!
Tôi sẽ chỉ cho bạn cách xây dựng một biểu đồ thanh với các thanh hình chữ nhật như hình dưới đây trước tiên. Sau đó, chúng ta sẽ thay đổi các thanh hình chữ nhật thành các thanh cong. Hãy để Lát gọi biểu đồ bên dưới Basic BarChart
BasicBarChart Class
Về cơ bản, BasicBarChart là một lớp con của UIView. Nó chứa một scrollView để hỗ trợ cuộn và scrollView chứa một thể hiện CALayer gọi là mainLayer. mainLayer chứa tất cả các lớp khác được tạo bởi biểu đồ và kích thước của mainLayer bằng với sizeSize của scrollView. Hình ảnh dưới đây thể hiện cách biểu đồ được hiển thị:
Chúng ta hãy xem định nghĩa của lớp BasicBarChart.
class BasicBarChart: UIView {
/// the width of each bar
let barWidth: CGFloat = 40.0
/// space between each bar
let space: CGFloat = 20.0
/// space at the bottom of the bar to show the title
private let bottomSpace: CGFloat = 40.0
/// space at the top of each bar to show the value
private let topSpace: CGFloat = 40.0
/// contain all layers of the chart
private let mainLayer: CALayer = CALayer()
/// contain mainLayer to support scrolling
private let scrollView: UIScrollView = UIScrollView()
...
}
Chúng ta có thể sử dụng CAScrollLayer để hỗ trợ cuộn, tuy nhiên ở đây tôi sử dụng UISCrollView vì nó dễ dàng hơn nhiều.
dataEntries property
BasicBarChart có một thuộc tính được gọi là dataEntries là tập hợp của BarEntry. Giao diện người dùng của biểu đồ sẽ được cập nhật khi dataEntries được đặt:
class BasicBarChart: UIView {
...
var dataEntries: [BarEntry]? = nil {
didSet {
mainLayer.sublayers?.forEach({$0.removeFromSuperlayer()})
if let dataEntries = dataEntries {
scrollView.contentSize = CGSize(width: (barWidth + space)*CGFloat(dataEntries.count), height: self.frame.size.height)
mainLayer.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
drawHorizontalLines()
for i in 0..<dataEntries.count {
showEntry(index: i, entry: dataEntries[i])
}
}
}
}
...
}
BarEntry là một struct đơn giản đại diện cho dữ liệu của mỗi thanh:
struct BarEntry {
let color: UIColor
/// Ranged from 0.0 to 1.0
let height: Float
/// To be shown on top of the bar
let textValue: String
/// To be shown at the bottom of the bar
let title: String
}
Bất cứ khi nào thuộc tính dataEntries được thiết lập, mainLayer sẽ clean mọi thứ và tạo lại bằng phương thức showEntry.
showEntry method
Phương thức showEntry chỉ cần tính toán vị trí x và y của thanh và sau đó gọi phương thức drawBar.
private func showEntry(index: Int, entry: BarEntry) {
/// Starting x postion of the bar
let xPos: CGFloat = space + CGFloat(index) * (barWidth + space)
/// Starting y postion of the bar
let yPos: CGFloat = translateHeightValueToYPosition(value: entry.height)
drawBar(xPos: xPos, yPos: yPos, color: entry.color)
/// Draw text above the bar
drawTextValue(xPos: xPos - space/2, yPos: yPos - 30, textValue: entry.textValue, color: entry.color)
/// Draw text below the bar
drawTitle(xPos: xPos - space/2, yPos: mainLayer.frame.height - bottomSpace + 10, title: entry.title, color: entry.color)
}
drawBar method
Phương thức drawBar rất đơn giản, nó chỉ tính toán khung của barLayer, đặt màu nền và sau đó, thêm barLayer vào mainLayer.
private func drawBar(xPos: CGFloat, yPos: CGFloat, color: UIColor) {
let barLayer = CALayer()
barLayer.frame = CGRect(x: xPos, y: yPos, width: barWidth, height: mainLayer.frame.height - bottomSpace - yPos)
barLayer.backgroundColor = color.cgColor
mainLayer.addSublayer(barLayer)
}
Các phương thức drawTextValue và drawTitle cũng khá đơn giản.
private func drawTextValue(xPos: CGFloat, yPos: CGFloat, textValue: String, color: UIColor) {
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: xPos, y: yPos, width: barWidth+space, height: 22)
textLayer.foregroundColor = color.cgColor
textLayer.backgroundColor = UIColor.clear.cgColor
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.contentsScale = UIScreen.main.scale
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
textLayer.fontSize = 14
textLayer.string = textValue
mainLayer.addSublayer(textLayer)
}
private func drawTitle(xPos: CGFloat, yPos: CGFloat, title: String, color: UIColor) {
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: xPos, y: yPos, width: barWidth + space, height: 22)
textLayer.foregroundColor = color.cgColor
textLayer.backgroundColor = UIColor.clear.cgColor
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.contentsScale = UIScreen.main.scale
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
textLayer.fontSize = 14
textLayer.string = title
mainLayer.addSublayer(textLayer)
}
Horizontal lines
Trong biểu đồ thanh cơ bản, bạn có thể thấy rằng tôi có 3 đường ngang, một ở dưới cùng của biểu đồ, một ở đầu biểu đồ và một đường đứt nét ở giữa. Những dòng này có thể được tạo bằng CAShapeLayer:
let path = UIBezierPath()
path.move(to: CGPoint(x: xPos, y: yPos))
path.addLine(to: CGPoint(x: scrollView.frame.size.width, y: yPos))
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.lineWidth = 0.5
if lineInfo["dashed"] as! Bool {
lineLayer.lineDashPattern = [4, 4]
}
lineLayer.strokeColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1).cgColor
self.layer.insertSublayer(lineLayer, at: 0)
Chỉ cần vẽ một đường dẫn thẳng bằng UIBezierPath, sau đó, gán đường dẫn cho thuộc tính đường dẫn của một cá thể CAShapeLayer. Có một thuộc tính rất hay của CAShapeLayer đó là lineDashPotype cho phép bạn tạo một đường đứt nét giống như đường kẻ mà tôi có ở giữa biểu đồ.
Bởi vì tôi muốn các dòng đó là tĩnh khi người dùng cuộn biểu đồ, vì vậy, tôi thêm chúng trực tiếp vào self.layer thay vì mainLayer.
Như vậy là chúng ta đã vẽ xong biểu đồ thanh cơ bản, các bạn có thể xem chi tiết hơn trong mã code ở cuối bài nhé!
Giờ hãy cùng xây dựng biểu đồ thanh cong
Class BasicBarChart là để vẽ ra biểu đồ thanh cơ bản, tôi sẽ tạo một class có tên là BeautifulBarChart để vẽ ra biểu đồ thanh cong. Sự khác biệt chính giữa 2 biểu đồ này là phương thức drawBar và drawTextValue. Các phương pháp và tính chất chỉ hơi khác nhau chút.
Đây là mã cho phương thức drawBar:
private func drawBar(xPos: CGFloat, yPos: CGFloat, height: CGFloat, color: UIColor) {
let leftPath: UIBezierPath = UIBezierPath()
leftPath.move(to: CGPoint(x: xPos, y: yPos))
leftPath.addCurve(to: CGPoint(x: xPos+barWidth/2, y: yPos - height), controlPoint1: CGPoint(x: (xPos+barWidth/2), y: yPos), controlPoint2: CGPoint(x: xPos + barWidth*3/10, y: yPos - height))
leftPath.addLine(to: CGPoint(x: xPos + barWidth/2, y: yPos))
let leftLine = CAShapeLayer()
leftLine.path = leftPath.cgPath
leftLine.lineWidth = 0.0
leftLine.fillColor = color.cgColor
leftLine.strokeColor = color.cgColor
let rightPath: UIBezierPath = UIBezierPath()
rightPath.move(to: CGPoint(x: xPos+barWidth, y: yPos))
rightPath.addCurve(to: CGPoint(x: xPos + barWidth/2, y: yPos-height), controlPoint1: CGPoint(x: xPos+barWidth/2, y: yPos), controlPoint2: CGPoint(x: xPos + barWidth*7/10, y: yPos-height))
rightPath.addLine(to: CGPoint(x: xPos + barWidth/2, y: yPos))
let rightLine = CAShapeLayer()
rightLine.path = rightPath.cgPath
rightLine.lineWidth = 0.0
rightLine.fillColor = color.cgColor
rightLine.strokeColor = color.cgColor
mainLayer.addSublayer(leftLine)
mainLayer.addSublayer(rightLine)
}
Mỗi thanh được hình thành bởi 2 hình dạng. Hình ảnh dưới đây minh họa cách tạo hình bên trái bằng UIBezierPath để vẽ đường cong bezier:
Việc tạo ra đường bên phải tương tự như đường bên trái. Khi bạn có đường dẫn, một hình có thể được tạo bằng CAShapeLayer. Sau đó, chỉ cần thêm 2 hình dạng đó vào mainLayer.
Và hiển thị 2 biểu đồ để chúng ta so sánh:
Các bạn có thể xem toàn bộ project ở đây.
Cảm ơn đã theo dõi bài viết này, các bạn có thể tìm hiểu nhiều hơn các bài viết về iOS tại blog của Techmaster, và các khóa học được xây dựng đáp ứng với xu thế công nghệ năm 2019
Techmaster có khóa học iOS Swift, React Native, Flutter ngắn hạn và dài hạn đảm bảo chất lượng đầu ra của học viên
Bình luận