前言
九宮格頭像在網上一搜一大把,相關的優質博文也很多。在這裏我將從另一個角度來分析和實現她。很多朋友對微信的羣頭像很感興趣,那種九宮格頭像乍一看還蠻高級的。但真要我們說出個具體實現方案,相信也沒幾個人能說的清楚。
我們先給幾個方案:
- 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 開發中也可以用同樣的思路來實現。