美文网首页
SkeletonView 骨架屏源码解析

SkeletonView 骨架屏源码解析

作者: milawoai | 来源:发表于2019-11-27 20:05 被阅读0次

SkeletonView

pod "SkeletonView"

import SkeletonView

view.isSkeletonable = true

view.showSkeleton()   

库中常用的算法

  1. recursiveSearch 递归遍历
protocol IterableElement {}
extension UIView: IterableElement {}
extension CALayer: IterableElement {}

//MARK: Recursive
protocol Recursive {
    associatedtype Element: IterableElement
    func recursiveSearch(leafBlock: VoidBlock, recursiveBlock: RecursiveBlock<Element>)
}

extension Array: Recursive where Element: IterableElement {
    func recursiveSearch(leafBlock: VoidBlock, recursiveBlock: RecursiveBlock<Element>) {
        guard count > 0 else {
            leafBlock()
            return
        }
        forEach { recursiveBlock($0) }
    }
}

当Array没有元素(count == 0)的时候,调用leafBlock,否则对每个元素调用recursiveBlock

ViewLayer来说,这个检索可以方便的处理Subview(SubLayer)树。

添加骨骼视图的要点

骨骼视图本质上是为View添加相同形状的mask,然后在Mask上添加动画。那么,其本质就是如下问题:

  1. 找到需要添加骨骼视图的View
  2. 根据View生成具有相同形状的骨骼Mask视图
  3. 对骨骼Mask视图进行插入,更新和删除

其中,为UIView,UITableView,UICollectionView添加的方法并不一致。让我们先看下UIView上的情况。

UIView的骨骼视图

找到哪些View需要添加骨骼视图

通过extension可以方便的找到需要添加骨骼视图的View。让我们来看下Skeleton的使用:

view.isSkeletonable = true

而在Skeleton中,存在如下扩展:

extension UIView {
    @objc var subviewsSkeletonables: [UIView] {
        return subviewsToSkeleton.filter { $0.isSkeletonable }
    }

    @objc var subviewsToSkeleton: [UIView] {
        return subviews
    }
}

我们可以看到,UIView.subviewsSkeletonables就是View上所有需要产生动画的子View。

生成具有相同形状的骨骼mask
SkeletonLayer

骨骼mask实际上是一个CALayer,我们通过结构体SkeletonLayer存储:

struct SkeletonLayer {
    private var maskLayer: CALayer // 要展示的骨骼
    private weak var holder: UIView? // 骨骼持有者
}

mask的初始化,更新,移除都是由SkeletonLayer完成的

初始化

其初始化进行了如下操作:

init(
    type: SkeletonType, // 1 SkeletonType
    colors: [UIColor],
    skeletonHolder holder: UIView) {
        self.holder = holder // 在holder上添加maske Layer
        self.maskLayer = type.layer // 1
        self.maskLayer.anchorPoint = .zero
        self.maskLayer.bounds = holder.maxBoundsEstimated // 2 maxBoundsEstimated
        addMultilinesIfNeeded()
        self.maskLayer.tint(withColors: colors)
    }

SkeletonType是一个枚举,决定骨骼视图要不要使用渐变。,如果type == .gradient,maskLayer就是一个CAGradientLayer() ,否则maskLayer就是CALayer()

maxBoundsEstimated 是UIView的扩展,通过比较frame的size与Constraints的宽高来计算当前View的最大Bounds。

tint 是CALayer的扩展, 使用了recursiveSearch,为CALayer以及SubCALayer设置背景颜色。

更新bounds

当需要更新MaskLayer的size时,SkeletonLayer通过func layoutIfNeeded 重新读取holdermaxBoundsEstimated并进行赋值

 func layoutIfNeeded() {
  if let bounds = holder?.maxBoundsEstimated {
    maskLayer.bounds = bounds
  }
  updateMultilinesIfNeeded()
}
SkeletonLayerBuilder

SkeletonLayer 通过 SkeletonLayerBuilder 来创建,Builder提供了链式调用的方法配置 skeletonType ,colors,holder; 最终通过func build() -> SkeletonLayer?

MultilinesLayers

对于 UILabel,UITextView 这种展示文字的组件来说,如果文字太长,有可能需要展示多行骨骼视图。这些视图实际上是附在maskLayer的一系列子Layer。
首先,SkeletonView为UILabel,UITextView 设置了协议ContainsMultilineText,并在SkeletonLayer中通过判断当前的holder是否实现该协议来来判断是否需要开启多行Layer的配置:


protocol ContainsMultilineText {
    var numLines: Int { get } // 当前展示了多少行?
    var lastLineFillingPercent: Int { get } // 最后一行填充的百分比
    var multilineCornerRadius: Int { get }  // layer的圆角是多少
}

extension UILabel: ContainsMultilineText {}
extension UITextView: ContainsMultilineText {}

struct SkeletonLayer {
    private var multiLineViewHolder: ContainsMultilineText? {
        guard let multiLineView = holder as? ContainsMultilineText,multiLineView.numLines != 1      else { return nil } // numLines为1时只有一行,不需要配置
    return multiLineView
  }
}

multiLineViewHolder不为空时,就会在SkeletonLayer初始化时开始添加MultilinesLayers

struct SkeletonLayer {
    init(type: SkeletonType, colors: [UIColor], skeletonHolder holder: UIView) {
    ...
        // 开始添加
        addMultilinesIfNeeded()
    ...
    }
    
    func addMultilinesIfNeeded() {
        maskLayer.addMultilinesLayers(
            lines: multiLineView.numLines, 
            type: type, 
            lastLineFillPercent: multiLineView.lastLineFillingPercent,               
            multilineCornerRadius: multiLineView.multilineCornerRadius)
    }
}

顺着代码,我们在CALayer的扩展中找到了func addMultilinesLayers

extension CALayer {
 func addMultilinesLayers(
    lines: Int, // 来自协议 ContainsMultilineText
    type: SkeletonType,
    lastLineFillPercent: Int, // 来自协议 ContainsMultilineText
    multilineCornerRadius: Int // 来自协议 ContainsMultilineText
 ) {
    //计算需要几行Layer,计算方法是
    // 父Layer的高度 bounds.height 除以(子Layer的高度 + 子Layer的间隔并比较是否超出最大行数
  let numberOfSublayers = calculateNumLines(maxLines: lines) 
  // layer的创建者   
  let layerBuilder = SkeletonMultilineLayerBuilder()
            .setSkeletonType(type)
            .setCornerRadius(multilineCornerRadius) 
      
  // 循环生成Layer,并插入父maskLayer    
   (0..<numberOfSublayers).forEach { index in
        // 获取行宽 计算方法是
        // 父Layer的宽度 bounds.width 减去 左右宽,如果是最后一行,还要乘上缩短的系数**lastLineFillPercent**
        var width = getLineWidth(
            index: index,
            numberOfSublayers: numberOfSublayers, 
            lastLineFillPercent: lastLineFillPercent)
    if let layer = layerBuilder
                .setIndex(index)
                .setWidth(width)
                .build() {
                // 生成的一行一行的Layer被附在了maskLayer上
                addSublayer(layer)
            }
    }  
}

下图可以看到多行Layer的样式:

image.png
Insert SketelonLayer

在用户调用func showSkeleton() 时, 调用顺序如下

view.showSkeleton()  => 
recursiveShowSkeleton() =>
showSkeletonIfNotActive() =>
addSkeletonLayer() => 
SkeletonLayerBuilder().build() =>
view.layer.insertSublayer()

让我们来看下func insertSublayer()

func insertSublayer(
_ layer: SkeletonLayer, // 要插入的Layer
at idx: UInt32,        // 插入在那个Index
transition: SkeletonTransitionStyle, // layer出现动画
completion: (() -> Void)? = nil) // 结束回调
{
    // 插入Layer
    insertSublayer(layer.contentLayer, at: idx)
    switch transition {
        case .none: // 没有动画
            completion?()
            break
        case .crossDissolve(let duration): // 淡入淡出
            layer.contentLayer.setOpacity(from: 0, to: 1, duration: duration, completion: completion)
        }
}


layer.insertSublayer(
    skeletonLayer,
    at: UInt32.max,
    transition: config.transition) {
    [weak self] in
        if config.animated {
            self?.startSkeletonAnimation(config.animation) // 开始加载动画
        }
}
更新视图

SkeletonView提供了func updateSkeleton(skeletonConfig) 来手工更新骨骼视图。
此外,SkeletonView通过Runtime替换了组件的layoutSubviews方法:

 private func swizzleLayoutSubviews() {
 swizzle(
    selector:#selector(UIView.layoutSubviews),
    with: #selector(UIView.skeletonLayoutSubviews),
    inClass: UIView.self,
    usingClass: UIView.self)
 }

func layoutSubviews会在view的size发生改变的时候调用,而func skeletonLayoutSubviews会调用SkeletonLayer,更新Layer的bounds:

if let bounds = holder?.maxBoundsEstimated {
    maskLayer.bounds = bounds
}
删除视图

func hideSkeleton 会将所有Layer从对应的UIView上Remove掉。

相关文章

网友评论

      本文标题:SkeletonView 骨架屏源码解析

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