AFImageHelper具体实现了什么?

晚上被AFImageHelper这个开源库给深深吸引了,早在去年自己刚学习iOS开发时候就做过图片的模糊模板处理,当时用的是UIVisualEffectView处理的,效果很不好。但是今天看到这个库,只想惊叹一声,牛逼了我的哥,基本上把和UIImage相关的扩展都添加了,仔细看了一下代码,不是很难懂,代码量也不是很多,所以阅读起来一定没啥问题,所以我就简单地阅读了一下,主要是想借鉴下别人的实现思路,来补充自己的知识吧。

从 Image from a URL 说起

// Fetches an image from a URL. If caching is set, it will be cached by NSCache for future queries. The cached image is returned if available, otherise the placeholder is set. When the image is returned, the closure gets called.
func imageFromURL(url: String, placeholder: UIImage, fadeIn: Bool = true, closure: ((image: UIImage?)
// 参数url: 表示图片url地址
// 参数placeholder: 占位图
// 参数fadeIn: 表示是否以fade形式展示,具体实现用了 CATransition
// 参数clourse: 网络端获取的图片

该方法的具体实现。该方法是对UIImageView的扩展,该方法主要实现了在有placeholder存在且web image还未加载完成的之前(网络加载需要时间),先显示placeholder图片,点进去会发现它实际上调用了UIImage下面的这个方法:

class func image(fromURL url: String, placeholder: UIImage, shouldCacheImage: Bool = true, closure: @escaping (_ image: UIImage?) -> ()) -> UIImage?

该类方法是UIImage的一个扩展,目的是从网络端获取一张图片,并提供了placeholdercache 选项,获取的图片放在了 closure 里面,返回的图片放在return里面,具体的实现看下面:

 class func image(fromURL url: String, placeholder: UIImage, shouldCacheImage: Bool = true, closure: @escaping (_ image: UIImage?) -> ()) -> UIImage? {
        // From Cache
        if shouldCacheImage {
            // 这里可能需要注意下UIImage.shared...
            // 很巧妙的单例设计,不过也有问题
            if let image = UIImage.shared.object(forKey: url as AnyObject) as? UIImage {
                closure(nil)
                return image
            }
        }
        // Fetch Image
        let session = URLSession(configuration: URLSessionConfiguration.default)
        if let nsURL = URL(string: url) {
            session.dataTask(with: nsURL, completionHandler: { (data, response, error) -> Void in
                if (error != nil) {
                    DispatchQueue.main.async {
                        closure(nil)
                    }
                }
                if let data = data, let image = UIImage(data: data) {
                    if shouldCacheImage {
                        UIImage.shared.setObject(image, forKey: url as AnyObject)
                    }
                    DispatchQueue.main.async {
                        closure(image)
                    }
                }
                session.finishTasksAndInvalidate()
            }).resume()
        }
        return placeholder
    }

用了 SDWebImage 加载 placeHolder 图片的方式。将 placeholder 放在返回图片里,webImage(异步) 放在闭包里。通过 UIImageView 的扩展方法轻松实现 placeholderwebImage 的切换。
下面是那个巧妙的单例设计,说它巧妙是因为我可能第一次见这种写法,不过也有一定问题,谁能猜到这个 UIImage.shared 实际上是一个 NSCache 实例呢???不知道为什么这么写。至少我觉得用 sharedCache 也比这个好点吧。

 /**
A singleton shared NSURL cache used for images from URL
*/
extension UIImage {
    static var shared: NSCache<AnyObject, AnyObject>! {
        struct StaticSharedCache {
            static var shared: NSCache<AnyObject, AnyObject>? = NSCache()
        }   
        return StaticSharedCache.shared!
    }
}

如何实现 UIImage With Colors

创建只带一种颜色的 image (a solid color)
  1. 创建一个特定 sizeImageContext
  2. 获取该context
  3. 给该context指定fillColor
  4. 获取该image
  5. 关闭该context
 convenience init?(color: UIColor, size: CGSize = CGSize(width: 10, height: 10)) {
        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
        
        let context = UIGraphicsGetCurrentContext()
        context?.setFillColor(color.cgColor)
        context?.fill(rect)
        
        self.init(cgImage:(UIGraphicsGetImageFromCurrentImageContext()?.cgImage!)!)
        UIGraphicsEndImageContext()
    }

不过我觉得这个另一种简易可行的方式就是直接设置 UIViewbackgroundColor 就OK吧。

创建一种带梯度的颜色背景图片 (a gradient color)

设置方式代码和上面设置 a solid color 一样,只不过多了一条context.drawLinearGradient 设置。

 convenience init?(gradientColors:[UIColor], size:CGSize = CGSize(width: 10, height: 10), locations: [Float] = [] )
    {
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        let context = UIGraphicsGetCurrentContext()
        
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let colors = gradientColors.map {(color: UIColor) -> AnyObject! in return color.cgColor as AnyObject! } as NSArray
        let gradient: CGGradient
        if locations.count > 0 {
          // 在这里使用map做了转换,将 Float 转换成 CGFloat
          let cgLocations = locations.map { CGFloat($0) }
          gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: cgLocations)!
        } else {
          gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: nil)!
        }
        context!.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: size.height), options: CGGradientDrawingOptions(rawValue: 0))
        self.init(cgImage:(UIGraphicsGetImageFromCurrentImageContext()?.cgImage!)!)
        UIGraphicsEndImageContext()
    }
给已有图片添加一层梯度模板 (Applies gradient color overlay to an image.)

这个和上面的a gradient color实现方式很像,上面那条只是创建一个梯度模板的图片,而这里给已有图片添加梯度模板,所以这里相比于上面那个实现就多了将context绘制在image上步骤,当然别忘了clip操作,不然获取的图片大小size可能不和原来一样。该段代码的具体实现在这里,为了节省篇幅就不将代码贴出来了。
不过我还是觉得原作者这么写是不严密的,原因如下:
self.cgImage可能返回NULL的情况,具体情况请看官方文档 If the UIImage object was initialized using a CIImage object, the value of the property is NULL., 这个时候就没必要进行下面的操作了。

创建一个带Label的图片(Creates a text label image.)

这个实现还是比较好理解的,步骤如下:具体实现在这里

  1. 创建一个UILabel
  2. 将UILabel转换成UIImage
  3. 将UIImage以convenience形式初始化出来

可能难点在于步骤2, 具体实现如下:

    convenience init?(fromView view: UIView) {
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, 0)
        view.layer.render(in: UIGraphicsGetCurrentContext()!) // 将view的layer绘制在imageContext上
        self.init(cgImage:(UIGraphicsGetImageFromCurrentImageContext()?.cgImage!)!)
        UIGraphicsEndImageContext()
    }
创建放射状的 UIImage (Image with Radial Gradient)

一开始我以为实现这个是很难的,但是在看完该代码之后,瞬间觉得很简单,直接使用UIGraphicsGetCurrentContext()?.drawRadialGradient(...)方法完美解决。该方法参数比较多,不过都是那种我们一下子就能看懂意思的,在这里就不细说了,具体实现在这里,实现步骤如下:

  1. 初始化drawRadialGradient方法需要的参数,
    这里需要注意创建 CGGradient 的时候使用 public init?(colorSpace space: CGColorSpace, colorComponents components: UnsafePointer<CGFloat>, locations: UnsafePointer<CGFloat>?, count: Int) 这个初始化方法。
  2. 实行绘制操作
检测图片是否包含Alpha

实现如下:

 var hasAlpha: Bool {
    let alpha: CGImageAlphaInfo = self.cgImage!.alphaInfo
    switch alpha {
    case .first, .last, .premultipliedFirst,    .premultipliedLast:
        return true
    default:
        return false
    }
}    

至于CGImageAlphaInfo,贴张图:根据这个很容易就知道为什么作者在这里使用了
case .first, .last, .premultipliedFirst

关于图片剪裁(image crop)

func crop(bounds: CGRect) -> UIImage?

直接使用了系统CGImage提供的方法cropping:, 指定其bounds,scale 和 orientation 参数即可。
具体代码如下:

func crop(bounds: CGRect) -> UIImage? {
    return UIImage(cgImage: (self.cgImage?.cropping(to: bounds)!)!, scale: 0.0, orientation: self.imageOrientation)
}
func cropToSquare() -> UIImage?

该方法从名字就可以看出来, 这是为了获取到Square形状的剪裁图片,但是假如一张图片原图就不是Square的,那么获取的时候就需要剪裁了,作者在这里只是选择了长和宽中短的那个作为最后的边长,而确定了边长,位置也取了中间位置。
具体代码如下:

func cropToSquare() -> UIImage? {
    let size = CGSize(width: self.size.width * self.scale, height: self.size.height * self.scale)
    let shortest = min(size.width, size.height)
        
    let left: CGFloat = (size.width > shortest) ? (size.width - shortest) / 2 : 0
    let top: CGFloat = (size.height > shortest) ? (size.height - shortest) / 2 : 0
        
    let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
    let insetRect = rect.insetBy(dx: left, dy: top)
        
    return crop(bounds: insetRect)
}

关于图片Resize

顾名思义,resize image表示改变image的size,也就是长和宽。要是不看源码,让我去猜测怎么实现的话,我一定会猜到Quartz 2D绘制。没错,作者在这里也就是使用了这个。具体步骤如下:

  1. 根据你需要的contentMode计算出需要改变的比例
  2. 创建CGContext, 包含了一些配置相关的,具体可以去看Apple官方的描述
  3. 将该图片绘制在context上,具体使用了func draw(_ image: CGImage, in rect: CGRect, byTiling: Bool = default)方法。rect可以来控制size大小

相关代码实现如下:

func resize(toSize: CGSize, contentMode: UIImageContentMode = .scaleToFill) -> UIImage? {
    let horizontalRatio = size.width / self.size.width;
    let verticalRatio = size.height / self.size.height;
    var ratio: CGFloat!
        
    switch contentMode {
        case .scaleToFill:
            ratio = 1
        case .scaleAspectFill:
            ratio = max(horizontalRatio, verticalRatio)
        case .scaleAspectFit:
            ratio = min(horizontalRatio, verticalRatio)
    }
        
    let rect = CGRect(x: 0, y: 0, width: size.width * ratio, height: size.height * ratio)
        
    // Fix for a colorspace / transparency issue that affects some types of
    // images. See here: http://vocaro.com/trevor/blog/2009/10/12/resize-a-uiimage-the-right-way/comment-page-2/#comment-39951
        
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let context = CGContext(data: nil, width: Int(rect.size.width), height: Int(rect.size.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
        
    let transform = CGAffineTransform.identity
        
    // Rotate and/or flip the image if required by its orientation
        context?.concatenate(transform);
    // Set the quality level to use when rescaling
    context!.interpolationQuality = CGInterpolationQuality(rawValue: 3)!
        
    //CGContextSetInterpolationQuality(context, CGInterpolationQuality(kCGInterpolationHigh.value))
        
    // Draw into the context; this scales the image
    guard let image = cgImage else {
        return nil
    }
    context?.draw(image, in: rect)
        
        // Get the resized image from the context and a UIImage
    if let _cgImage = context?.makeImage() {
        return UIImage(cgImage: _cgImage, scale: self.scale, orientation: self.imageOrientation)
    }
    return nil
}

给图片(非view)添加Corner Radius

我之前还真没想到过给image添加Corner Radius,每回都是直接给view添加Corner Radius。不过我觉得肯定和Quartz 2D绘制有关。作者在这里提供了四种不同情况,在这里只解释第一种,其他三种也是基于这一种的,直接看步骤吧。

  1. 准备bitmap context相关的参数
  2. 创建bitmap context, 在这里创建的context需要用到bitmapInfo参数,具体bitmapInfo是干什么用的呢?或者还有bitmap图片是什么呢?如果明白了这些就知道了作者为啥在这个方法之前先调用了applyAlpha()方法。
  3. 创建相关绘制路径,然后获取图片并返回

CGBitmapInfo:Component information for a bitmap image.
BitmapImage:


具体代码如下:(实现起来也不是很复杂,如果自己真的写不出来,也是一种学习的思路)

func roundCorners(cornerRadius: CGFloat) -> UIImage? {
        // If the image does not have an alpha layer, add one
        guard let imageWithAlpha = applyAlpha() else {
            return nil
        }
        guard let _cgImage = imageWithAlpha.cgImage else {
            return nil
        }
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        let width = _cgImage.width
        let height = _cgImage.height
        let bits = _cgImage.bitsPerComponent
        guard let colorSpace = _cgImage.colorSpace else {
            return nil
        }
        let bitmapInfo = _cgImage.bitmapInfo

        let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bits, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)
        let rect = CGRect(x: 0, y: 0, width: CGFloat(width)*scale, height: CGFloat(height)*scale)
        
        context?.beginPath()
        if (cornerRadius == 0) {
            context?.addRect(rect)
        } else {
            context?.saveGState()
            context?.translateBy(x: rect.minX, y: rect.minY)
            context?.scaleBy(x: cornerRadius, y: cornerRadius)
            let fw = rect.size.width / cornerRadius
            let fh = rect.size.height / cornerRadius
            context?.move(to: CGPoint(x: fw, y: fh/2))
            context?.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw/2, y: fh), radius: 1)
            context?.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh/2), radius: 1)
            context?.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw/2, y: 0), radius: 1)
            context?.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh/2), radius: 1)
            context?.restoreGState()
        }
        context?.closePath()
        context?.clip()
        
        context?.draw(_cgImage, in: rect)
        if let image = context?.makeImage() {
            UIGraphicsEndImageContext()
            return UIImage(cgImage: image, scale:scale, orientation: .up)
        }
        UIGraphicsEndImageContext()
        return nil
    }

如何给图片添加Border

这个真的是很常用的,我们经常看到一些比如头像的话有一个边框,那么这个开源库提供的方法就可以轻易为我们的图片添加边框,而免去了让设计师作图了。
来看下具体实现吧:

  1. 准备参数,创建CGContext
  2. 设置border相关
  3. 将border应用到image上,主要是通过添加inset和strokeEllipse(in: inset)来做。
    具体代码如下:(代码比较长)
func apply(border: CGFloat, color: UIColor) -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(size, false, 0)
    guard let _cgImage = cgImage else {
        return nil
    }
    let width = _cgImage.width
    let height = _cgImage.height
    let bits = _cgImage.bitsPerComponent
    guard let colorSpace = _cgImage.colorSpace else {
        return nil
    }
    let bitmapInfo = _cgImage.bitmapInfo
    let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bits, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)
    var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0
    color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
        
    context?.setStrokeColor(red: red, green: green, blue: blue, alpha: alpha)
    context?.setLineWidth(border)
        
    let rect = CGRect(x: 0, y: 0, width: size.width*scale, height: size.height*scale)
    let inset = rect.insetBy(dx: border*scale, dy: border*scale)
        
    context?.strokeEllipse(in: inset)
    context?.draw(_cgImage, in: inset)
    if let image = context?.makeImage() {
        UIGraphicsEndImageContext()
        return UIImage(cgImage: image)
    }
    UIGraphicsEndImageContext()
    return nil
}

添加Image Effects效果

虽然iOS 7系统以后也提供了Effects效果,不过那个往往实现的和设计师的总是有点偏差。作者在这里提供了五种添加Image Effects方法,一种实现方法,四种不同默认实现方法。来看看他做了什么吧。

参考:https://github.com/melvitax/ImageHelper
原文地址:https://vsccw.com/2017/01/15/afimagehelper-01/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,104评论 5 13
  • 1、禁止手机睡眠[UIApplication sharedApplication].idleTimerDisabl...
    DingGa阅读 1,116评论 1 6
  • --绘图与滤镜全面解析 概述 在iOS中可以很容易的开发出绚丽的界面效果,一方面得益于成功系统的设计,另一方面得益...
    韩七夏阅读 2,717评论 2 10
  • QQ登录是一个很常见的功能, 也有很多第三方库帮我们实现了这样的功能, 今天提到的是一个UI上的问题. 问题重现 ...
    bigParis阅读 341评论 0 4
  • 越来越不喜欢吐露自己的想法,也不知道该怎么去说。不知道后面漫长的日子怎么过啊。为什么大家都觉得机会更多呢?是因为足...
    发呆的笑笑阅读 129评论 0 0