前言
九宫格头像在网上一搜一大把,相关的优质博文也很多。在这里我将从另一个角度来分析和实现她。很多朋友对微信的群头像很感兴趣,那种九宫格头像乍一看还蛮高级的。但真要我们说出个具体实现方案,相信也没几个人能说的清楚。
我们先给几个方案:
- sever 端生成好九宫格头像,client 端直接通过生成好的图片 url 来显示
- 在一个 ImageView 上(View 上也可),放九个 Imageview,然后让他们分别加载图片。
- 将九张图片拼接生成一张图片,保存在本地然后使用她
看看具体效果
思路
根据上面的方案我们来详细讲解一下。
第一种,很明显和我们关系不大,只需要选择一款第三方图片加载库就能轻松搞定。
第二种,也是现在在网上大量文献使用的方法,相对好理解。在这里我也简单介绍一下。直接上代码,代码是我从网上找的(根据需要做了部分修改)。因为时间有点久,原地址在哪也不记得了。望原文作者见谅。
class NineGridImageView {
private var cellImageViewSideLength: CGFloat?
private var margin: CGFloat?
var delegate: NineGridImageViewDelegate?
// 生成九宫格图片到传入的 Imageview 中。
func generateNineGridImageViewTo(_ canvasView: UIImageView, _ urls: [String?]) {
var imageviews: [UIImageView] = []
// 根据传入的urls的个数,生成对应的 Imageview,并添加到 Imageview数组备用
for url in urls {
let imageview = UIImageView(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
// 这里是一个代理,用来在外部实现Imageview的加载
delegate?.onDisplayImage(imageview, url)
imageviews.append(imageview)
}
// 将加载的Imageview添加到原始Imageview上
stitchingOn(canvasView, withImageViews: &imageviews)
}
// 根据 Imageview 的个数来设置对应的 Imageview 在原始 Imageview 的位置和大小,并将子 Imageview添加到原始Imageview里
private func stitchingOn(_ canvasView: UIImageView, withImageViews imageviews: inout [UIImageView], marginValue: CGFloat? = nil) {
// 根据子Imageview的个数来确定子Imageview直接的间距
if marginValue == nil {
margin = canvasView.frame.size.width / 18.0
} else if imageviews.count == 4 {// 解决4张图遮挡头像的问题
margin = canvasView.frame.size.width / 15.0
} else {
margin = marginValue
}
imageViewSideLengthWith(canvasView.frame, imageviews.count)
if imageviews.count == 1 {
let imageView1 = imageviews[0]
let row_1_origin = (canvasView.frame.size.width - cellImageViewSideLength!) / 2
imageView1.frame = CGRect(x: row_1_origin, y: row_1_origin, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
} else if imageviews.count == 2 {
let row_1_origin_y = (canvasView.frame.size.width - cellImageViewSideLength!) / 2
imageviews = matrixFor(&imageviews, row_1_origin_y)
} else if imageviews.count == 3 {
let row_1_origin_y = (canvasView.frame.size.height - cellImageViewSideLength! * 2) / 3
let imageview1 = imageviews[0]
imageview1.frame = CGRect(x: (canvasView.frame.size.width - cellImageViewSideLength!)/2, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageviews = matrixFor(&imageviews, row_1_origin_y + cellImageViewSideLength! + margin!)
} else if imageviews.count == 4 {
let row_1_origin_y = (canvasView.frame.size.height - cellImageViewSideLength! * 2) / 3
imageviews = matrixFor(&imageviews, row_1_origin_y)
} else if imageviews.count == 5 {
let row_1_origin_y = (canvasView.frame.size.height - cellImageViewSideLength! * 2 - margin!) / 2
let imageview1 = imageviews[0]
imageview1.frame = CGRect(x: (canvasView.frame.size.width - 2 * cellImageViewSideLength! - margin!) / 2, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
let imageview2 = imageviews[1]
imageview2.frame = CGRect(x: imageview1.frame.origin.x + imageview1.frame.size.width + margin!, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageviews = matrixFor(&imageviews, row_1_origin_y + cellImageViewSideLength! + margin!)
} else if imageviews.count == 6 {
let row_1_origin_y = (canvasView.frame.size.height - cellImageViewSideLength! * 2 - margin!) / 2
imageviews = matrixFor(&imageviews, row_1_origin_y)
} else if imageviews.count == 7 {
let row_1_origin_y = (canvasView.frame.size.height - cellImageViewSideLength! * 3) / 4
let imageview1 = imageviews[0]
imageview1.frame = CGRect(x: (canvasView.frame.size.width - cellImageViewSideLength!) / 2, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageviews = matrixFor(&imageviews, row_1_origin_y + cellImageViewSideLength! + margin!)
} else if imageviews.count == 8 {
let row_1_origin_y = (canvasView.frame.size.height - cellImageViewSideLength! * 3) / 4
let imageview1 = imageviews[0]
imageview1.frame = CGRect(x: (canvasView.frame.size.width - 2 * cellImageViewSideLength! - margin!) / 2, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
let imageview2 = imageviews[1]
imageview2.frame = CGRect(x: imageview1.frame.origin.x + imageview1.frame.size.width + margin!, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageviews = matrixFor(&imageviews, row_1_origin_y + cellImageViewSideLength! + margin!)
} else if imageviews.count == 9 {
let row_1_origin_y = (canvasView.frame.size.height - cellImageViewSideLength! * 3) / 4
imageviews = matrixFor(&imageviews, row_1_origin_y)
}
for imageview in imageviews {
canvasView.addSubview(imageview)
}
}
// 计算子Imageview的边长
private func imageViewSideLengthWith(_ canvasViewFrame: CGRect, _ count: Int) {
var sideLength: CGFloat = 0.0
if count == 1 {
sideLength = (canvasViewFrame.size.width - margin! * 2) / 1.3
} else if count >= 2 && count <= 4 {
sideLength = (canvasViewFrame.size.width - margin! * 3) / 2
} else {
sideLength = (canvasViewFrame.size.width - margin! * 4) / 3
}
cellImageViewSideLength = sideLength
}
// 位置计算
private func matrixFor(_ imageviews: inout [UIImageView], _ originY: CGFloat) -> [UIImageView] {
let count = imageviews.count
var cellCount: Int
var maxRow: Int
var maxColumn : Int
var ignoreCountofBegining: Int
if count <= 4 {
maxRow = 2
maxColumn = 2
ignoreCountofBegining = count % 2
cellCount = 4
} else {
maxRow = 3
maxColumn = 3
ignoreCountofBegining = count % 3
cellCount = 9
}
for i in 0..<cellCount {
if i > imageviews.count - 1 { break }
if i < ignoreCountofBegining { continue }
let row: CGFloat = floor(CGFloat((i - ignoreCountofBegining) / maxRow))
let column: CGFloat = CGFloat((i - ignoreCountofBegining) % maxColumn)
let origin_x = margin! + cellImageViewSideLength! * column + margin! * column
let origin_y = originY + cellImageViewSideLength! * row + margin! * row
let imageview = imageviews[i]
imageview.frame = CGRect(x: origin_x, y: origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
}
return imageviews
}
}
当然还有最重要的上面提到的代理。
protocol NineGridImageViewDelegate {
/// 图片加载细节让外部使用者处理
func onDisplayImage(_ imageView: UIImageView, _ url: String?)
}
一般情况下,我们会直接为 Imageview 扩展这个协议
// 给 UIImageView 添加九宫格图片生成功能
extension UIImageView: NineGridImageViewDelegate {
public func generateNineGrid(urls: [String?]) {
self.image = nil
let instance = NineGridImageView()
instance.delegate = self
instance.generateNineGridImageViewTo(self, urls)
}
func onDisplayImage(_ imageView: UIImageView, _ url: String?) {
if url == nil {
imageView.image = UIImage(named: "user_icon")
} else {
imageView.setImageWithURL(url!, placeholderImageStr: "user_icon")
}
}
/// 设置Imageview的图片,可以看出我用了 Kingfisher
func setImageWithURL(_ url: String, placeholderImageStr: String? = nil) {
if let placeholderImageStr = placeholderImageStr {
// update swift3.0
self.kf.setImage(with: URL(string: url), placeholder: UIImage(named: placeholderImageStr), options: [.processor(RoundCornerImageProcessor(cornerRadius: 10))])
} else {
// update swift3.0
self.kf.setImage(with: URL(string: url), placeholder: nil, options: [.processor(RoundCornerImageProcessor(cornerRadius: 12))])
}
}
}
总结一下实现思路,大体是:给一个要展示九宫格头像的源 Imageview 传入需要展示的图片urls 数组。根据 urls 的个数生成子 Imageview 数组,并且将其添加到源 Imageview。核心逻辑在于子 Imageview 如何排列到源 Imageview 上。子 Imageview 的大小,位置计算应该是最难的地方,建议多了解一下。
这个方案有性能隐患,当需要大量使用九宫格头像的时候,可想而知满屏幕会有多少个 Imageview,刷新界面或重绘也会占用很多内存。
第三种,将第一种方案和第二种方案结合。也就是在本地将九宫格图片绘制出来,并保存在本地待下次使用。
同样的我们先看看代码。
// MARK: - 修改后的方法
// 拼接 image 数组
private func stitchingOn(_ canvasViewFrame: CGRect, withImages images: [UIImage], marginValue: CGFloat? = nil) -> UIImage? {
if marginValue == nil {
margin = canvasViewFrame.size.width / 18.0
} else if images.count == 4 {// 解决4张图遮挡头像的问题
margin = canvasViewFrame.size.width / 15.0
} else {
margin = marginValue
}
imageViewSideLengthWith(canvasViewFrame, images.count)
var imageRects: [(UIImage, CGRect)] = []
if images.count == 1 {
let image = images[0]
let row_1_origin = (canvasViewFrame.size.width - cellImageViewSideLength!) / 2
let rect = CGRect(x: row_1_origin, y: row_1_origin, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageRects.append((image, rect))
} else if images.count == 2 {
let row_1_origin_y = (canvasViewFrame.size.width - cellImageViewSideLength!) / 2
imageRects.append(contentsOf: matrixFor(images, row_1_origin_y))
} else if images.count == 3 {
let row_1_origin_y = (canvasViewFrame.size.height - cellImageViewSideLength! * 2) / 3
let image1 = images[0]
let rect = CGRect(x: (canvasViewFrame.size.width - cellImageViewSideLength!)/2, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageRects.append((image1, rect))
imageRects.append(contentsOf: matrixFor(images, row_1_origin_y + cellImageViewSideLength! + margin!))
} else if images.count == 4 {
let row_1_origin_y = (canvasViewFrame.size.height - cellImageViewSideLength! * 2) / 3
imageRects.append(contentsOf: matrixFor(images, row_1_origin_y))
} else if images.count == 5 {
let row_1_origin_y = (canvasViewFrame.size.height - cellImageViewSideLength! * 2 - margin!) / 2
let image1 = images[0]
let rect1 = CGRect(x: (canvasViewFrame.size.width - 2 * cellImageViewSideLength! - margin!) / 2, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageRects.append((image1, rect1))
let image2 = images[1]
let rect2 = CGRect(x: rect1.origin.x + rect1.size.width + margin!, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageRects.append((image2, rect2))
imageRects.append(contentsOf: matrixFor(images, row_1_origin_y + cellImageViewSideLength! + margin!))
} else if images.count == 6 {
let row_1_origin_y = (canvasViewFrame.size.height - cellImageViewSideLength! * 2 - margin!) / 2
imageRects.append(contentsOf: matrixFor(images, row_1_origin_y))
} else if images.count == 7 {
let row_1_origin_y = (canvasViewFrame.size.height - cellImageViewSideLength! * 3) / 4
let image1 = images[0]
let rect1 = CGRect(x: (canvasViewFrame.size.width - cellImageViewSideLength!) / 2, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageRects.append((image1, rect1))
imageRects.append(contentsOf: matrixFor(images, row_1_origin_y + cellImageViewSideLength! + margin!))
} else if images.count == 8 {
let row_1_origin_y = (canvasViewFrame.size.height - cellImageViewSideLength! * 3) / 4
let image1 = images[0]
let rect1 = CGRect(x: (canvasViewFrame.size.width - 2 * cellImageViewSideLength! - margin!) / 2, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageRects.append((image1, rect1))
let image2 = images[1]
let rect2 = CGRect(x: rect1.origin.x + rect1.size.width + margin!, y: row_1_origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
imageRects.append((image2, rect2))
imageRects.append(contentsOf: matrixFor(images, row_1_origin_y + cellImageViewSideLength! + margin!))
} else if images.count == 9 {
let row_1_origin_y = (canvasViewFrame.size.height - cellImageViewSideLength! * 3) / 4
imageRects.append(contentsOf: matrixFor(images, row_1_origin_y))
}
let resultImage = composeImages(canvasViewFrame, imageRects)
return resultImage
}
// 计算每个 image 绘制的位置,返回一个包含image和位置的元数组
private func matrixFor(_ images: [UIImage], _ originY: CGFloat) -> [(UIImage, CGRect)] {
let count = images.count
var cellCount: Int
var maxRow: Int
var maxColumn : Int
var ignoreCountofBegining: Int
if count <= 4 {
maxRow = 2
maxColumn = 2
ignoreCountofBegining = count % 2
cellCount = 4
} else {
maxRow = 3
maxColumn = 3
ignoreCountofBegining = count % 3
cellCount = 9
}
var result: [(UIImage,CGRect)] = []
for i in 0..<cellCount {
if i > images.count - 1 { break }
if i < ignoreCountofBegining { continue }
let row: CGFloat = floor(CGFloat((i - ignoreCountofBegining) / maxRow))
let column: CGFloat = CGFloat((i - ignoreCountofBegining) % maxColumn)
let origin_x = margin! + cellImageViewSideLength! * column + margin! * column
let origin_y = originY + cellImageViewSideLength! * row + margin! * row
let rect = CGRect(x: origin_x, y: origin_y, width: cellImageViewSideLength!, height: cellImageViewSideLength!)
result.append((images[i], rect))
}
return result
}
// 将需要拼接的 image 绘制到一起
private func composeImages(_ canvasViewFrame: CGRect, _ images: [(UIImage,CGRect)]) -> UIImage? {
let size = CGSize(width: canvasViewFrame.size.width, height: canvasViewFrame.size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
for (image, rect) in images {
image.draw(in: rect)
}
let result_image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result_image
}
上面代码的核心变化,在于计算的是image的位置。并且加入了新的方法,也就是 image 的绘制 composeImages() 方法。ImageHelper 是我封装的 Kingfisher 工具类,稍后也会贴出相关代码。
// 生成九宫格图片传入到 Imageview 中。
func generateNineGridImageViewTo(_ canvasView: UIImageView, _ urls: [String?], _ forkey: String? = nil) {
// forkey 是新生成的九宫格图片的唯一标识。
if ImageHelper.sharedInstance.isImageCached(key: forkey!) {
ImageHelper.sharedInstance.retrieveImage(forkey: forkey!, completionBlock: { (image) in
canvasView.image = image
})
} else {
delegate?.getDrawImages(urls, completionBlock: { (images) in
let image = self.stitchingOn(canvasView.frame, withImages: images)
if image != nil {
ImageHelper.sharedInstance.storeImage(image: image!, forkey: forkey!)
}
DispatchQueue.main.async {
canvasView.image = image
}
})
}
}
在 protocol NineGridImageViewDelegate 中加入 getDrawImages 方法。并在 extension UIImageView 的时候实现它,具体逻辑如下:
public func getDrawImages(_ urls: [String?], completionBlock: @escaping ([UIImage]) -> Void){
ImageHelper.sharedInstance.downLoadImage(urls: urls) {
ImageHelper.sharedInstance.retrieveImage(keys: urls, placeholderImageStr: "user_icon", completionBlock: completionBlock)
}
}
以上就是全部内容了。回看整个代码,你会发现第三种方案也有不少问题。比如何时更新九宫格图片,更新的时候一样会出现图片闪一下的问题。当然这些问题目前来看还是可以接受的。
彩蛋:ImageHelper,对 Kingfisher 的操作。
class ImageHelper {
class var sharedInstance: ImageHelper {
struct Static {
static let instance: ImageHelper = ImageHelper()
}
return Static.instance
}
/// 自定义KingfisherManager cache system,在这里我们暂时只使用 default 的 cache
fileprivate func initKingfisherManager() {
let cache = KingfisherManager.shared.cache
// Set max disk cache to 100 mb. Default is no limit.
cache.maxDiskCacheSize = UInt(100 * 1024 * 1024)
// Set max disk cache to duration to 30 days, Default is 1 week.
cache.maxCachePeriodInSecond = TimeInterval(60 * 60 * 24 * 30)
}
/// 计算图片使用磁盘大小
func getDiskCacheSize() -> UInt?{
let cache = KingfisherManager.shared.cache
var cachesize: UInt? = nil
// Get the disk size taken by the cache.
cache.calculateDiskCacheSize {size in
cachesize = size
}
return cachesize
}
/// 清理缓存,包括: memory & disk
func clearCache() {
let cache = KingfisherManager.shared.cache
// Clear memory cache right away.
cache.clearMemoryCache()
// Clear disk cache. This is an async operation.
cache.clearDiskCache()
}
func downLoadImage(urls: [String?], completionBlock: @escaping () -> Void){
let group = DispatchGroup()
for url in urls {
guard url != nil else {
continue
}
if isImageCached(key: url!) {
continue
}
guard let urlTemp = URL(string: url!) else {
continue
}
group.enter()
ImageDownloader.default.downloadImage(with: urlTemp, options: [], progressBlock: nil) { (image, error, url, data) in
if error == nil, let image = image {
ImageCache.default.store(image, forKey: (url?.absoluteString)!)
}
group.leave()
}
}
group.notify(queue: DispatchQueue.global()) {
completionBlock()
}
}
func retrieveImage(keys: [String?], placeholderImageStr: String? = nil, completionBlock: @escaping ([UIImage]) -> Void){
let group = DispatchGroup()
var images: [UIImage] = []
for key in keys {
group.enter()
if key?.description == nil {
DispatchQueue.global().async {
if let holder = placeholderImageStr {
let placeholder = UIImage(named: holder)
assert(placeholder != nil, "placeholderImageStr 不存在")
images.append(placeholder!)
group.leave()
}
}
} else {
ImageCache.default.retrieveImage(forKey: key!, options: nil) { (image, cacheType) in
if let image = image {
images.append(image)
} else {
NSLog("--------- retrieveImage error -- key : \(key!)")
if let holder = placeholderImageStr {
let placeholder = UIImage(named: holder)
images.append(placeholder!)
assert(placeholder != nil, "placeholderImageStr 不存在")
}
}
group.leave()
}
}
}
group.notify(queue: DispatchQueue.global()) {
completionBlock(images)
}
}
func retrieveImage(forkey: String, completionBlock: @escaping (UIImage?) -> Void) {
ImageCache.default.retrieveImage(forKey: forkey, options: []) { (image, cacheType) in
completionBlock(image)
}
}
func isImageCached(key: String) -> Bool{
let result = ImageCache.default.isImageCached(forKey: key)
return result.cached
}
func storeImage(image: Image, forkey: String) {
if isImageCached(key: forkey) {
ImageCache.default.removeImage(forKey: forkey, fromDisk: true) {
ImageCache.default.store(image, forKey: forkey)
}
} else {
ImageCache.default.store(image, forKey: forkey)
}
}
}
ImageHelper 唯一需要注意的只有 DispatchGroup 的使用。
结束语
最后还是感谢一下第二种方案的原作者。虽然已经找不到原链接,但直接拿来学习使用,并在其基础上做了修改,多少还是要声明感谢一下的。
以上代码均是 swift 编写。只是提供了一种思路,android 开发中也可以用同样的思路来实现。