換一種思路實現 - 九宮格羣頭像

前言

九宮格頭像在網上一搜一大把,相關的優質博文也很多。在這裏我將從另一個角度來分析和實現她。很多朋友對微信的羣頭像很感興趣,那種九宮格頭像乍一看還蠻高級的。但真要我們說出個具體實現方案,相信也沒幾個人能說的清楚。

我們先給幾個方案:

  • 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 開發中也可以用同樣的思路來實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章