1-渐变阴影和文本

Posted 颐和园

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了1-渐变阴影和文本相关的知识,希望对你有一定的参考价值。

本文是《Core Graphics 入门教程》的第一讲。学完本文,你将学会如何在自定义控件中用 CG 函数绘制渐变色、阴影以及文本。

ShadowButton

假设我们想定制一个按钮控件叫做 ShadowButton——这不值得大惊小怪,但特别的一点就是,我们准备用 Core Graphics 2D 绘制它的 UI。

首先创建这个 class。首先,我们需要继承 UIControl 而不是 UIView,这样是为了方便使用它的 state 属性并对用户的触摸进行处理。

class ShadowButton: UIControl {
	// 1
    lazy var outerShadow: NSShadow = {
        let outerShadow = NSShadow()
        outerShadow.shadowColor = UIColor.black
        outerShadow.shadowOffset = CGSize(width: 3.1, height: 3.1)
        outerShadow.shadowBlurRadius = 5
        return outerShadow
    }()
    lazy var innerShadow: NSShadow = {
        let innerShadow = NSShadow()
        innerShadow.shadowColor = UIColor.white
        innerShadow.shadowOffset = CGSize(width: 3.1, height: 3.1)
        innerShadow.shadowBlurRadius = 9
        return innerShadow
    }()
    lazy var text: Text? = Text(text: "Shadow Button", font: UIFont.systemFont(ofSize: 18), color: .black)
    lazy var gradient: CGGradient = {
        let deepFillColor = UIColor(red: 0.502, green: 0.502, blue: 0.502, alpha: 1.000)
        let lightFillColor = UIColor(red: 0.871, green: 0.871, blue: 0.871, alpha: 1.000)
        let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: [lightFillColor.cgColor, deepFillColor.cgColor] as CFArray, locations: [0, 1])!
        return gradient
    }()
    var buttonRadius: CGFloat = 11
    // 2
    override func draw(_ rect: CGRect) {
        
        if isSelected == true {
            selectDraw(rect)
        }else {
            unselectDraw(rect)
        }
    }
    // 3
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.isSelected = true
        setNeedsDisplay()
    }
    // 4
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.isSelected = false
        setNeedsDisplay()
    }
    // 5
    public  func selectDraw(_ frame: CGRect) {

        let rect = RectanglePainter.insetFrame(frame, delta: outerShadow.shadowBlurRadius)
        RectanglePainter.drawGradient(gradient: gradient, frame: rect, cornerRadius: buttonRadius, outerShadow: outerShadow)
        RectanglePainter.drawInnerShadow(rect, cornerRadius: buttonRadius, innerShadow: innerShadow)
        if text != nil {
            TextPainter.drawText(rect, text: text!)
        }
    }
    // 6
    public func unselectDraw(_ frame: CGRect) {

        let rect = RectanglePainter.insetFrame(frame, delta: outerShadow.shadowBlurRadius)
        RectanglePainter.drawGradient(gradient: gradient, frame: rect, cornerRadius: buttonRadius, outerShadow: outerShadow)
        if text != nil {
            TextPainter.drawText(rect, text: text!)
        }
    }
}
  1. 按钮属性,包括圆角、内阴影、外阴影、文本等,方便从外部定制按钮样式。这里使用了懒加载属性的定义,这样在第一次调用这些属性时,这些属性值会自动初始化。请注意 NSShadow、Text 和CGGrandient 这些结构体的构造方式。
  2. 用 Core Graphics 绘图等最主要方法就是 draw(rect:) 方法,这里我们会判断按钮的 isSelected 状态,并调用对应的方法进行 UI 的绘制。
  3. touchesBegan 函数,响应用户的触摸动作。这样当用户触摸按钮时会有一个加亮的效果。正常情况控件状态是 unselected 的,但当用户点击它时,我们改变控件的 isSelected 为 true,然后调用 setNeedsDisplay 重新绘制控件——这实际上会触发 draw(rect:) 方法。
  4. touchesEnded 函数,响应用户的松开动作。这样当用户释放按钮时会有一个加暗的效果。当用户释放它时,我们改变控件的 isSelected 为 false,并调用 setNeedsDisplay 重新绘制控件,这样按钮又恢复原样。
  5. selectDraw 方法负责 isSelected 为 true 时的绘制。这里调用了两个 Painter 类,这些 Painter 类中封装了 Core Graphics 绘制方法。首先调用 RectanglePainter 绘制一个矩形(同时绘制外阴影),然后绘制矩形的内阴影,最后调用 TextPainter 绘制文本。
  6. unselectDraw 方法负责 isSelected 为 false 时的绘制。同 selectDraw 方法一样,但不用绘制内阴影(去掉加亮效果)。

RectangleButtonPainter 和 TextPainter负责体力活——绘制矩形和文本。

ShadowButtonPainter

	class RectanglePainter {
	// 1
    public static func insetFrame(_ frame: CGRect, delta: CGFloat) -> CGRect {
        return CGRect(x: delta, y: delta, width: frame.width-delta*2, height: frame.height-delta*2)
    }
	// 2
    public static func drawGradient(gradient: CGGradient?, frame: CGRect, cornerRadius: CGFloat?, outerShadow: NSShadow?) {
    	 // 3
        let context = UIGraphicsGetCurrentContext()
        if let context = context {
        	  // 4
            let path = UIBezierPath(roundedRect: frame, cornerRadius: cornerRadius ?? 0)
            // 5
            context.saveGState()
            // 6
            if  let shadow = outerShadow, let color = shadow.shadowColor as? UIColor {
                context.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: color.cgColor)
            }
            // 7
            context.beginTransparencyLayer(auxiliaryInfo: nil)
            // 8
            path.addClip()
			  // 9
            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()
            }
            // 10
            context.endTransparencyLayer()
            context.restoreGState()
        }

    }
    // 11
    public static func drawInnerShadow(_ frame: CGRect, cornerRadius: CGFloat?, innerShadow:NSShadow) {
        let context = UIGraphicsGetCurrentContext()
        if let context = context, let color = innerShadow.shadowColor as? UIColor {
        	  // 12
            let path = UIBezierPath(roundedRect: frame, cornerRadius: cornerRadius ?? 0)
            context.saveGState()
            // 13
            context.clip(to: path.bounds)
            // 14
            context.setAlpha(color.cgColor.alpha)
            // 15
            context.beginTransparencyLayer(auxiliaryInfo: nil)
            // 16
            let rectangleOpaqueShadow = color.withAlphaComponent(1)
            // 17
            context.setShadow(offset: innerShadow.shadowOffset, blur: innerShadow.shadowBlurRadius, color: rectangleOpaqueShadow.cgColor)
            // 18
            context.setBlendMode(.sourceOut)
            context.beginTransparencyLayer(auxiliaryInfo: nil)
			  // 19
            rectangleOpaqueShadow.setFill()
            path.fill()
			  // 20
            context.endTransparencyLayer()
            context.endTransparencyLayer()
            context.restoreGState()
        }
    }
}
  1. insetFrame 方法,计算一个“缩小的”矩形,缩小的范围为 delta 个像素。这个方法在绘制带阴影效果的控件时经常会用到,因为矩形的阴影往往会占据额外的控件,因此我们绘制控件的矩形空间时不能把整个控件的 frame 都占满,要留有足够的控件绘制阴影。

  2. drawGradient 方法负责绘制渐变填充+外阴影,需要传入 4 个参数:渐变色 gradient、矩形范围 frame、圆角半径 cornerRadius、外阴影 outerShadow。

  3. 首先获取当前上下文 context,这是最重要的东西,在 CG 绘图中我们总是需要它,没有它我们什么都干不了。

  4. 构造一个圆角的矩形,作为按钮的形状(不包含阴影)。调用贝塞尔曲线的构造方法创建一个圆角矩形。

  5. CG 绘图经常涉及到对 context 的状态改变。由于 context 对象是全局的,在我们进行操作之前,最好对 context 原来对状态进行保存,等操作完毕再恢复,这样就不至于因为一次绘制影响了后续的绘制。状态的保存使用 saveGState 函数,状态的恢复使用 restoreGState 函数。

  6. 设置 context 的阴影,这样当我们开始填充渐变矩形时,顺便就会绘制所设定的 shadow。当然,如果 outerShadow 参数为 nil,我们就什么都不干。

  7. 开始新的透明图层,透明图层类似于 ps 中图层的概念,而且带有 alpha 通道。方便你组合不同的图形。

  8. 然后路径裁剪。这样填充内容不会超出路径的内部。

  9. 在路径内部绘制渐变色。

  10. 结束透明图层,恢复 context 图形状态。

  11. drawInnerShadow 负责绘制高光(内阴影)。它需要 3 个参数:矩形范围 frame,圆角半径 corderRadius,内阴影 innerShadow。

  12. 构造一个圆角的矩形,作为高光部分的形状。调用贝塞尔曲线的构造方法创建一个圆角矩形。

  13. 路径剪切。

  14. 设置 context 的 alpha 值,使得当前绘制的 alpha 值等同于 innerShadow 的 color 的 alpha 值(之前这个值是 1)。

  15. 然后设置一个透明图层。

  16. 设置内阴影的颜色,但是需要将内阴影颜色的 alpha 去掉,不管原来是多少都设置为 1。因为我们的内阴影颜色 alpha 值本来也是 1,我们可以把 14 一行去掉,将 15 行改为:

    rectangleOpaqueShadow = color 
    

    效果也是一样的。不过这里是为了避免万一出现 alpha 不为 1 的情况,这时候需要我们手动将内阴影的 alpha 调整为 1,否则效果会出乎我们意料。

  17. 用这个 alpha 1 的内阴影颜色设置 context 的阴影。

  18. 设置混合模式为 source out。混合模式的使用比较复杂,它在填充时并不是直接填充颜色,而是将颜色值进行一定的计算,计算成新的颜色值后用于填充。

  19. 用计算出的不透明的内阴影颜色填充路径。

  20. 结束透明图层,恢复 context。

绘制文本

文本比较简单:

    private func drawText(_ rect: CGRect, color: UIColor?) {
        if let color = color, let string = text?.text, let font = text?.font {
        	  // 1
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.alignment = .center
            // 2
            let fontAttributes = [
                .font: font,
                .foregroundColor: color,
                .paragraphStyle: paragraphStyle,
            ] as [NSAttributedString.Key: Any]
			  // 3
            let height: CGFloat = string.boundingRect(with: CGSize(width: rect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: fontAttributes, context: nil).height
            context?.saveGState()
            context?.clip(to: rect)
            // 4
            string.draw(in: CGRect(x: rect.minX, y: rect.minY + (rect.height - height) / 2, width: rect.width, height: height), withAttributes: fontAttributes)
            context?.restoreGState()
        }
    }
  1. 设置段落居中(垂直居中)
  2. 设置字体属性。
  3. 计算文本高度。
  4. 绘制文本。

测试

在 viewDidLoad 方法中加上:

        let button = ShadowButton(frame: CGRect(x: 100,y: 200,width: 200,height: 50))
        button.gradient = gradient()
        button.buttonRadius = 11
        button.innerShadow = innerShadow()
        button.outerShadow = outerShadow()
        button.text = text("Hello")
        
        button.backgroundColor = .white
        
        addSubview(button)

效果如下:

以上是关于1-渐变阴影和文本的主要内容,如果未能解决你的问题,请参考以下文章

css(字体,文本,边距,边框,阴影,背景,渐变,多重背景,列表)

带有渐变 * 和 * 笔划的 Android TextView

Android绘图阴影渐变和位图运算处理

Android绘图阴影渐变和位图运算处理

带有图层阴影的UILabel文本颜色透明度?

带有渐变和阴影的 UIView