之所以要讨论 iOS 中的圆角,是因为圆角在各种 app 中大量被使用,而普通设置圆角的方式又会触发离屏渲染导致 FPS 的下降。
对于 UIImageView 来说,普遍的做法是将 image 通过 Core Graphics 画出一张带圆角的图片。像头像这样尺寸相对固定的图片来说这样做是很简单的,这里 详细的探讨了这件事,但是对于大图片的展示,要多考虑的一个问题是 contentMode。
有两种思路,第一种思路是剪裁图片,让这张图片刚好为相对应 contentMode 的 imageView 呈现的图片的大小。
计算的代码在这个 gist 中可以找到:
// port of http://stackoverflow.com/a/17948778/3071224
import UIKit
extension CGSize {
static func aspectFit(aspectRatio : CGSize, boundingSize: CGSize) -> CGSize {
var boundingSize = boundingSize
let mW = boundingSize.width / aspectRatio.width;
let mH = boundingSize.height / aspectRatio.height;
if( mH < mW ) {
boundingSize.width = boundingSize.height / aspectRatio.height * aspectRatio.width;
}
else if( mW < mH ) {
boundingSize.height = boundingSize.width / aspectRatio.width * aspectRatio.height;
}
return boundingSize;
}
static func aspectFill(aspectRatio :CGSize, minimumSize: CGSize) -> CGSize {
var minimumSize = minimumSize
let mW = minimumSize.width / aspectRatio.width;
let mH = minimumSize.height / aspectRatio.height;
if( mH > mW ) {
minimumSize.width = minimumSize.height / aspectRatio.height * aspectRatio.width;
}
else if( mW > mH ) {
minimumSize.height = minimumSize.width / aspectRatio.width * aspectRatio.height;
}
return minimumSize;
}
}
其实一般来说,我们只关心的是 aspectFill,下面就是绘制和剪裁的代码,需要注意的是要找准 image 在画布中绘制时的位置,是上下或左右居中的。
extension UIImage {
func clipTo(boundingSize: CGSize, radius: CGFloat) -> UIImage {
// the size when image in imageview with contentmode of .aspectFill
let fillSize = CGSize.aspectFill(aspectRatio: size, minimumSize: boundingSize)
let originX = (boundingSize.width - fillSize.width)/2
let originY = (boundingSize.height - fillSize.height)/2
UIGraphicsBeginImageContextWithOptions(boundingSize, false, 0)
let context = UIGraphicsGetCurrentContext()!
context.interpolationQuality = .low
// begin at (originX, originY), draw a fillSize image
draw(in: CGRect(x: originX, y: originY, width: fillSize.width, height: fillSize.height))
let output = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return output
}
}
另外将 interpolationQuality 设置为 low,可以得到和系统 imageView 一样的裁剪分辨率。
其实这种思路反而将问题复杂了,我们其实可以直接将已经设置好 image 的 imageView 画成一张图片,然后重新赋值,就像这样:
extension UIImageView {
func resizeImage(boundingSize: CGSize, cornerRadius: CGFloat = 0.0) {
if image != nil {
UIGraphicsBeginImageContextWithOptions(boundingSize, false, 0)
let context = UIGraphicsGetCurrentContext()!
context.interpolationQuality = .low
// clip the cornerRadius
let roundRect = CGRect(origin: .zero, size: boundingSize)
let roundPath = UIBezierPath(roundedRect: roundRect, cornerRadius: cornerRadius)
roundPath.addClip()
layer.render(in: context)
let output = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
image = output
}
}
}
这里我还为它添加了圆角,唯一的问题是需要在 image 设置好后在调用此方法,这个开源项目的 的思路差不多就是这样的,你可以在 这里 查看它的介绍文章。
还可以像这个 SO 答案提到的那样,建立一个专门用来绘制的 layer, 这样就不用考虑 image 是否设置了。
UIGraphicsBeginImageContextWithOptions(size, false, 0)
let context = UIGraphicsGetCurrentContext()!
context.interpolationQuality = .low
imageLayer.contentsGravity = "resizeAspectFill"
imageLayer.cornerRadius = 5
imageLayer.masksToBounds = true
imageLayer.frame.size = size
imageLayer.contents = image?.cgImage
imageLayer.render(in: context)
let output = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
在这篇文章中,作者提到在 iOS 9 以后设置 cornerRadius 和 maskToBounds 已经不会触发离屏渲染了,但是不知道依据在哪里。
经过我测试,当一个屏幕内的圆角数量不多的情况下,即使 4s 也可以滑动得很流畅,总之过早的优化是万恶之源,等到出现问题了再来看这篇文章也不迟。