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()

    }
  1. drawBorder 方法绘制圆角矩形的边框,但不填充。
  2. 创建路径,计算 frame 时将线宽扣除。
  3. 这 3 句绘制了矩形边框。
  4. drawFillColor 方法绘制填充矩形(非渐变色)。
  5. 创建路径。
  6. 填充颜色。

扩展 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)
    }
  1. 绘制边框。
  2. 绘制填充色背景。
  3. 绘制背景的内阴影。
  4. 绘制渐变填充,即进度条的滑条。
  5. 绘制滑条的内阴影。
  6. 绘制文本(进度数值)

测试

为了方便测试,增加一个 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()
        }
    }
  1. 增加了一个判断,但 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()
        }

    }

  1. 如果 clipRect 参数不为空,将 clipRect 设置为裁切区并进行裁剪。

  2. 将 path 加入裁切区进行二次裁剪。

  3. 恢复状态。

然后修改了外两个重载方法,加上 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和二次裁剪的主要内容,如果未能解决你的问题,请参考以下文章

视频二次裁剪时间计算出原片的时间片段算法

(手机拍照)3构图

(手机拍照)3构图

一文速学-时间序列分析算法之一次移动平均法和二次移动平均法详解+实例代码

Android反编译和二次打包

PHP中逻辑性core问题解决创建型分销模式和二次分销的模式