美文网首页iOS开发
IOS基础:绘图

IOS基础:绘图

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-20 10:10 被阅读0次

    原创:知识点总结性文章
    无私奉献,为国为民,创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 比较
    • 一、Bitmap 位图
      • 1、Bitmap的概念
      • 2、iOS中的Bitmap
      • 3、实际应用
    • 二、CoreGraphics
      • 1、简介
      • 2、drawRect: 常用方法
      • 3、drawRect: 自定义view的步骤
      • 4、drawRect: 绘制直线
      • 5、drawRect: 绘制曲线
      • 6、drawRect: 绘制几何图形
      • 7、drawRect: 绘制功能块
      • 8、drawRect: 绘制文字和图片
      • 9、drawRect: 修改图形
      • 10、CAShapeLayer
      • 11、CAGradientLayer
    • Demo
    • 参考文献

    一、Bitmap 位图

    1、Bitmap的概念

    位图(Bitmap),又称点阵图,是使用像素阵列来表示的图像。位图的像素都分配有特定的位置和颜色值。每个像素的颜色信息由RGB组合或者灰度值表示。根据位深度可将位图分为1、4、8、16、24及32位图像等。每个像素使用的信息位数越多,可用的颜色就越多,颜色表现就越逼真,相应的数据量越大。例如,位深度为 1 的像素位图只有两个可能的值(黑色和白色),所以又称为二值位图。位深度为 8 的图像有 2(即 256)个可能的值。位深度为 8 的灰度模式图像有 256 个可能的灰色值。RGB图像由三个颜色通道组成。8 位/通道的RGB图像中的每个通道有 256 个可能的值,这意味着该图像有 1600 万个以上可能的颜色值。有时将带有 8 位/通道(bpc)RGB图像称作 24 位图像。通常将使用24位RGB组合数据位表示的的位图称为真彩色位图。

    由上面的描述可知,我们可以将bitmap理解为一个点阵图或者是一个数组,其中的每个元素都是一个像素信息,假设对于一个32位RGBA图像来说,则每个元素包含着三个颜色组件(R,G,B)和一个Alpha组件,每一个组件占8位(8bite = 1byte = 32 / 4)。这些像素集合起来就可以表示出一张图片。

    2、iOS中的Bitmap

    Bitmap的数据由CGImageRef封装。由以下几个函数可以创建CGImageRef对象:

    CGImageCreate //最灵活,但也是最复杂的一种方式,要传入11个参数 
    CGImageSourceCreate:ImageAtIndex //通过已经存在的Image对象来创建
    CGImageSourceCreate:ThumbnailAtIndex //和上一个函数类似,不过这个是创建缩略图
    CGBitmapContextCreateImage //通过Copy Bitmap Graphics来创建
    CGImageCreateWith:ImageInRect //通过在某一个矩形内数据来创建
    

    如果要使用bitmap对图片进行各种处理,则需要先创建位图上下文。(CGBitmapContextCreate,Swift中则是CGContext)先看一下初始化方法的一个例子:

        let w = Int(image.size.width)// 位图的宽和高。
        let h = Int(image.size.height)
        let bitsPerComponent = 8// 颜色组件或者alpha组件占的bite数
        let bytesPerRow = w * 4// 位图的每一行占的字节数
        let colorSpace = CGColorSpaceCreateDeviceRGB()// 颜色空间,是RGBA、CMYK还是灰度值。
        let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue// 一个常量,描述这个位图上下文所对应的位图的基本信息
    
        let bufferData = UnsafeMutablePointer<UInt32>.allocate(capacity: w * h)
        bufferData.initialize(repeating: 0, count: w * h)
    
        let cxt = CGContext(data: bufferData, //data: 用于存放位图的点阵数据,当生成上下文并调用 CGContextDrawImage 方法将指定图片绘制进上下文之后,data里面就会有该图片的位图像素信息,可以当做一个数组指针来使用。我们可以对这个data里面的内容进行操作,然后以这个data为主要参数通过生成 CGDataProvider 实例并调用 CGImageCreate 方法来重新生成一个CGImage。
                                    width: w,// 位图的宽和高。
                                    height: h,// 如width = 10,height = 20则代表每一行有10个像素,每一列有20个像素。
                                    bitsPerComponent: bitsPerComponent,// 颜色组件或者alpha组件占的bite数。以32位图像为例:bitsPerComponent = 8
                                    bytesPerRow: bytesPerRow,// 位图的每一行占的字节数。以32位图像为例:一个像素有4byte(rgba),那么bytesPerRow = width * 4
                                    space: colorSpace,// 颜色空间,是RGBA、CMYK还是灰度值。RGBA : CGColorSpaceCreateDeviceRGB( )、 CMYK : CGColorSpaceCreateDeviceCMYK( ) 、灰度值 : CGColorSpaceCreateDeviceGray( )
                                    bitmapInfo: bitmapInfo)// 一个常量,描述这个位图上下文所对应的位图的基本信息。通常是多个枚举值做或运算的最终值(CGBitmapInfo 和 CGImageAlphaInfo)。比如可以置顶是否具有alpha通道,alpha通道的位置(是RGBA还是ARGB),字节排列的顺序等等。
    

    3、实际应用

    ❶ 获取图片中点击位置的颜色
    1. 获取imageView控件bounds范围内像素数据。
    2. 通过CGPoint的参数计算出该点对应的像素的索引位置并取出像素数据。
    3. 逐个字节解析出rgb颜色分量和alpha分量值最后生成UIColor最为结果返回。
    extension UIImageView {
        
        func color(forPoint p : CGPoint) -> UIColor? {
            guard let pixels = self.pixels else {
                return nil
            }
            guard let index = pixelIndex(for: p) else {
                return nil
            }
            let color = self.color(forPixel: pixels[index])
            return color
        }
    
        /*
         获取图片的像素数据
         */
        var pixels : [UInt32]? {
            return self.getPixelsData(inRect: self.bounds)
        }
        /*
         根据坐标点获取该点对应的像素所在数组中的索引
         - p : 置顶的坐标点
         */
        func pixelIndex(for p : CGPoint) -> Int? {
            let size = self.bounds.size
            guard p.x > 0 && p.x <= size.width && p.y > 0 && p.y < size.height else {
                return nil
            }
            // 相当于 height * bytesPerRow + x
            let floatIndex = Int(size.width * p.y + p.x)
            let intIndex = Int(size.width) * Int(p.y) + Int(p.x)
            print("float index : \(floatIndex), intIndex : \(intIndex)")
            // 这里一定要都转换成Int类型再求值,否则最后算出来的index会有偏差
            return Int(size.width) * Int(p.y) + Int(p.x)
        }
        
        func color(forPixel pixel: UInt32) -> UIColor {
            // 创建位图上下文的时候,可以指定两种bitmapInfo
            // 如果指定了premultipliedFirst,说明颜色组件是以 alpha red green blue 的顺序排列的
            // 如果指定了premultipliedLast,说明颜色组件是以 red green blue alpha 的顺序排列的
            // 那么下面解析r,g,b,a四个值的时候的顺序就会有所差别。
            let r = CGFloat((pixel >> 0)  & 0xff) / 255.0
            let g = CGFloat((pixel >> 8)  & 0xff) / 255.0
            let b = CGFloat((pixel >> 16) & 0xff) / 255.0
            let a = CGFloat((pixel >> 24) & 0xff) / 255.0
            print("r : \(r), g : \(g), b : \(b), a : \(a)")
            let color = UIColor(displayP3Red: r, green: g, blue: b, alpha: 1)
            return color
        }
        
        /*
         获取图片中指定范围内的位图数据(rgba数组)
         - rect : 置顶要获取像素数组的范围
         生成rect范围内的像素数据,较为耗时,所以在真正使用的时候最好有缓存策略。
         */
        func getPixelsData(inRect rect : CGRect) -> [UInt32]? {
            
            guard let img = self.image, let cgImg = img.cgImage else {
                return nil
            }
            /*
             不能直接以image的宽高作为绘制的宽高,因为image的size可能会比控件的size大很多。
             所以在生成bitmapContext的时候需要以实际的控件宽高为准
             */
            let w = Int(rect.size.width)
            let h = Int(rect.size.height)
            let bitsPerComponent = 8 // 32位的图像,所以每个颜色组件包含8bit
            let bytesPerRow = w * 4  // 1 byte = 8 bit, 32位图像的话,每个像素包含4个byte
            let colorSpace = CGColorSpaceCreateDeviceRGB()
            let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue // RGBA
            // let bitmapInfo = CGImageAlphaInfo.premultipliedFirst.rawValue // ARGB
            // 因为是32位图像,RGBA各占8位 8*4=32,所以像素数据的数组的元素类型应该是UInt32。
            var bufferData = Array<UInt32>(repeating: 0, count: w * h)
            guard let cxt = CGContext(data: &bufferData, width: w, height: h, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
                return nil
            }
            // 将图像绘制进上下文中
            cxt.draw(cgImg, in: rect)
            return bufferData
        }
    }
    

    【注意1】
    步骤1获取图像的像素数据(getPixelsData)是耗时操作。
    在实际使用时应该在获取之后进行缓存,以备之后重复使用。

    【注意2】
    代码中计算指定像素的索引位置时,要把各个计算元素都先转换成Int类型之后再计算,如果以CGFloat类型计算完再转换成Int类型的话,得到的索引值会有偏差,导致获取到的颜色不正确。

    Int(size.width * p.y + p.x) --> 错误
    Int(size.width) * Int(p.y) + Int(p.x) --> 正确
    
    改进应用一中的方案

    另一种思路获取点击位置的颜色。上面的getPixelsData需要获取整张图片的像素数据,对于只想要取得某一个点位置的颜色来说,效率较低。所以只生成容纳一个像素的bitmap,根据 p 点的位置对BitmapContext进行平移变换,使 BitmapContext的绘制原点位于 p 点。(默认渲染原点是在左上角),然后直接根据bufferData中像素数据生成颜色并返回。

        /*
         另一种思路获取点击位置的颜色。上面的getPixelsData需要获取整张图片的像素数据,
         对于只想要取得某一个点位置的颜色来说,效率较低。所以只生成容纳一个像素的bitmap,
         然后直接根据bufferData中像素数据生成颜色并返回
         */
        func getColor(fromPoint p : CGPoint) -> UIColor? {
            
            let w = 1
            let h = 1
            let bitsPerComponent = 8
            let bytesPerRow = w * 4
            let colorSpace = CGColorSpaceCreateDeviceRGB()
            let bitmapInfo = CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.noneSkipLast.rawValue // RGBA
            // 可以声明为一个有1个元素的UInt32数组
            var bufferData = Array<UInt32>(repeating: 0, count: 1)
            // 或者为一个有4个元素的UInt8数组
            // var bufferData = Array<UInt8>(repeating: 0, count: 4)
    
            guard let cxt = CGContext(data: &bufferData, width: w, height: h, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
                return nil
            }
            /*
             这里需要注意,由于上边生成的位图上下文只包含一个像素数据,相当于一个点。
             而这个位图上下文的默认渲染原点是图片的左上角,也就是(0,0)的位置,如果直接从bufferData获取的话,其实是图片左上角第一个像素的颜色。
             所以这里需要将位图上下文做一个反方向的平移变换,使p点成为位图上下文的渲染原点
             */
            cxt.translateBy(x: -p.x, y: -p.y)
            
            /*
             将图像渲染到上下文中,这里需要注意的是,需要在平移之后才渲染,否则获取到的颜色不正确。
             */
            layer.render(in: cxt)
            
            // 只包含一个UInt32像素数据
            let component = bufferData.first!
            let r = CGFloat((component >> 0)  & 0xff) / 255.0
            let g = CGFloat((component >> 8)  & 0xff) / 255.0
            let b = CGFloat((component >> 16) & 0xff) / 255.0
            let a = CGFloat((component >> 24) & 0xff) / 255.0
            
            // 包含四个UInt8(每一个元素代表RGBA中的一个)元素的数组
            // let r = CGFloat(bufferData[0]) / 255.0
            // let g = CGFloat(bufferData[1]) / 255.0
            // let b = CGFloat(bufferData[2]) / 255.0
            // let a = CGFloat(bufferData[3]) / 255.0
    
            let color = UIColor(displayP3Red: r, green: g, blue: b, alpha: a)
            return color
        }
    
    ❷ 设置图片透明度
    func setAlpha(_ alpha : CGFloat, sourceImage : UIImage?) -> UIImage? {
        guard let img = sourceImage, let cgImg = img.cgImage else {
            return nil
        }
        UIGraphicsBeginImageContextWithOptions(img.size, false, 1)
        
        guard let cxt = UIGraphicsGetCurrentContext() else {
            return nil
        }
        // 调用draw方法之后,图片的downMirror的,所以这里需要提前做一下反转和平移变换
        cxt.scaleBy(x: 1, y: -1)
        cxt.translateBy(x: 0, y: -img.size.height)
        cxt.setBlendMode(CGBlendMode.multiply)
        cxt.setAlpha(alpha)
        cxt.draw(cgImg, in: CGRect(x: 0, y: 0, width: img.size.width, height: img.size.height))
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return newImage
    }
    
    ❸ 二分法压缩图片大小
        static func compress2Data(_ comressImage: UIImage,
                                  limitBytes maxBytesLength: Int) -> Data {
            var max: CGFloat = 1
            var min: CGFloat = 0
            var compression: CGFloat = 1
            var compressedData: Data! = nil
            for _ in 0 ..< 6 {
                compression = (max + min) / 2
                compressedData = comressImage.jpegData(compressionQuality: compression)!
                if CGFloat(compressedData.count) < CGFloat(maxBytesLength) * 0.9 {
                    min = compression
                } else if compressedData.count > maxBytesLength {
                    max = compression
                } else {
                    break
                }
            }
            return compressedData
        }
    
    ❹ 获取图片格式
    public enum ImageFormatType {
        case jpg
        case png
        case gif
        case webP
        case unknown
    }
    
    public struct ImageHeaderData {
        static var PNG: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
        static var JPEG_SOI: [UInt8] = [0xFF, 0xD8]
        static var JPEG_IF: [UInt8] = [0xFF]
        static var GIF: [UInt8] = [0x47, 0x49, 0x46]
    }
    
    extension Data {
        public var imageFormatType: ImageFormatType {
            var buffer = [UInt8](repeating: 0, count: 8)
            (self as NSData).getBytes(&buffer, length: 8)
            if buffer == ImageHeaderData.PNG {
                return .png
            } else if buffer[0] == ImageHeaderData.JPEG_SOI[0] &&
                buffer[1] == ImageHeaderData.JPEG_SOI[1] &&
                buffer[2] == ImageHeaderData.JPEG_IF[0]
            {
                return .jpg
            } else if buffer[0] == ImageHeaderData.GIF[0] &&
                buffer[1] == ImageHeaderData.GIF[1] &&
                buffer[2] == ImageHeaderData.GIF[2]
            {
                return .gif
            }
            
            if count < 12 {
                return .unknown
            }
            
            let endIndex = index(startIndex, offsetBy: 12)
            let testData = subdata(in: startIndex..<endIndex)
            guard let testString = String(data: testData, encoding: .ascii) else {
                return .unknown
            }
            
            if testString.hasPrefix("RIFF") && testString.hasSuffix("WEBP") {
                return .webP
            } else {
                return .unknown
            }
        }
    }
    
    ❺ 将图像中最多的颜色替换成另一种颜色
    1. 根据指定图片生成位图上下文,调用 draw 方法之后可以获取到这个图片的像素数据(bufferData)。
    2. 生成一个小尺寸的位图上下文,统计这个上下文的像素数据中出现次数最多的rgba并返回(getMaxCountColor方法)
    3. 遍历 步骤1 中生成的每一个像素,如果rgb三个颜色值的偏差值小于我们所指定的偏差值(leeway),就将这个像素的颜色改为我们所要指定的rgb颜色。
    4. 将修改颜色之后的像素数据(bufferData)当做参数生成CGDataProvider实例,然后以CGDataProvider实例为参数生成CGImage实例。
    5. 最后通过CGImage实例生成修改了像素颜色的 UIImage实例并返回。
    /*
     将图像中出现次数最多的颜色修改为置顶的颜色
     */
    func changeMaxCountColorToColor(withRed red : Int,
                                    green : Int,
                                    blue : Int,
                                    alpha : CGFloat,
                                    leeway : Float,
                                    sourceImage : UIImage?) -> UIImage? {
        guard let image = sourceImage, let cgImage = image.cgImage else {
            return nil
        }
        let w = Int(image.size.width)
        let h = Int(image.size.height)
        let bitsPerComponent = 8
        let bytesPerRow = w * 4
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
        // var bufferData = Array<UInt32>(repeating: 0, count: w * h)
        // var bufferData = [UInt32](repeatElement(0, count: w*h))
        let bufferData = UnsafeMutablePointer<UInt8>.allocate(capacity: w * h * 4)
        bufferData.initialize(repeating: 0, count: w * h)
        
        guard let cxt = CGContext(data: bufferData, width: w, height: h, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
            return nil
        }
        cxt.draw(cgImage, in: CGRect(x: 0, y: 0, width: CGFloat(w), height: CGFloat(h)))
        guard let maxC = getMaxCountColor(image) else {
            return nil
        }
        for i in 0 ..< w * h {
            let byteStart = i * 4
            let r = Float(bufferData.advanced(by: byteStart).pointee)
            let g = Float(bufferData.advanced(by: byteStart + 1).pointee)
            let b = Float(bufferData.advanced(by: byteStart + 2).pointee)
            if abs(Float(maxC.r)-r) < leeway && abs(Float(maxC.g)-g) < leeway && abs(Float(maxC.b)-b) < leeway {
                bufferData.advanced(by: byteStart).pointee = UInt8(red)
                bufferData.advanced(by: byteStart+1).pointee = UInt8(green)
                bufferData.advanced(by: byteStart+2).pointee = UInt8(blue)
                //传进来的alpha是归一化的值,所以这里需要除以255.0
                bufferData.advanced(by: byteStart+3).pointee = UInt8(alpha * 255.0)
            }
        }
        let dataProvider = CGDataProvider(dataInfo: nil, data: bufferData, size: bytesPerRow * h) {
            (_, data, _) in
            data.deallocate()
        }
        guard let provider = dataProvider else {
            return nil
        }
        
        let cgBitmapInfoUInt32 = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
        let cgBitmapInfo = CGBitmapInfo(rawValue: cgBitmapInfoUInt32)
        let newCGImageOptional = CGImage(width: w, height: h,
                                         bitsPerComponent: 8,
                                         bitsPerPixel: 32,
                                         bytesPerRow: bytesPerRow,
                                         space: colorSpace,
                                         bitmapInfo: cgBitmapInfo,
                                         provider: provider,
                                         decode: nil,
                                         shouldInterpolate: true,
                                         intent: CGColorRenderingIntent.defaultIntent)
        guard let newCGImage = newCGImageOptional else {
            return nil
        }
        let newImage = UIImage(cgImage: newCGImage)
        // 如果在这里将bufferData的内存释放,那么会导致新图片赋值到imageView.image之后看不到图片
        // 应该在创建 CGDataProvider 时的回调函数里面释放
        // bufferData.deinitialize(count: w*h)
        // bufferData.deallocate()
        return newImage
    }
    
    /*
     获取出现次数最多的rgba
     */
    func getMaxCountColor(_ image : UIImage?) -> (r : Int, g : Int, b : Int, a : Int, pixelColor : UInt32)? {
        guard let image = image, let cgImage = image.cgImage else {
            return nil
        }
        // 先把图片缩小 加快计算速度. 但越小结果误差可能越大
        let w = 150
        let h = 150
        let bitsPerComponent = 8
        let bytesPerRow = w * 4
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue // RGBA
        var bufferData = Array<UInt32>(repeating: 0, count: w * h)
        guard let cxt = CGContext(data: &bufferData, width: w, height: h, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
            return nil
        }
        cxt.draw(cgImage, in: CGRect(x: 0, y: 0, width: CGFloat(w), height: CGFloat(h)))
        var colorCountDic = [UInt32 : Int]()
        var maxCountColor : UInt32 = 0
        let colorNum = w * h
        for i in 0 ..< colorNum {
            let color = bufferData[I]
            if let count = colorCountDic[color] {
                colorCountDic[color] = count + 1
            } else {
                colorCountDic[color] = 1
            }
            if let maxColorCount = colorCountDic[maxCountColor] {
                if colorCountDic[color]! > maxColorCount {
                    maxCountColor = color
                }
            } else {
                maxCountColor = color
            }
        }
        let r = Int((maxCountColor >> 0)  & 0xff)
        let g = Int((maxCountColor >> 8)  & 0xff)
        let b = Int((maxCountColor >> 16) & 0xff)
        let a = Int((maxCountColor >> 24) & 0xff)
        return (r,g,b,a, maxCountColor)
    }
    
    /*
     将图像中出现次数最多的颜色变为透明色
     */
    func changeMaxCountColorToTransparent(leeway : Float, image : UIImage?) -> UIImage? {
        return changeMaxCountColorToColor(withRed: 0, green: 0, blue: 0, alpha: 0, leeway: 10, sourceImage: image)
    }
    

    【注意1】
    bufferData 的生成方式和上面的例子不同了。上面的 getPixelsData 方法里面 bufferData 是一个 Swift 数组,在从图像中获取像素数据之后直接使用即可。但是在这个例子里面 bufferDataUnsafeMutablePointer类型。原因是如果 bufferDataSwift数组的话,在后面生成 CGDataProvider 实例并最终生成UIImage之后,放在控件上显示不出来。

    UnsafeMutablePointer<UInt32>类型相当于C语言的数组指针,即C语言的数组。若想使用该类型的变量的话需要自己创建并分配内存和释放。 还有一点需要注意的是,如果使用 UnsafeMutablePointer 变量,就不需要在传参的时候写上取地址符(&bufferData)了,如果是普通的Swift数组的话则需要加上。

    【注意2】
    这里使用了 UInt8 数组来存储像素(上面的例子里面是UInt32数组),原因是为了方便像素数据中颜色分量的重新赋值,而不用做各种按位与和或的运算。

        for i in 0 ..< w * h {
            let byteStart = i * 4
            let r = Float(bufferData.advanced(by: byteStart).pointee)
            let g = Float(bufferData.advanced(by: byteStart + 1).pointee)
            let b = Float(bufferData.advanced(by: byteStart + 2).pointee)
            if abs(Float(maxC.r)-r) < leeway && abs(Float(maxC.g)-g) < leeway && abs(Float(maxC.b)-b) < leeway {
                bufferData.advanced(by: byteStart).pointee = UInt8(red)
                bufferData.advanced(by: byteStart+1).pointee = UInt8(green)
                bufferData.advanced(by: byteStart+2).pointee = UInt8(blue)
                //传进来的alpha是归一化的值,所以这里需要除以255.0
                bufferData.advanced(by: byteStart+3).pointee = UInt8(alpha * 255.0)
            }
        }
    

    【注意3】
    修改完颜色并根据像素数据生成CGImage的时候也要指明bitmapInfo,因为最终的图片可以是半透明的,即我们需要alpha通道生效。所以此时应该指明 CGImageAlphaInfo.premultipliedLast或者 .premultipliedFirst


    二、CoreGraphics

    1、简介

    ❶ UIKit

    它是高级别的图形接口,它的API都是基于Objective-C的。它能够访问绘图、动画、字体、图片等内容。UIkit中坐标系的原点在左上角,而Quartz 2D的坐标系的原点在左下角。

    ❷ Quartz2D

    是一个二维(二维即平面)绘图引擎(封装的一套用于绘图的函数库),同时支持iOS和Mac系统(可以跨平台开发)。API(应用程序界面)是纯C语言的,来自于Core Graphics框架,其数据类型和函数基本都以CG作为前缀。

    ❸ Quartz2D作用
    • 自定义view:iOS中大部分系统控件的内容都是通过Quartz2D画出来的,有些UI界面极其复杂、而且比较个性化,用普通的UI控件无法实现,这时可以利用Quartz2D技术,通过重写drawRect:方法将其画出来 。
    • 裁剪图片(矩形图片裁剪成圆形图片)
    • 基于路径的绘图,如线条、三角形、矩形、圆、弧等
    • 透明度绘图、遮盖、阴影
    • 颜色管理、防锯齿渲染
    ❹ 一些绘图概念
    • position:它是用来设置当前的layer在父控件当中的位置的,默认它的坐标原点,以父控件的左上角为(0.0)点。
    • anchorPoint:锚点,就是把锚点定到position所指的位置。它是决点CALayer身上哪一个点会在position属性所指的位置,anchorPoint它是以当前的layer左上角为原点(0.0),它的取值范围是0~1,它的默认在中间也就是(0.5,0.5)的位置。
    • setNeedsDisplay:重新绘制整个视图,系统就会自动帮你调用drawRect方法
    • setNeedsDisplayInRect:重新绘制视图的部分区域,最好不要绘制视图的全部,以减少绘制带来开销
    • setNeedsLayout:标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,在下一轮runloop结束前刷新,对于这一轮runloop之内的所有布局和UI上的更新只会刷新一次,layoutSubviews一定会被调用。修改了当前视图的size或者设置了不同的frame是会被系统会给你标记setNeedsLayout的。view的创建并不被标记为需要刷新的,除非你设置了一个CGRectZeroframe。初始化的frame可能被系统默认设置CGRectZero,还有addsubViews也会导致标记为需要刷新的。只是修改了当前视图的 x y,没有标记setNeedsLayout的话,layoutIfNeeded 是不会调用的。
    • layoutIfNeeded:如果有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)。如果想在当前runloop中立即刷新,调用顺序应该是:
    [self setNeedsLayout];
    [self layoutIfNeeded];
    
    • layoutSubviews

    继承于UIView的子类重写,进行布局更新,刷新视图。如果某个视图自身的bounds或者子视图的bounds发生改变,那么这个方法会在当前runloop结束的时候被调用。为什么不是立即调用呢?因为渲染毕竟比较消耗性能,特别是视图层级复杂的时候。这种机制下任何UI控件布局上的变动不会立即生效,而是每次间隔一个周期,所有UI控件在布局上的变动统一生效并且在视图上更新,苹果通过这种高性能的机制保障了视图渲染的流畅性。

    runloopobserver回调=>CoreAnimation渲染引擎一次事务的提交=>CoreAnimation递归查询图层是否有布局上的更新=>CALayer layoutSublayers=>UIView layoutSubviews这样一个调用的流程。从这里也可以看到UIView其实就是相当于CALayer的代理。

    ❺ 触发视图重新绘制的动作
    • 当遮挡你的视图的其他视图被移动或删除操作的时候;
    • 将视图的hidden属性声明设置为NO,使其从隐藏状态变为可见;
    • 将视图滚出屏幕,然后再重新回到屏幕上;
    • 显式调用视图的setNeedsDisplay或者setNeedsDisplayInRect:方法
    • 当继承自UIView的视图被创建,设置frame后,并且已经添加到某个view,则会调用;若不设置frame则不会触发drawRect
    • view某一属性例如背景色发生改变时,调用layoutIfNeeded,会触发drawRect;若view中布局或属性没有发生变化,则不会调用。

    2、drawRect: 常用方法

    常用拼接路径函数:

    // 新建一个起点
    void CGContextMoveToPoint(CGContextRef c, CGFloat x, CGFloat y)
    
    // 添加新的线段到某个点
    void CGContextAddLineToPoint(CGContextRef c, CGFloat x, CGFloat y)
    
    // 添加一个矩形
    void CGContextAddRect(CGContextRef c, CGRect rect)
    
    // 添加一个椭圆
    void CGContextAddEllipseInRect(CGContextRef context, CGRect rect)
    
    // 添加一个圆弧
    void CGContextAddArc(CGContextRef c, CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, CGFloat endAngle, int clockwise)
    

    常用绘制路径函数:

    //Mode参数决定绘制的模式
    void CGContextDrawPath(CGContextRef c, CGPathDrawingMode mode)
    
    //绘制空心路径(注意不是线段!)
    void CGContextStrokePath(CGContextRef c)
    
    //绘制实心路径(注意不是线段!)
    void CGContextFillPath(CGContextRef c)
    

    图形上下文栈的操作:

    //将当前的上下文copy一份,保存到栈顶(那个栈叫做”图形上下文栈”)
    void CGContextSaveGState(CGContextRef c)
    
    //将栈顶的上下文出栈,替换掉当前的上下文
    void CGContextRestoreGState(CGContextRef c)
    

    矩阵操作:利用矩阵操作,能让绘制到上下文中的所有路径一起发生变化

    //缩放
    void CGContextScaleCTM(CGContextRef c, CGFloat sx, CGFloat sy)
    
    //旋转
    void CGContextRotateCTM(CGContextRef c, CGFloat angle)
    
    //平移
    void CGContextTranslateCTM(CGContextRef c, CGFloat tx, CGFloat ty)
    

    3、drawRect: 自定义view的步骤

    1、新建一个类,继承自UIView
    2、在-(void)drawRect:(CGRect)rect方法实现下述几步,注意只有在drawRect方法中才能拿到图形上下文,才可以画图。

    - (void)drawRect:(CGRect)rect {
    }
    

    3、取得跟当前view相关联的图形上下文。

        // 固定操作,上下文压栈
        CGContextRef context = UIGraphicsGetCurrentContext();
        UIGraphicsPushContext(context);
    

    4、拼接路径,绘制相应的图形内容。

        //贝塞尔曲线
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:leftTop];
        [path addLineToPoint:anchor];
        [path addLineToPoint:leftBottom];
        [path closePath];
    

    5、把路径添加到上下文。

        CGContextAddPath(context, path.CGPath);
    

    6、设置绘图状态。

        // 描边宽度
        CGContextSetLineWidth(context, lineWidth);
        // 描边颜色
        CGContextSetStrokeColorWithColor(context, lineColor.CGColor);
        CGContextSetFillColorWithColor(context, timeLineColor.CGColor);
    

    7、利用图形上下文将绘制的所有内容渲染显示到view上面。

        CGContextStrokePath(context);
        CGContextFillPath(context);
    

    4、drawRect: 绘制直线

    一条直线
    一条直线
    - (void)drawRect:(CGRect)rect
    {
        [self drawStraightLine];
    }
    
    // 一条直线
    - (void)drawStraightLine
    {
        // 获取图形上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
    // 一、直接使用图形上下文画图
        // 描述路径
        CGContextMoveToPoint(context, 10, 19);// 起始点
        CGContextAddLineToPoint(context, 100, 65);// 添加线
        
        // 完成路线绘制
        CGContextStrokePath(context);
        
    // 二、使用图形上下文+CGPathRef画线
        // 使用path来画线
        CGMutablePathRef path = CGPathCreateMutable();
        // 添加点
        CGPathMoveToPoint(path, NULL, 34, 23);
        CGPathAddLineToPoint(path, NULL, 100, 43);
        // 将path添加到图像上下文上
        CGContextAddPath(context, path);
        // 渲染上下文
        CGContextStrokePath(context);
        
    // 三、可以只使用贝塞尔曲线画线,不获取图形上下文,因为在底层系统已经给封装了
        // 1.创建路径
        UIBezierPath *bezierPath = [UIBezierPath bezierPath];
        // 2.画线
        [bezierPath moveToPoint:CGPointMake(80, 45)];
        [bezierPath addLineToPoint:CGPointMake(150, 88)];
        // 3.渲染绘制
        [bezierPath stroke];
        
    // 四、同时使用图形上下文+贝塞尔曲线画线
        // 使用贝塞尔曲线和图形上下文画图
         UIBezierPath *contextPath = [UIBezierPath bezierPath];
         [contextPath moveToPoint:CGPointZero];
         [contextPath addLineToPoint:CGPointMake(233, 69)];
        //这个是C语言的写法,必须使用path.CGPath,否则无效
         CGContextAddPath(context, contextPath.CGPath);
         CGContextStrokePath(context);
    }
    

    用四种方法,说明绘制图形有很多种方法,绘制图形实际上就是一直设置path,底层的用的都是CGMutablePathRef。使用贝塞尔曲线画图的好处在于,每一个贝塞尔底层都有一个图形上下文,如果是用CGContextMoveToPoint画图,实际上就是一个图形上下文,不好去控制,所以建议多条线可以使用贝塞尔曲线。不推荐使用第4种方式,两个东西杂糅,不太好。

    相交直线
    相交直线
    // 只有在drawRect方法中才能拿到图形上下文,才可以画图
    - (void)drawRect:(CGRect)rect
    {
        [self drawIntersectingLines];
    }
    
    - (void)drawIntersectingLines
    {
        // 1.获取图形上文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        // 2.绘制路径,并且将其添加到图形上下文
        CGContextMoveToPoint(context,123, 45);// 起始点
        CGContextAddLineToPoint(context, 45, 80);// 添加一根线
        CGContextAddLineToPoint(context, 223, 159);// 添加另一根线
    
        // 3.设置路径属性
        [[UIColor greenColor] set];// 设置颜色,这种设置方式就不用考虑实线还是填充图形了
        CGContextSetLineWidth(context, 10);// 设置线的宽度
        CGContextSetLineJoin(context, kCGLineJoinMiter);// 设置链接处的链接类型
        CGContextSetLineCap(context, kCGLineCapButt);// 设置线的头部样式
    
        // 4.渲染绘制
        CGContextStrokePath(context);
    }
    
    两条不相交的线段
    两条不相交的线段
    // 只有在drawRect方法中才能拿到图形上下文,才可以画图
    - (void)drawRect:(CGRect)rect
    {
        [self drawTwoLine];
    }
    
    - (void)drawTwoLine
    {
        // 设置红色线段
        UIBezierPath *redPath = [UIBezierPath bezierPath];
        [redPath moveToPoint:CGPointMake(12, 49)];
        [redPath addLineToPoint:CGPointMake(68, 34)];
        [redPath setLineWidth:3];
        [[UIColor redColor] set];
        [redPath stroke];
    
        // 设置绿色线段
        UIBezierPath *greenPath = [UIBezierPath bezierPath];
        [greenPath moveToPoint:CGPointMake(145, 167)];
        [greenPath addLineToPoint:CGPointMake(98, 34)];
        [greenPath setLineWidth:10];
        [[UIColor greenColor] set];
        [greenPath setLineCapStyle:kCGLineCapRound];
        [greenPath stroke];
    }
    
    绘制温度计🌡️
    温度计
    // 只有在drawRect方法中才能拿到图形上下文,才可以画图
    - (void)drawRect:(CGRect)rect
    {
        [self drawThermometerLine];
    }
    
    // 绘制温度计
    - (void)drawThermometerLine
    {
        // 获取图形上文
        CGContextRef context = UIGraphicsGetCurrentContext();
        UIGraphicsPushContext(context);// 压入上下文
        
        // 画线
        CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);// 红线
        CGContextSetLineWidth(context, 5.0);// 线宽
        
        CGFloat height = self.bounds.size.height;
        
        if (!_isTop)// 线帽在底部
        {
            CGPoint top = CGPointMake(150, 0);// 线顶
            CGPoint cycleTop = CGPointMake(150, 16 - 6 - 5);// 线帽顶
            
            // 创建线条路径
            UIBezierPath *topPath = [UIBezierPath bezierPath];
            [topPath moveToPoint:top];
            [topPath addLineToPoint:cycleTop];
            CGContextAddPath(context, topPath.CGPath);
        }
        
        if (!_isBottom)// 线帽在顶部
        {
            CGPoint cycleBottom = CGPointMake(150, 16 + 6 + 5);// 线帽底
            CGPoint bottom = CGPointMake(150, height);// 线底
            
            // 创建线条路径
            UIBezierPath *bottomPath = [UIBezierPath bezierPath];
            [bottomPath moveToPoint:cycleBottom];
            [bottomPath addLineToPoint:bottom];
            CGContextAddPath(context, bottomPath.CGPath);
        }
        
        // 渲染绘制线条
        CGContextStrokePath(context);
        
        // 画线帽
        CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);// 黄色
        CGPoint center = CGPointMake(150, 16);// 圆心
        
        UIBezierPath *cyclePath = [UIBezierPath bezierPath];
        [cyclePath moveToPoint:CGPointMake(center.x, center.y+6)];// 圆心
        [cyclePath addArcWithCenter:center radius:6 startAngle:0 endAngle:M_PI*2 clockwise:YES];// 圆
        [cyclePath closePath];// 闭合
        CGContextAddPath(context, cyclePath.CGPath);
        CGContextFillPath(context);
    }
    
    // 温度计上下颠倒
    - (void)isTop:(BOOL)isTop isBottom:(BOOL)isBottom
    {
        _isTop = isTop;
        _isBottom = isBottom;
        [self setNeedsDisplay];
    }
    
    绘制点线
    绘制点线
    // 只有在drawRect方法中才能拿到图形上下文,才可以画图
    - (void)drawRect:(CGRect)rect
    {
        [self drawDotLine];
    }
    
    // 绘制点线
    - (void)drawDotLine
    {
        // 获取图形上文
        CGContextRef context = UIGraphicsGetCurrentContext();
        UIGraphicsPushContext(context);// 压入上下文
        
        // 设置线属性
        UIColor *lineColor = [UIColor redColor];
        CGFloat height = 10;
        
        CGContextSetLineWidth(context, 10);// 点的高度
        CGContextSetStrokeColorWithColor(context, lineColor.CGColor);// 红线
        CGFloat lengths[] = {3, 2};// 表示先绘制3个点,再跳过2个点,如此反复
        // phase参数表示在第一个虚线绘制的时候跳过多少个点,这里为0
        CGContextSetLineDash(context, 0, lengths, 2);// 2表示lengths数组的长度
        
        // 绘制线
        CGPoint left = CGPointMake(20, height/2);
        CGPoint right = CGPointMake(200, height/2);
        
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:left];// 起始点
        [path addLineToPoint:right];// 结束点
        
        // 将线添加到上下文
        CGContextAddPath(context, path.CGPath);
        
        // 渲染绘制
        CGContextStrokePath(context);
    }
    

    5、drawRect: 绘制曲线

    绘制一条曲线
    绘制一条曲线
    // 只有在drawRect方法中才能拿到图形上下文,才可以画图
    - (void)drawRect:(CGRect)rect
    {
        [self drawCurve];
    }
    
    // 绘制一条曲线
    - (void)drawCurve
    {
        // 获取图形上文
        CGContextRef context = UIGraphicsGetCurrentContext();
    
        // 设置起点
        CGContextMoveToPoint(context, 10, 10);
        
        // 图形上下文、需要要突出点的x和y值、曲线结束点的x和y值
        CGContextAddQuadCurveToPoint(context, 50, 50, 100, 10);
    
        // 设置颜色
        [[UIColor redColor] set];
    
        // 渲染图层
        CGContextStrokePath(context);
    }
    

    6、drawRect: 绘制几何图形

    三角形、圆、圆饼、心
    三角形、圆、圆饼、心
    - (void)drawRect:(CGRect)rect
    {
        [self collectionView];
    }
    
    // 三角形、圆、圆饼、心
    - (void)collectionView
    {
        CGContextRef context = UIGraphicsGetCurrentContext();
        UIGraphicsPushContext(context);
        
        // timeline部分
        UIColor *timeLineColor = [UIColor redColor];
        
        //============================================
        // 描边
        CGContextSetStrokeColorWithColor(context, timeLineColor.CGColor);// 描边颜色
        CGContextSetLineWidth(context, 2.0);// 描边线宽
        
    // 三角形
        CGPoint triangleTop = CGPointMake(50, 10);// 顶点
        CGPoint triangleLeft = CGPointMake(0, 60);// 左点
        CGPoint triangleRight = CGPointMake(100, 60);// 右点
        
        UIBezierPath *trianglePath = [UIBezierPath bezierPath];// 创建路径
        [trianglePath moveToPoint:triangleTop];// 起始点
        [trianglePath addLineToPoint:triangleLeft];// 左边
        [trianglePath addLineToPoint:triangleRight];// 右边
        [trianglePath closePath];// 闭合
        
        CGContextAddPath(context, trianglePath.CGPath);// 添加路径
        
    // 圆
        CGPoint strokeCenter = CGPointMake(150, 30);// 圆心
        UIBezierPath *strokeCyclePath = [UIBezierPath bezierPathWithArcCenter:strokeCenter radius:25 startAngle:0 endAngle:M_PI*2 clockwise:YES];// 圆形
        [strokeCyclePath closePath];// 闭合
        
        CGContextAddPath(context, strokeCyclePath.CGPath);// 添加路径
        
        // 描边
        CGContextStrokePath(context);
        
        //==============================
        // 填充
        CGContextSetFillColorWithColor(context, timeLineColor.CGColor);// 填充颜色
        
    // 圆饼
        CGPoint fillCenter = CGPointMake(220, 30);// 圆心
        UIBezierPath *fillCyclePath = [UIBezierPath bezierPathWithArcCenter:fillCenter radius:25 startAngle:0 endAngle:M_PI*2 clockwise:YES];// 圆形
        [fillCyclePath closePath];// 闭合
        
        CGContextAddPath(context, fillCyclePath.CGPath);// 添加路径
        
    // 心:双圆弧+双贝塞尔曲线
        CGPoint startPoint = CGPointMake(260, 30);// 起始点
        
        UIBezierPath *heartPath = [UIBezierPath bezierPath];// 创建路径
        [heartPath moveToPoint:startPoint];// 起始点
        [heartPath addArcWithCenter:CGPointMake(280, 30) radius:20 startAngle:-M_PI endAngle:0 clockwise:YES];// 圆弧1
        [heartPath addArcWithCenter:CGPointMake(320, 30) radius:20 startAngle:-M_PI endAngle:0 clockwise:YES];// 圆弧2
        [heartPath addCurveToPoint:CGPointMake(300, 80) controlPoint1:CGPointMake(330, 60) controlPoint2:CGPointMake(330, 60)];// 贝塞尔曲线1
        [heartPath addCurveToPoint:startPoint controlPoint1:CGPointMake(270, 60) controlPoint2:CGPointMake(270, 60)];// 贝塞尔曲线2
        [heartPath closePath];// 闭合
        CGContextAddPath(context, heartPath.CGPath);// 添加路径
        
        // 填充
        CGContextFillPath(context);
    }
    
    圆角矩形
    圆角矩形
    - (void)drawRoundedRectangle
    {
        // 绘制一个圆角矩形
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(10, 10, 100, 40) cornerRadius:5];
        [[UIColor orangeColor] set];
        [path stroke];// 设置边框的颜色
    
        // 绘制一个圆角矩形
        UIBezierPath *path2 = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(10, 110, 100, 40) cornerRadius:5];
        [[UIColor greenColor] set];
        [path2 fill];// fill填充内部的颜色,必须是封闭的图形
    
        // 绘制一个圆形(不规范的绘制法:圆角直径是正方形的边长)
        UIBezierPath *path3 = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(140, 10, 100, 100) cornerRadius:50];
        [[UIColor cyanColor] set];
        [path3 fill];
    }
    
    弧线
    弧线
    - (void)drawRect:(CGRect)rect
    {
        [self drawArc];
    }
    
    // 弧线
    - (void)drawArc
    {
        /** 绘制弧度曲线
         *  ArcCenter 曲线中心
         *  radius       半径
         *  startAngle 开始的弧度
         *  endAngle 结束的弧度
         *  clockwise YES顺时针,NO逆时针
         */
        UIBezierPath *greenPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 100) radius:14 startAngle:0 endAngle:M_PI clockwise:YES];
        [[UIColor greenColor] set];
        [greenPath stroke];
    
        // 结束角度如果是 M_PI * 2 就是一个圆
        // M_PI是180度 M_PI_2是90度
        UIBezierPath *purplePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(240, 140) radius:40 startAngle:0 endAngle:M_PI * 2 clockwise:YES];
        [[UIColor purpleColor] set];
        [purplePath fill];
    
        // 绘制115度角的圆弧
        // 这里的角度都是弧度制度,如果我们需要15°,可以用15°/180°*π得到
        UIBezierPath *orangePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(160, 100) radius:20 startAngle:0 endAngle:115/360.0*(M_PI *2) clockwise:NO];
        [[UIColor orangeColor] set];
        [orangePath stroke];
    }
    
    扇形
    扇形
    - (void)drawRect:(CGRect)rect
    {
        [self drawSector];
    }
    
    // 扇形
    - (void)drawSector
    {
        // 获取图形上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        // 绘制曲线
        CGFloat centerX = 100;
        CGFloat centerY = 100;
        CGFloat radius = 30;
        
        // 添加一个点
        CGContextMoveToPoint(context, centerX, centerY);
        // 添加一个圆弧
        CGContextAddArc(context, centerX, centerY, radius, M_PI, M_PI_2, NO);
        // 关闭线段
        CGContextClosePath(context);
        // 渲染
        CGContextStrokePath(context);
    }
    

    7、drawRect: 绘制功能块

    下载进度
    进度条
    - (void)drawRect:(CGRect)rect
    {
        [self drawDownloadProgress];
    }
    
    // 多次调用drawRect
    - (void)setProgress:(CGFloat)progress
    {
        _progress = progress;
        
        // 要想调用drawRect:(CGRect)rect方法,必须要使用setNeedsDisplay,否则无效
        [self setNeedsDisplay];
    }
    
    // 下载进度
    - (void)drawDownloadProgress
    {
        CGFloat radius = self.frame.size.height * 0.5 - 10;// 半径
        CGFloat endAadius = self.progress * M_PI * 2 - M_PI_2;// 结束点
        
        // 创建路径
        UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius+5,radius+5) radius:radius-5 startAngle:-M_PI_2 endAngle:endAadius clockwise:YES];// 弧线
        path.lineWidth = 10;// 线宽
        path.lineCapStyle = kCGLineCapRound;// 圆角
        [[UIColor redColor] set];// 红色
        [path stroke];// 绘制
    }
    
    - (void)changeProgress:(id)sender
    {
        if ([sender isKindOfClass:[UISlider class]])
        {
            UISlider *slider = sender;
            self.drawView.progress = slider.value;
        }
    }
    
    饼状图
    饼状图
    - (void)drawRect:(CGRect)rect
    {
        [self drawPieChart];
    }
    
    // 画饼状图
    - (void)drawPieChart
    {
        CGPoint center = CGPointMake(100, 100);// 圆心
        CGFloat radius = 50;// 半径
        CGFloat startAngle = 0;// 开始角度
        CGFloat endAngle = 0;// 结束角度
        
        for (int i = 0; i < self.nums.count; I++)
        {
            NSNumber *data = self.nums[i];// 各块披萨上面的数字
            
            // startAngle跳转到最新的endAngle的位置
            startAngle = endAngle;
            
            // 重新计算endAngle的最新位置
            endAngle = startAngle + [data floatValue]/self.total * 2 * M_PI;
            
            // 路径
            UIBezierPath *path  = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
            
            // 连接弧形和圆心
            [path addLineToPoint:center];
            
            // 填充随机颜色
            CGFloat randRed = arc4random_uniform(256)/255.0;
            CGFloat randGreen = arc4random_uniform(256)/255.0;
            CGFloat randBlue = arc4random_uniform(256)/255.0;
            UIColor *randomColor = [UIColor colorWithRed:randRed green:randGreen blue:randBlue alpha:1.0];
            [randomColor set];
            [path fill];
        }
    }
    
    // 所有披萨的数字和
    - (NSInteger)total
    {
        if (_total == 0)
        {
            for (int j = 0; j < self.nums.count; j++)
            {
                _total += [self.nums[j] integerValue];
            }
        }
        return _total;
    }
    
    // 各块披萨上面的数字
    -(NSArray *)nums
    {
        if (!_nums)
        {
            _nums = @[@23,@34,@33,@13,@30,@44,@66];
        }
        return _nums;
    }
    
    柱状图
    柱状图
    - (void)drawRect:(CGRect)rect
    {
        [self drawBarChart:rect];
    }
    
    - (NSArray *)barNums
    {
        if (!_barNums)
        {
            _barNums = @[@23,@34,@93,@2,@55,@46];
        }
        return _barNums;
    }
    
    - (void)drawBarChart:(CGRect)rect
    {
        // 获取图形上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        // 两个柱状间距
        CGFloat margin = 30;
        
        // 每个柱状宽度 = (总宽度 - 柱状间距的宽度和)/ 柱状个数
        CGFloat width = (rect.size.width - (self.barNums.count + 1) * margin) / self.barNums.count;
        
        for (int i = 0; i < self.barNums.count; I++)
        {
            // 柱X坐标
            CGFloat x = margin + (width + margin) * I;
            
            // 计算柱Y坐标 = 总高度 - 柱状高度
            CGFloat num = [self.barNums[i] floatValue] / 100;// 柱所代表的数字
            CGFloat y = rect.size.height * (1 - num);// y坐标
            CGFloat height = rect.size.height * num;// 柱高度
            
            // 绘柱图
            CGRect barRect =  CGRectMake(x, y, width, height);
            CGContextAddRect(context,barRect);
            
            // 填充随机颜色
            CGFloat randRed = arc4random_uniform(256)/255.0;
            CGFloat randGreen = arc4random_uniform(256)/255.0;
            CGFloat randBlue = arc4random_uniform(256)/255.0;
            UIColor *randomColor = [UIColor colorWithRed:randRed green:randGreen blue:randBlue alpha:1.0];
            [randomColor set];
    
            CGContextFillPath(context);
        }
    }
    
    评分星星
    评分星星
    画评分星星
    @property (nonatomic, assign, readwrite) int starRadius;// 星星半径
    @property (nonatomic, assign, readwrite) int defaultNumber;// 默认画五个星
    @property (nonatomic, assign, readwrite) int starWithColorNumber;// 有几颗星星需要填充颜色
    @property (nonatomic, strong, readwrite) UIColor *starColor;// 描边颜色
    
    - (void)drawStar
    {
        CGFloat innerRadius = self.starRadius*sin(M_PI/10)/cos(M_PI/5);
        
        for (int i = 0; i < self.defaultNumber; I++)
        {
            // 创建星星视图
            UIView *starView = [[UIView alloc] initWithFrame:CGRectMake(2*self.starRadius*cos(M_PI/10)*i, 0, 2*self.starRadius*cos(M_PI/10), self.starRadius+self.starRadius*cos(M_PI/10))];
            
            // 创建星星形状图层
            CAShapeLayer *starMask = [CAShapeLayer layer];
            
            // 第一个点
            CGPoint firstPoint = CGPointMake(self.starRadius, 0);
            // 第二个点
            CGPoint secondPoint = CGPointMake(self.starRadius+innerRadius*sin(M_PI/5), self.starRadius - innerRadius*cos(M_PI/5));
            // 第三个点
            CGPoint thirdPoint = CGPointMake(self.starRadius*cos(M_PI/10)+self.starRadius, self.starRadius-self.starRadius*sin(M_PI/10));
            // 第四个点
            CGPoint forthPoint = CGPointMake(self.starRadius + innerRadius*sin(3*M_PI/10), self.starRadius + innerRadius*cos(3*M_PI/10));
            // 第五个点
            CGPoint fifthPoint = CGPointMake(self.starRadius*cos(M_PI*3/10)+self.starRadius, self.starRadius+self.starRadius*sin(M_PI*3/10));
            // 第六个点
            CGPoint sixthPoint = CGPointMake(self.starRadius, self.starRadius+innerRadius);
            // 第七个点
            CGPoint seventhPoint = CGPointMake(self.starRadius-self.starRadius*cos(M_PI*3/10), self.starRadius+self.starRadius*sin(M_PI*3/10));
            // 第八个点
            CGPoint eighthPoint = CGPointMake(self.starRadius - innerRadius*sin(3*M_PI/10), self.starRadius + innerRadius*cos(3*M_PI/10));
            // 第九个点
            CGPoint ninethPoint = CGPointMake(self.starRadius-self.starRadius*cos(M_PI/10), self.starRadius-self.starRadius*sin(M_PI/10));
            // 第十个点
            CGPoint tenthPoint = CGPointMake(self.starRadius - innerRadius*sin(M_PI/5), self.starRadius - innerRadius*cos(M_PI/5));
            
            // 创建路径图
            UIBezierPath *starMaskPath = [UIBezierPath bezierPath];
            [starMaskPath moveToPoint:firstPoint];
            [starMaskPath addLineToPoint:secondPoint];
            [starMaskPath addLineToPoint:thirdPoint];
            [starMaskPath addLineToPoint:forthPoint];
            [starMaskPath addLineToPoint:fifthPoint];
            [starMaskPath addLineToPoint:sixthPoint];
            [starMaskPath addLineToPoint:seventhPoint];
            [starMaskPath addLineToPoint:eighthPoint];
            [starMaskPath addLineToPoint:ninethPoint];
            [starMaskPath addLineToPoint:tenthPoint];
            [starMaskPath closePath];// 闭合路径
            starMask.path = starMaskPath.CGPath;// 星星形状图层的路径
            starMask.strokeColor = _starColor.CGColor;// 绘边颜色
            
            // 填充颜色
            if (i < self.starWithColorNumber)
            {
                starMask.fillColor = _starColor.CGColor;
            }
            else
            {
                starMask.fillColor = UIColor.whiteColor.CGColor;
            }
            
            // 添加星星形状图层到视图
            [starView.layer addSublayer:starMask];
            [self addSubview:starView];
        }
    }
    
    使用方式
    - (void)createStarView
    {
        int starRadius = 30;// 外接圆半径
        int defaultNumber = 5;// 默认画五个星
        
        self.star = [[DrawFunctionView alloc] initWithFrame:CGRectZero];
        self.starViewHeight = starRadius+starRadius * cos(M_PI/10);// 星星高度
        self.starViewWidth = 2 * defaultNumber*starRadius * cos(M_PI/10);// 星星宽度
        self.star.starColor = [UIColor redColor];// 描边颜色
        self.star.starWithColorNumber = 2;// 有几颗星星需要填充颜色
        self.star.starRadius = starRadius;// 星星半径
        self.star.defaultNumber = defaultNumber;// 默认画五个星
        
        self.star.backgroundColor = [UIColor whiteColor];
        [self.view addSubview:self.star];
        [self.star mas_makeConstraints:^(MASConstraintMaker *make) {
            make.height.width.equalTo(@300);
            make.center.equalTo(self.view);
        }];
    }
    

    8、drawRect: 绘制文字和图片

    绘制图片

    绘制文字和图片的时候,不用去获取图像上下文。

    • drawAtPoint:从指定的点为图片的左上角的起点开始绘制,绘制出来的图形跟图片尺寸一样大,图片是按照原始大小进行绘制,图片的大小超出当前view的视图范围,则无法进行绘制。
    • drawInRect:在指定的rect区域内绘制整张图片,图片会按照指定区域的宽高进行缩放,所以这种方式一定可以显示完整的图片,但是会进行一些缩放。
    • drawAsPatternInRect: 在指定的rect区域内平铺图片,如果一张图片不够用,则会在剩下的地方重新放置该图片,图片的大小尺寸不会改变。
    drawInRect
    - (void)drawRect:(CGRect)rect
    {
        [self drawImage:rect];
    }
    
    // 绘制图片
    - (void)drawImage:(CGRect)rect
    {
        // 1.用矩形去填充一个区域
        UIRectFill(rect);
        
        // 2.绘制一个矩形的边框
        UIRectFrame(rect);
        
        // 3.剪切图片,超出的图片位置都要剪切掉!必须要在绘制之前写,否则无效
        UIRectClip(CGRectMake(20, 20, 400, 400));
        
        // 4.创建图片
        UIImage *image = [UIImage imageNamed:@"luckcoffee.JPG"];
        // 在什么范围下
        [image drawInRect:rect];
        // 在哪个位置开始画
        //[image drawAtPoint:CGPointMake(10, 10)];
        // 平铺
        //[image drawAsPatternInRect:rect];
    }
    
    绘制富文本
     NSFontAttributeName                设置字体属性,默认值:字体:Helvetica(Neue) 字号:12
     NSForegroundColorAttributeNam      设置字体颜色,取值为 UIColor对象,默认值为黑色
     NSBackgroundColorAttributeName     设置字体所在区域背景颜色,取值为 UIColor对象,默认值为nil, 透明色
     NSLigatureAttributeName            设置连体属性,取值为NSNumber 对象(整数),0 表示没有连体字符,1 表示使用默认的连体字符
     NSKernAttributeName                设定字符间距,取值为 NSNumber 对象(整数),正值间距加宽,负值间距变窄
     NSStrikethroughStyleAttributeName  设置删除线,取值为 NSNumber 对象(整数)
     NSStrikethroughColorAttributeName  设置删除线颜色,取值为 UIColor 对象,默认值为黑色
     NSUnderlineStyleAttributeName      设置下划线,取值为 NSNumber 对象(整数),枚举常量NSUnderlineStyle中的值,与删除线类似
     NSUnderlineColorAttributeName      设置下划线颜色,取值为 UIColor 对象,默认值为黑色
     NSStrokeWidthAttributeName         设置笔画宽度,取值为 NSNumber 对象(整数),负值填充效果,正值中空效果
     NSStrokeColorAttributeName         填充部分颜色,不是字体颜色,取值为 UIColor 对象
     NSShadowAttributeName              设置阴影属性,取值为 NSShadow 对象
     NSTextEffectAttributeName          设置文本特殊效果,取值为 NSString 对象,目前只有图版印刷效果可用:
     NSBaselineOffsetAttributeName      设置基线偏移值,取值为 NSNumber (float),正值上偏,负值下偏
     NSObliquenessAttributeName         设置字形倾斜度,取值为 NSNumber (float),正值右倾,负值左倾
     NSExpansionAttributeName           设置文本横向拉伸属性,取值为 NSNumber (float),正值横向拉伸文本,负值横向压缩文本
     NSWritingDirectionAttributeName    设置文字书写方向,从左向右书写或者从右向左书写
     NSVerticalGlyphFormAttributeName   设置文字排版方向,取值为 NSNumber 对象(整数),0 表示横排文本,1 表示竖排文本
     NSLinkAttributeName                设置链接属性,点击后调用浏览器打开指定URL地址
     NSAttachmentAttributeName          设置文本附件,取值为NSTextAttachment对象,常用于文字图片混排
     NSParagraphStyleAttributeName      设置文本段落排版格式,取值为 NSParagraphStyle 对象
    
    绘制文本
    - (void)drawRect:(CGRect)rect
    {
        [self drawText];
    }
    
    // 绘制文字
    - (void)drawText
    {
        NSString *str = @"遂以为九天之上,有诸般神灵,九幽之下,亦是阴魂归处,阎罗殿堂。";
        
        // 设置文字的属性
        NSMutableDictionary *paras = [NSMutableDictionary dictionary];
        paras[NSFontAttributeName] = [UIFont systemFontOfSize:40];// 字号
        paras[NSForegroundColorAttributeName] = [UIColor redColor];// 红色字体
        paras[NSStrokeColorAttributeName] = [UIColor blackColor];// 橘色描边
        paras[NSStrokeWidthAttributeName] = @3;// 橘色宽度
    
        // 创建阴影对象
        NSShadow *shodow = [[NSShadow alloc] init];
        shodow.shadowColor = [UIColor blueColor];// 阴影颜色
        shodow.shadowOffset = CGSizeMake(5, 6);// 阴影偏移
        shodow.shadowBlurRadius = 4;// 阴影模糊半径
        paras[NSShadowAttributeName]  = shodow;// 阴影属性
        
        // 富文本
        [str drawAtPoint:CGPointZero withAttributes:paras];
    }
    
    雪花飘动
    雪花飘动
    - (void)drawRect:(CGRect)rect
    {
        [self drawSnow:rect];
    }
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self)
        {
            [self createSnowTimer];
        }
        return self;
    }
    
    // 每秒计时器都会调用
    - (void)drawSnow:(CGRect)rect
    {
        // 设置下雪的动画
        UIImage *image = [UIImage imageNamed:@"雪花.jpg"];
        
        // 全局变量,每次+10
        _snowY += 10;
        
        // 定点绘图
        [image drawAtPoint:CGPointMake(150, _snowY)];
    
        // 置0回到开头
        if (_snowY >= rect.size.height)
        {
            _snowY = 0;
        }
    }
    
    // 如果在绘图的时候需要用到定时器,通常使用CADisplayLink
    // NSTimer很少用于绘图,因为调度优先级比较低,并不会准时调用
    - (void)createSnowTimer
    {
        // 创建定时器
        CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(timeChange)];
        
        // 添加到主运行循环
        [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    }
    
    // CADisplayLink:每次屏幕刷新的时候就会调用,屏幕一般一秒刷新60次
    - (void)timeChange
    {
        [self setNeedsDisplay];
    }
    
    编辑图片

    实现图片的水印、裁剪、截屏、压缩等效果,这里以压缩图片为例,其余步骤类似。


    压缩图片
    // 压缩图片
    - (UIImage*)imageWithImageSimple:(UIImage*)image scaledToSize:(CGSize)newSize
    {
        // 开启一个图片上下文
        UIGraphicsBeginImageContext(newSize);
    
        // 这里就是不同的部分:如压缩图片只需要将旧图片按照新尺寸绘制即可,无需获取当前的上下文
        // CGContextRef context = UIGraphicsGetCurrentContext();
        [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
    
        // 从上下文当中取出新图片
        UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
        
        // 关闭上下文
        UIGraphicsEndImageContext();
        
        // 返回新图片
        return newImage;
    }
    

    9、drawRect: 修改图形

    修改椭圆
    修改椭圆
    - (void)drawRect:(CGRect)rect
    {
        [self drawEllipse];
    }
    
    // 修改椭圆
    - (void)drawEllipse
    {
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        // 画一个椭圆
        CGPathRef path = CGPathCreateWithEllipseInRect(CGRectMake(0, 0, 200, 100),nil);
        [[UIColor redColor] set];// 红色
    
        // 偏移
        CGContextTranslateCTM(context, 150, 10);
        // 旋转
        CGContextRotateCTM(context, M_PI_4);
        // 缩放
        CGContextScaleCTM(context, 0.25, 2);
    
        // 绘图
        CGContextAddPath(context, path);
        CGContextFillPath(context);
    }
    

    10、CAShapeLayer

    介绍
    • CAShapeLayer继承自CALayer,可使用CALayer的所有属性。
    • CAShapeLayer需要和贝塞尔曲线配合使用才有意义。贝塞尔曲线可以为其提供形状。CAShapeLayer 有一个神奇的属性path用这个属性配合上 UIBezierPath 这个类就可以达到超神的效果。
    • 使用CAShapeLayer与贝塞尔曲线可以实现不在viewDrawRect方法中画出一些想要的图形。
    // 圆点
    - (void)dot
    {
        CAShapeLayer *maskLayer = [CAShapeLayer layer];
        maskLayer.frame = CGRectMake(30, 30, 10, 10);
        maskLayer.cornerRadius = 5;
        maskLayer.backgroundColor = [UIColor redColor].CGColor;
        [self.view.layer addSublayer:maskLayer];
    }
    

    运行效果一片空白,说明单独使用CAShapeLayer无效。

    单独使用CAShapeLayer无效
    关于CAShapeLayer和DrawRect的比较
    • DrawRect:属于CoreGraphic框架,占用CPU,消耗性能大
    • CAShapeLayer:属于CoreAnimation框架,通过GPU来渲染图形,节省性能。动画渲染直接提交给手机GPU,不消耗内存。
    属性

    strokeStart strokeEnd:两者的取值都是0~1,决定贝塞尔曲线的划线百分比,对应值的改变支持隐式动画。
    lineCap:线端点类型,也就是对应曲线结束的点的显示样式。
    lineJoin:连接点类型,也就是对应曲线节点的位置的显示样式。
    lineDashPattern:数组中奇数位实线长度,偶数位虚线长度(注意:这里的奇数,偶数以数组的第一个元素索引为1计算)。
    lineDashPhase:虚线开始的位置,可以使用此属性做一个滚动的虚线框。

    path:贝塞尔曲线和图层结合才能绘制出各种各样的图形。当与贝塞尔曲线一起使用的时候,生成的曲线的位置是相对于生成的layer的,所以当你利用贝塞尔曲线设置了path后,再设置layerpositionbounds你会感觉很奇怪,最简单的方式就是单纯利用贝塞尔曲线决定图层的位置和大小。

    矩形
    // 矩形
    - (void)rectangle
    {
        // X、Y坐标和宽高均起作用了
        UIBezierPath *rectPath = [UIBezierPath bezierPathWithRect:CGRectMake(90, 60, 150, 150)];
        
        CAShapeLayer *maskLayer = [CAShapeLayer layer];
        maskLayer.path = rectPath.CGPath;
        
        maskLayer.lineWidth = 10.0;
        maskLayer.fillColor = [UIColor clearColor].CGColor;
        maskLayer.strokeColor = [UIColor colorWithRed:88/255.0 green:185/255.0 blue:157/255.0 alpha:1].CGColor;
        
        // X、Y坐标起作用了,但是宽度和高度设置何值好像都影响
        maskLayer.frame = CGRectMake(90, 60, 500, 500);
        
        [self.view.layer addSublayer:maskLayer];
    }
    

    运行效果如下:

    矩形
    曲线
    普通曲线
    - (void)commonCurve
    {
        // 创建 path
        UIBezierPath *path = [[UIBezierPath alloc] init];
        CGPoint startPoint = CGPointMake(50, 200);
        CGPoint endPoint = CGPointMake(200, 200);
        CGPoint controlPoint1 = CGPointMake(100, 150);
        CGPoint controlPoint2 = CGPointMake(150, 300);
        [path moveToPoint:startPoint];
        [path addCurveToPoint:endPoint controlPoint1:controlPoint1 controlPoint2:controlPoint2];
    
        // 创建 shapeLayer
        CAShapeLayer *shapeLayer = [[CAShapeLayer alloc]init];
        [self.view.layer addSublayer:shapeLayer];
        shapeLayer.path = path.CGPath;
    
        shapeLayer.fillColor = [UIColor clearColor].CGColor;
        shapeLayer.strokeColor = [UIColor blackColor].CGColor;
        shapeLayer.lineWidth = 5;
    }
    
    直线
    普通直线
    - (void)ordinaryStraightLine
    {
        UIBezierPath *linePathTop = [UIBezierPath bezierPath];
        [linePathTop  moveToPoint:CGPointMake(0, 130)];
        [linePathTop  addLineToPoint:CGPointMake(400, 130)];
    
        CAShapeLayer *maskLayerTop = [CAShapeLayer layer];
        maskLayerTop.path = linePathTop.CGPath;
        
        maskLayerTop.strokeColor = [UIColor grayColor].CGColor;
        maskLayerTop.lineWidth = 10.0;
        // X、Y坐标起作用了,但是宽度和高度设置何值好像都影响
        maskLayerTop.frame = CGRectMake(0, 125, 500, 10);
    
        [self.view.layer addSublayer:maskLayerTop];
    }
    
    三角形、矩形、圆形
    几何图形
    @implementation ShapeLayerView
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self)
        {
            self.backgroundColor = [UIColor clearColor];
            
            // 创建圆的路径
            UIBezierPath *cyclePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(30, 30) radius:20 startAngle:0 endAngle:M_PI*2 clockwise:YES];
            [cyclePath closePath];// 闭合路径,这里可有可无,圆已经连接完了
            
            // 创建形状图层
            CAShapeLayer *cycleLayer = [CAShapeLayer layer];
            cycleLayer.path = cyclePath.CGPath;// 路径为圆
            cycleLayer.strokeColor = [UIColor redColor].CGColor;// 红色描边
            cycleLayer.lineWidth = 10.0;// 线宽
            cycleLayer.fillColor = [UIColor clearColor].CGColor;// 空白填充
            cycleLayer.frame = CGRectMake(0, 0, 60, 60);// 相对于view
            // 添加到view图层中
            [self.layer addSublayer:cycleLayer];
            
            // 三角形的三个点
            CGPoint triangleTop = CGPointMake(30, 10);
            CGPoint triangleLeft = CGPointMake(10, 50);
            CGPoint triangleRight = CGPointMake(50, 50);
            
            // 创建三角形路径
            UIBezierPath *trianglePath = [UIBezierPath bezierPath];
            [trianglePath moveToPoint:triangleTop];// 起始点
            [trianglePath addLineToPoint:triangleLeft];// 连线
            [trianglePath addLineToPoint:triangleRight];// 连线
            [trianglePath closePath];// 这里必须闭合,否则空了一边没有连接
            
            // 创建形状图层
            CAShapeLayer *triangleLayer = [CAShapeLayer layer];
            triangleLayer.path = trianglePath.CGPath;// 路径为三角形
            triangleLayer.strokeColor = [UIColor blackColor].CGColor;// 黑色描边
            triangleLayer.lineWidth = 1.0;// 线宽
            triangleLayer.fillColor = [UIColor redColor].CGColor;
            triangleLayer.frame = CGRectMake(60, 0, 60, 60);
            [self.layer addSublayer:triangleLayer];
            
            // 矩形路径
            UIBezierPath *rectanglePath = [UIBezierPath bezierPath];
            [rectanglePath moveToPoint:CGPointMake(10, 10)];// 起始点
            [rectanglePath addLineToPoint:CGPointMake(10, 50)];// 连线
            [rectanglePath addLineToPoint:CGPointMake(50, 50)];// 连线
            [rectanglePath addLineToPoint:CGPointMake(50, 10)];// 连线
            [rectanglePath closePath];// 这里必须闭合,否则空了一边没有连接
            
            // 创建形状图层
            CAShapeLayer *rectangleLayer = [CAShapeLayer layer];
            rectangleLayer.path = rectanglePath.CGPath;// 路径为矩形
            rectangleLayer.strokeColor = [UIColor redColor].CGColor;// 红色描边
            rectangleLayer.lineWidth = 6.0;// 线宽
            rectangleLayer.fillColor = [UIColor yellowColor].CGColor;// 黄色填充
            rectangleLayer.frame = CGRectMake(120, 0, 60, 60);
            [self.layer addSublayer:rectangleLayer];
        }
        return self;
    }
    
    @end
    
    圆环
    image.png
    - (void)createRing
    {
        self.backgroundColor = [UIColor clearColor];
    
        // 取最小一边的一半作为半径
        CGSize size = self.bounds.size;
        CGFloat radius = 0;
        if (size.width >= size.height)
        {
            radius = size.height / 2;
        }
        else
        {
            radius = size.width / 2;
        }
        
        // 圆点
        CGPoint center = CGPointMake(size.width/2, size.height/2);
    
    // 上半圆环
        // 创建顶部路径
        UIBezierPath *topMaskPath = [UIBezierPath bezierPath];
        [topMaskPath moveToPoint:CGPointMake(0, center.y)];// 起始点
        [topMaskPath addArcWithCenter:center radius:radius startAngle:M_PI endAngle:M_PI*2 clockwise:YES];// 半圆
        [topMaskPath addLineToPoint:CGPointMake(center.x + radius - 20, center.y)];// 添加外环和内环的连接线
        [topMaskPath addArcWithCenter:center radius:radius-20 startAngle:M_PI*2 endAngle:M_PI clockwise:NO];// 半圆
        [topMaskPath closePath];// 闭合
        
        // 创建顶部形状图层作为mask
        CAShapeLayer *topMaskLayer = [CAShapeLayer layer];
        topMaskLayer.path = topMaskPath.CGPath;// 路径
    
        
        // 创建顶部图层
        CALayer *topLayer = [CALayer layer];
        topLayer.backgroundColor = [UIColor redColor].CGColor;// 红色背景
        topLayer.mask = topMaskLayer;// 形状图层作为mask
        topLayer.frame = self.bounds;
        [self.layer addSublayer:topLayer];
    
    // 下半圆环
        // 创建底部路径
        UIBezierPath *bottomMaskPath = [UIBezierPath bezierPath];
        [bottomMaskPath moveToPoint:CGPointMake(0, center.y)];// 起始点
        [bottomMaskPath addArcWithCenter:center radius:radius startAngle:M_PI endAngle:0 clockwise:NO];// 半圆
        [bottomMaskPath addLineToPoint:CGPointMake(center.x+radius-20, center.y)];// 添加外环和内环的连接线
        [bottomMaskPath addArcWithCenter:center radius:radius-20 startAngle:0 endAngle:M_PI clockwise:YES];// 半圆
        [bottomMaskPath closePath];// 闭合
        
        // 创建底部形状图层作为mask
        CAShapeLayer *bottomMaskLayer = [CAShapeLayer layer];
        bottomMaskLayer.path = bottomMaskPath.CGPath;// 路径
        
        // 创建底部图层
        CALayer *bottomLayer = [CALayer layer];
        bottomLayer.backgroundColor = [UIColor redColor].CGColor;// 红色背景
        bottomLayer.mask = bottomMaskLayer;// 形状图层作为mask
        bottomLayer.frame = self.bounds;
        [self.layer addSublayer:bottomLayer];
    }
    
    单选按钮
    2020-09-28 18:18:36.487181+0800 AnimationDemo[34102:19745405] 改变了颜色
    
    单选按钮
    - (void)radioButton
    {
        // ☑️ 以maskLayer.frame作为自己的bounds来源
        UIBezierPath *checkPath = [UIBezierPath bezierPath];
        [checkPath moveToPoint:CGPointMake(7, 19)];
        [checkPath addLineToPoint:CGPointMake(15, 25)];
        [checkPath addLineToPoint:CGPointMake(25, 9)];
        
        // 圆
        CAShapeLayer *maskLayer = [CAShapeLayer layer];
        maskLayer.path = checkPath.CGPath;
        maskLayer.strokeColor = [UIColor whiteColor].CGColor;
        maskLayer.lineWidth = 3.0;
        maskLayer.fillColor = [UIColor clearColor].CGColor;
        
        // 以checkButton.frame作为自己的bound来源
        // 宽、高对checkPath没影响,不影响按钮响应区域,但是改变了背景
        // 将cornerRadius设置为15,宽、高设置为30则产生了单选按钮效果
        maskLayer.frame = CGRectMake(0, 0, 300, 300);
        maskLayer.cornerRadius = 15;
        maskLayer.backgroundColor = [UIColor grayColor].CGColor;
        
        // 按钮
        UIButton *checkButton = [UIButton buttonWithType:UIButtonTypeSystem];
        checkButton.frame = CGRectMake(50, 435, 80, 85);
        checkButton.backgroundColor = [UIColor redColor];
        [checkButton.layer addSublayer:maskLayer];
        [checkButton addTarget:self action:@selector(changeColor) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:checkButton];
    }
    
    - (void)changeColor
    {
        NSLog(@"改变了颜色");
    }
    
    虚线
    虚线
    // 通过CAShapeLayer方式绘制虚线
    - (void)drawDashLine:(UIView *)lineView lineLength:(int)lineLength lineSpacing:(int)lineSpacing lineColor:(UIColor *)lineColor
    {
        CAShapeLayer *shapeLayer = [CAShapeLayer layer];
        [shapeLayer setBounds:lineView.bounds];// 大小
        [shapeLayer setPosition:CGPointMake(CGRectGetWidth(lineView.frame) / 2, CGRectGetHeight(lineView.frame))];// 位置
        [shapeLayer setFillColor:[UIColor clearColor].CGColor];// 填充颜色
        
        // 设置虚线颜色
        [shapeLayer setStrokeColor:lineColor.CGColor];
        // 设置虚线宽度(其实是高度)
        [shapeLayer setLineWidth:CGRectGetHeight(lineView.frame)];
        [shapeLayer setLineJoin:kCALineJoinRound];
        // 设置线宽,线间距
        [shapeLayer setLineDashPattern:[NSArray arrayWithObjects:[NSNumber numberWithInt:lineLength], [NSNumber numberWithInt:lineSpacing], nil]];
        
        // 设置路径
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathMoveToPoint(path, NULL, 0, 0);// 起点
        CGPathAddLineToPoint(path, NULL,CGRectGetWidth(lineView.frame), 0);// 终点
        [shapeLayer setPath:path];
        CGPathRelease(path);
        
        // 把绘制好的虚线添加上来
        [lineView.layer addSublayer:shapeLayer];
    }
    

    11、CAGradientLayer

    a、介绍

    含义:在其背景色上绘制颜色渐变,CAGradientLayer时和CAShapeLayer配合使用的,根据shape形状来绘制渐变色。

    CAGradientLayer的坐标系统:

    • CAGradientLayer的坐标系统是从(0,0)(1,1)绘制的矩形
    • CAGradientLayerframe值的size不为正方形的话,坐标系统会被拉伸
    • CAGradientLayerstartPointendPoint会直接决定颜色的绘制方向
    • CAGradientLayer的颜色分割点时以0到1的比例来计算的
    b、属性

    CALayer的所有属性:因为CAGradientLayer继承自CALayer,所以CAGradientLayer可以使用其父类CALayer所有开放的属性。
    colors:设置渐变的颜色,这是个数组,可以添加多个颜色,颜色属于CGColorRef类,所以必须加CGColor,而且前面还要加(id)

    gradient.colors = @[(id)[UIColor blackColor].CGColor,(id)[UIColor whiteColor].CGColor];
    

    locations:渐变颜色的区间分布,locations的数组长度和color一致也可以不一致,这个值一般不用管它,默认是nil,会平均分布。
    startPoint和endPoint:startPointendPoint分别为渐变的起始方向与结束方向,它是以矩形的四个角为基础的,(0,0)为左上角、(1,0)为右上角、(0,1)为左下角、(1,1)为右下角,默认是值是(0.5,0)(0.5,1)

    //渐变从左下角开始
    gradient.startPoint = CGPointMake(0, 1);
    //渐变到右上角结束
    gradient.endPoint = CGPointMake(1, 0);
    

    type:默认值是kCAGradientLayerAxial,表示按像素均匀变化。除了默认值也无其它选项。

    c、演示
    渐变
    @implementation GradientLayerView
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self)
        {
            [self createSubViews];
        }
        return self;
    }
    
    // 添加子视图
    - (void)createSubViews
    {
        // 从左到右红蓝渐变
        CAGradientLayer *leftToRightGradientLayer = [CAGradientLayer layer];
        leftToRightGradientLayer.colors = @[(id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor];// 红蓝
        leftToRightGradientLayer.startPoint = CGPointMake(0, 0.5);// 开始位置
        leftToRightGradientLayer.endPoint = CGPointMake(1, 0.5);// 结束位置
        leftToRightGradientLayer.frame = CGRectMake(0, 0, 100, 100);
        [self.layer addSublayer:leftToRightGradientLayer];
        
        // 多色对角线渐变
        CAGradientLayer *multiGradientLayer = [CAGradientLayer layer];
        multiGradientLayer.colors = @[(id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor, (id)[UIColor yellowColor].CGColor];// 红蓝黄
        multiGradientLayer.locations = @[@0, @0.5, @1];// 控制渐变发生的位置
        multiGradientLayer.startPoint = CGPointMake(0, 0);// 开始点
        multiGradientLayer.endPoint = CGPointMake(1, 1);// 结束点
        multiGradientLayer.frame = CGRectMake(110, 0, 100, 100);
        [self.layer addSublayer:multiGradientLayer];
        
        // 以圆心做渐变
        CAGradientLayer *radialGradientLayer = [CAGradientLayer layer];
        radialGradientLayer.colors = @[(id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor];// 红蓝
        radialGradientLayer.type = kCAGradientLayerRadial;// 以圆心做渐变
        radialGradientLayer.frame = CGRectMake(220, 0, 100, 100);
        radialGradientLayer.startPoint = CGPointMake(.5, .5);
        radialGradientLayer.endPoint = CGPointMake(.0, 1);
        [self.layer addSublayer:radialGradientLayer];
    }
    
    @end
    

    Demo

    Demo在我的Github上,欢迎下载。
    DrawDemo

    参考文献

    Quartz2D简介
    quartz2D 的从零到一学习使用
    iOS之CAGradientLayer渐变色
    setNeedsLayout与layoutIfNeeded的区别
    【iOS】Bitmap 位图详解与实践

    相关文章

      网友评论

        本文标题:IOS基础:绘图

        本文链接:https://www.haomeiwen.com/subject/noirmktx.html