SkeletonView
pod "SkeletonView"
import SkeletonView
view.isSkeletonable = true
view.showSkeleton()
库中常用的算法
- 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。
对View和Layer来说,这个检索可以方便的处理Subview(SubLayer)树。
添加骨骼视图的要点
骨骼视图本质上是为View添加相同形状的mask,然后在Mask上添加动画。那么,其本质就是如下问题:
- 找到需要添加骨骼视图的View
- 根据View生成具有相同形状的骨骼Mask视图
- 对骨骼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 重新读取holder的maxBoundsEstimated并进行赋值
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.pngInsert 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掉。
网友评论