当前位置:网站首页>3-progressbar and secondary cropping

3-progressbar and secondary cropping

2022-06-23 08:24:00 the Summer Palace

In this section, we will create a custom progress bar component . Before that , We need to be right about RectanglePainer and TextPainter Make some extensions .

Expand RectanglePainter

stay RectanglePainter Add two functions to :

	 // 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 Method to draw the border of a rounded rectangle , But don't fill .
  2. Create a path , Calculation frame Line width will be deducted .
  3. this 3 Sentence draws a rectangular border .
  4. drawFillColor Method to draw a filled rectangle ( Non gradient ).
  5. Create a path .
  6. Fill color .

Expand TextPainter

First of all to drawText Method add one innerShadow Parameters :

public static func drawText(_ rect: CGRect, text: Text, innerShadow: NSShadow?) {

stay text.text.draw(in: textRect, withAttributes: fontAttributes) Add code after a sentence :

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

If innerShadow Not empty , Draw text shadow . The method of drawing inner shadow of text is similar to that of inner shadow of rectangle , Draw a shaded text over the original text again .

ProgressBar

Next, we'll implement ProgressBar, The first is the attribute declaration :

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

And then the most important draw Method :

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. bound box .
  2. Draw a fill color background .
  3. Draw the inner shadow of the background .
  4. Draw gradient fill , I.e. sliding bar of progress bar .
  5. Draw the inner shadow of the slider .
  6. Draw text ( Progress value )

test

For testing purposes , Add one more run Method :

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

stay viewDidLoad In the method :

let bar = ProgressBar(frame: CGRect(x: 100, y:200, width: 190, height: 47))
bar.backgroundColor = .white
addSubview(bar)
bar.run()

The effect is as follows :

You can see , When the progress bar starts moving , Somewhat bug. The slider is larger than the border . We need to solve this problem next .

modify Bug

This problem is not easy to solve , Because this is Core Graphics A limitation in drawing rounded rectangles . Here is just a simple solution , When the width of the slider is too narrow , We draw ellipses instead of rounded rectangles .

First, we need to extend RectanglePainter Of drawGradient Functions and drawInnerShadow function :

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

Compared with the original function , These two functions accept a path Parameters , And draw this path. Other code is basically the same .

And then in ProgressBar in , Add a function :

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

This function generates different values according to the width and height of the current slider path, If frame The width of is less than the height , Then we generate an ellipse to return , Otherwise, return to the normal rounded rectangle .

stay draw In the method , Replace old drawGradient Functions and drawInnerShadow function :

let path = rectanglePathFixed(barFrame, cornerRadius: cornerRadius)
        RectanglePainter.drawGradient(gradient: gradient, path: path, cornerRadius: cornerRadius, outerShadow: barInnerShadow)
        // 5
        RectanglePainter.drawInnerShadow(path, cornerRadius: cornerRadius, innerShadow: barInnerShadow)

This plan is not perfect , But it is much better than before :

Path clipping clip

To solve this problem perfectly , Path clipping is required . First let's look at drawInnerShadow Method , Add a clipRect Parameters :

    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. Added a judgment , but clipRect When the parameter is not empty , Path clipping uses clipRect Parameters , Otherwise, use path. This ensures that the current inner shadow does not draw the border of the control .

And then there was drawGradient function , Also add a clipRect Parameters :

    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. If clipRect Parameter is not empty , take clipRect Set as the cutting area and cut .

  2. take path Add cutting area for secondary cutting .

  3. Restore the state .

Then we modified the other two overloaded methods , add clipRect Parameters :

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

modify ProgressBar Of draw(rect:) Method , Add... When drawing the slider clipRect Parameters :

RectanglePainter.drawGradient(gradient: gradient, frame: barFrame, cornerRadius: cornerRadius, outerShadow: barInnerShadow, clipRect: frame)
RectanglePainter.drawInnerShadow(barFrame, cornerRadius: cornerRadius, innerShadow: barInnerShadow, clipRect: frame)

Running results :

原网站

版权声明
本文为[the Summer Palace]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/174/202206230756065383.html