换一种思路实现 - 九宫格群头像

作者: Robert_Zhang | 来源:发表于2017-11-23 09:54 被阅读55次

    前言

    九宫格头像在网上一搜一大把,相关的优质博文也很多。在这里我将从另一个角度来分析和实现她。很多朋友对微信的群头像很感兴趣,那种九宫格头像乍一看还蛮高级的。但真要我们说出个具体实现方案,相信也没几个人能说的清楚。

    我们先给几个方案:

    • sever 端生成好九宫格头像,client 端直接通过生成好的图片 url 来显示
    • 在一个 ImageView 上(View 上也可),放九个 Imageview,然后让他们分别加载图片。
    • 将九张图片拼接生成一张图片,保存在本地然后使用她

    看看具体效果


    九宫格头像.jpeg

    思路

    根据上面的方案我们来详细讲解一下。
    第一种,很明显和我们关系不大,只需要选择一款第三方图片加载库就能轻松搞定。

    第二种,也是现在在网上大量文献使用的方法,相对好理解。在这里我也简单介绍一下。直接上代码,代码是我从网上找的(根据需要做了部分修改)。因为时间有点久,原地址在哪也不记得了。望原文作者见谅。

    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 开发中也可以用同样的思路来实现。

    相关文章

      网友评论

        本文标题:换一种思路实现 - 九宫格群头像

        本文链接:https://www.haomeiwen.com/subject/ghtovxtx.html