3-ProgressBar和二次裁剪
Posted 颐和园
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3-ProgressBar和二次裁剪相关的知识,希望对你有一定的参考价值。
在本节我们将创建一个自定义的进度条组件。在此之前,我们需要对 RectanglePainer 和 TextPainter 进行一些扩展。
扩展 RectanglePainter
在 RectanglePainter 中增加两个函数:
// 1
public static func drawBorder(_ frame: CGRect, borderColor: UIColor, borderWidth: CGFloat, cornerRadius: CGFloat) {
// 2
let rect = insetFrame(frame, delta: borderWidth)
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
// 3
borderColor.setStroke()
path.lineWidth = borderWidth
path.stroke()
}
// 4
public static func drawFillColor(_ frame: CGRect, fillColor: UIColor, cornerRadius: CGFloat) {
// 5
let path = UIBezierPath(roundedRect: frame, cornerRadius: cornerRadius)
// 6
fillColor.setFill()
path.fill()
context?.restoreGState()
}
- drawBorder 方法绘制圆角矩形的边框,但不填充。
- 创建路径,计算 frame 时将线宽扣除。
- 这 3 句绘制了矩形边框。
- drawFillColor 方法绘制填充矩形(非渐变色)。
- 创建路径。
- 填充颜色。
扩展 TextPainter
首先为 drawText 方法增加一个 innerShadow 参数:
public static func drawText(_ rect: CGRect, text: Text, innerShadow: NSShadow?) {
在 text.text.draw(in: textRect, withAttributes: fontAttributes) 一句后添加代码:
if let shadow = innerShadow, let color = shadow.shadowColor as? UIColor {
context?.setAlpha(color.cgColor.alpha)
context?.beginTransparencyLayer(auxiliaryInfo: nil)
let textOpaqueTextShadow = color.withAlphaComponent(1)
context?.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: textOpaqueTextShadow.cgColor)
context?.setBlendMode(.sourceOut)
context?.beginTransparencyLayer(auxiliaryInfo: nil)
textOpaqueTextShadow.setFill()
let textInnerShadowFontAttributes = [
.font: text.font,
.foregroundColor: color,
.paragraphStyle: paragraphStyle,
] as [NSAttributedString.Key: Any]
text.text.draw(in: textRect, withAttributes: textInnerShadowFontAttributes)
context?.endTransparencyLayer()
context?.endTransparencyLayer()
}
如果 innerShadow 不为空,绘制文本阴影。绘制文字内阴影的方法类似于矩形内阴影,在原来的文本上再次绘制一个带阴影的文本。
ProgressBar
接下来实现 ProgressBar,首先是属性声明:
class ProgressBar: UIControl {
var bgColor = UIColor(red: 0.7, green: 0.9, blue: 0.9, alpha: 1.000)
var borderColor = UIColor(red: 0.0, green: 0.553, blue: 0.459, alpha: 1.000)
var textColor = UIColor(red: 0.173, green: 0.165, blue: 0.165, alpha: 1.000)
var gradient = CGGradient(colorsSpace: nil, colors: [UIColor.black.cgColor, UIColor.white.cgColor] as CFArray, locations: [1, 0])!
var cornerRadius:CGFloat = 15
var borderWidth:CGFloat = 2
var progress: CGFloat = 0 {
didSet{
if progress > 1 {
progress = 1
}else if progress < 0{
progress = 0
}
setNeedsDisplay()
}
}
lazy var barInnerShadow: NSShadow = {
let innerShadow = NSShadow()
innerShadow.shadowColor = UIColor.white
innerShadow.shadowOffset = CGSize(width: 3, height: 3)
innerShadow.shadowBlurRadius = 5
return innerShadow
}()
lazy var innerShadow: NSShadow = {
let innerShadow = NSShadow()
innerShadow.shadowColor = UIColor.gray
innerShadow.shadowOffset = CGSize(width: 3, height: 3)
innerShadow.shadowBlurRadius = 5
return innerShadow
}()
lazy var backgroundPath: UIBezierPath = {
let path = UIBezierPath(roundedRect: CGRect(x: borderWidth, y: borderWidth, width: frame.width-borderWidth*2, height: frame.height-borderWidth*2), cornerRadius: cornerRadius)
return path
}()
}
然后是最重要的 draw 方法:
override func draw(_ rect: CGRect) {
// 1
RectanglePainter.drawBorder(rect, borderColor: borderColor, borderWidth: borderWidth, cornerRadius: cornerRadius)
// 2
let frame = RectanglePainter.insetFrame(frame, delta: borderWidth)
RectanglePainter.drawFillColor(frame, fillColor: bgColor, cornerRadius: cornerRadius)
// 3
RectanglePainter.drawInnerShadow(frame, cornerRadius: cornerRadius, innerShadow: innerShadow)
// 4
let barFrame = CGRect(x:frame.minX, y: frame.minY, width: frame.width*progress, height: frame.height)
RectanglePainter.drawGradient(gradient: gradient, frame: barFrame, cornerRadius: cornerRadius, outerShadow: barInnerShadow)
// 5
RectanglePainter.drawInnerShadow(barFrame, cornerRadius: cornerRadius, innerShadow: barInnerShadow)
// 6
let str = String(format: "%.0f%%", progress*100)
let text = Text(text: str, font:UIFont.systemFont(ofSize: UIFont.systemFontSize), color: textColor)
TextPainter.drawText(frame, text: text, innerShadow: barInnerShadow)
}
- 绘制边框。
- 绘制填充色背景。
- 绘制背景的内阴影。
- 绘制渐变填充,即进度条的滑条。
- 绘制滑条的内阴影。
- 绘制文本(进度数值)
测试
为了方便测试,增加一个 run 方法:
func run() {
var date = Date()
date.addTimeInterval(1)
let timer = Timer(fire: date, interval: 0.1, repeats: true) { [weak self] t in
if let progress = self?.progress, progress >= 1 {
self?.progress = 0
}else {
self?.progress += 0.01
}
}
RunLoop.current.add(timer, forMode: .common)
}
在 viewDidLoad 方法中:
let bar = ProgressBar(frame: CGRect(x: 100, y:200, width: 190, height: 47))
bar.backgroundColor = .white
addSubview(bar)
bar.run()
效果如下:
可以看到,在进度条一开始移动的时候,有一些 bug。滑条的大小超过了边框。我们接下来需要解决这个问题。
修改 Bug
这个问题其实不是很好解决,因为这是 Core Graphics 在绘制圆角矩形时的一个限制。这里只是提供一种简单的解决思路,当滑条的宽度太窄时,我们用绘制椭圆来替代圆角矩形。
首先我们需要扩展 RectanglePainter 的 drawGradient 函数和 drawInnerShadow 函数:
public static func drawGradient(gradient: CGGradient?, path: UIBezierPath, cornerRadius: CGFloat?, outerShadow: NSShadow?) {
let context = UIGraphicsGetCurrentContext()
if let context = context {
let frame = path.bounds
Rectangle Drawing
context.saveGState()
if let shadow = outerShadow, let color = shadow.shadowColor as? UIColor {
context.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: color.cgColor)
}
context.beginTransparencyLayer(auxiliaryInfo: nil)
path.addClip()
if let gradient = gradient {
context.drawLinearGradient(gradient, start: CGPoint(x: frame.midX, y: frame.minY), end: CGPoint(x: frame.midX, y: frame.maxY), options: [])
}else {
path.fill()
}
context.endTransparencyLayer()
context.restoreGState()
}
}
public static func drawInnerShadow(_ path: UIBezierPath, cornerRadius: CGFloat?, innerShadow:NSShadow) {
let context = UIGraphicsGetCurrentContext()
if let context = context, let color = innerShadow.shadowColor as? UIColor {
context.saveGState()
context.clip(to: path.bounds)
// context.setAlpha(color.cgColor.alpha)
context.beginTransparencyLayer(auxiliaryInfo: nil)
let rectangleOpaqueShadow = color // color.withAlphaComponent(1)
context.setShadow(offset: innerShadow.shadowOffset, blur: innerShadow.shadowBlurRadius, color: rectangleOpaqueShadow.cgColor)
context.setBlendMode(.sourceOut)
context.beginTransparencyLayer(auxiliaryInfo: nil)
rectangleOpaqueShadow.setFill()
path.fill()
context.endTransparencyLayer()
context.endTransparencyLayer()
context.restoreGState()
}
}
跟原来的函数相比,这两个函数接受了一个 path 参数,并绘制这个 path。其它代码基本是一模一样的。
然后在 ProgressBar 中,增加一个函数:
private func rectanglePathFixed(_ frame: CGRect, cornerRadius: CGFloat?) -> UIBezierPath {
var radius = cornerRadius ?? 0
var rect = frame
if frame.width < frame.height {
let fix = (frame.height-frame.width)/2
rect = CGRect(x: frame.minX, y: frame.minY+fix, width: frame.width, height: frame.width)
radius = rect.width/2
return UIBezierPath(ovalIn: rect)
}
return UIBezierPath(roundedRect: rect, cornerRadius: radius)
}
这个函数根据当前滑条的宽和高产生不同的 path,如果 frame 的宽度小于高度,那么我们生成一个椭圆返回,否则返回正常的圆角矩形。
在 draw 方法中,替换旧的 drawGradient 函数和 drawInnerShadow 函数:
let path = rectanglePathFixed(barFrame, cornerRadius: cornerRadius)
RectanglePainter.drawGradient(gradient: gradient, path: path, cornerRadius: cornerRadius, outerShadow: barInnerShadow)
// 5
RectanglePainter.drawInnerShadow(path, cornerRadius: cornerRadius, innerShadow: barInnerShadow)
这个方案并不完美,但是比起原来要好许多:
路径裁剪 clip
要完美解决这个问题,需要用到路径裁剪。首先我们来看 drawInnerShadow 方法,为其增加一个 clipRect 参数:
public static func drawInnerShadow(_ frame: CGRect, cornerRadius: CGFloat?, innerShadow:NSShadow, clipRect: CGRect?) {
let context = UIGraphicsGetCurrentContext()
if let context = context, let color = innerShadow.shadowColor as? UIColor {
let path = UIBezierPath(roundedRect: frame, cornerRadius: cornerRadius ?? 0)
context.saveGState()
// 1
if let rect = clipRect {
context.clip(to: rect)
}else {
path.addClip()
}
context.beginTransparencyLayer(auxiliaryInfo: nil)
let rectangleOpaqueShadow = color // color.withAlphaComponent(1)
context.setShadow(offset: innerShadow.shadowOffset, blur: innerShadow.shadowBlurRadius, color: rectangleOpaqueShadow.cgColor)
context.setBlendMode(.sourceOut)
context.beginTransparencyLayer(auxiliaryInfo: nil)
rectangleOpaqueShadow.setFill()
path.fill()
context.endTransparencyLayer()
context.endTransparencyLayer()
context.restoreGState()
}
}
- 增加了一个判断,但 clipRect 参数非空时,路径裁剪使用 clipRect 参数进行,否则用 path。这保证了当前的内阴影不会绘制出控件的边框。
然后是 drawGradient 函数,同样增加一个 clipRect 参数:
public static func drawGradient(gradient: CGGradient?, frame: CGRect, cornerRadius: CGFloat?, outerShadow: NSShadow?, clipRect: CGRect?) {
let context = UIGraphicsGetCurrentContext()
if let context = context {
let path = UIBezierPath(roundedRect: frame, cornerRadius: cornerRadius ?? 0)
context.saveGState()
if let shadow = outerShadow, let color = shadow.shadowColor as? UIColor {
context.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: color.cgColor)
}
// 1
if let rect = clipRect {
let clipPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius ?? 0)
clipPath.addClip()
}
// 2
path.addClip()
if let gradient = gradient {
context.drawLinearGradient(gradient, start: CGPoint(x: frame.midX, y: frame.minY), end: CGPoint(x: frame.midX, y: frame.maxY), options: [])
}else {
path.fill()
}
// 3
context.restoreGState()
}
}
-
如果 clipRect 参数不为空,将 clipRect 设置为裁切区并进行裁剪。
-
将 path 加入裁切区进行二次裁剪。
-
恢复状态。
然后修改了外两个重载方法,加上 clipRect 参数:
public static func drawGradient(gradient: CGGradient?, frame: CGRect, cornerRadius: CGFloat?, outerShadow: NSShadow?) {
drawGradient(gradient: gradient, frame: frame, cornerRadius: cornerRadius, outerShadow: outerShadow, clipRect: nil)
}
public static func drawInnerShadow(_ frame: CGRect, cornerRadius: CGFloat?, innerShadow:NSShadow) {
drawInnerShadow(frame, cornerRadius: cornerRadius, innerShadow: innerShadow, clipRect: nil)
}
修改 ProgressBar 的 draw(rect:) 方法,在绘制滑条时添加 clipRect 参数:
RectanglePainter.drawGradient(gradient: gradient, frame: barFrame, cornerRadius: cornerRadius, outerShadow: barInnerShadow, clipRect: frame)
RectanglePainter.drawInnerShadow(barFrame, cornerRadius: cornerRadius, innerShadow: barInnerShadow, clipRect: frame)
运行结果:
以上是关于3-ProgressBar和二次裁剪的主要内容,如果未能解决你的问题,请参考以下文章