Texture(ASDK)自定义 Node 和 优化 Tab 框架

自定义 ASDisplayNode, 拆分复杂的 Node 为简单的 Node 组件等

Posted by poos on August 18, 2018

经常使用的 ASCellNode 如果比较复杂(普通 cell 也是一样),那么就会包含很多控件,如果不加以拆分,就回造成单个 cell 代码量过大,平行 cell 之间重复代码过多

关于 ASDisplayNode 的官方介绍

像 UIKit 中的 View 一样,Texture 中的 ASDisplayNode 同样可以自定义:ASDisplayNode 的所有接口如下:

ASDisplayNode Class Reference

但是通常我们只需要关心下面几个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//重写初始化方法以传入需要的参数
init() {
}


//主线程调用,在 didLoad 做一些事件绑定的操作
override func didLoad() {
    super.didLoad()
}

//后台线程调用,做布局的最后调整
override func layout() {
    super.layout()
}


//后台线程调用,实现布局
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
    return ASInsetLayoutSpec.init(insets: UIEdgeInsets.init(space: 0), child: moreButton)
}

直接讲一个复杂的例子

微信朋友圈发布动态时候会根据发送图片的个数进行不同的排列

例子:九宫格图片展示器

话不多说,来代码中继续讨论:

需求 :类似微信中的图片显示器,1图按照当前尺寸,多图安装9宫格排列,当然下边的示例代码只是简单化处理了一下

分析 :一种更加方案化的解决方式是使用 CollectionView;通过不同的 type 使用不同的 size 即可,但是 性能会比直接排布查很多,因为相对于这个场景,添加了很多不必要的优化

创建类型

可以通过枚举对象,方便的提供各个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private enum PicturesStyle {
    case larage
    case small

    init(_ count: Int) {
        if count == 1 {
            self = .larage
        } else {
            self = .small
        }
    }

    var width: Double {
        switch self {
            case larage:
            return 23
            //...
        }
    }
}
创建9宫格单个 Cell

单个 cell 创建比较简单,传入图片 url 和 需要在 collection 中显示的方格的自身宽带

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private class CollectionCellNode: ASCellNode {
    let imageNode = NetworkImageNode().then { (node) in
        node.defaultImage = UIImage.init(named: "cardPlaceholder")
    }
    let width: CGFloat

    init(_ path: String, width: CGFloat) {
        self.width = width
        super.init()
        automaticallyManagesSubnodes = true

        imageNode.url = URL.init(string: path)
        imageNode.isLayerBacked = true
    }

    override func layout() {
        super.layout()
        imageNode.layer.borderWidth = 0.5
        imageNode.layer.borderColor = UIColor.hex("ececec").cgColor
    }

    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        imageNode.style.preferredSize = CGSize.init(width: self.width, height: self.width)
        return ASInsetLayoutSpec.init(insets: UIEdgeInsets.init(space: 0), child: imageNode)
    }
}
创建图片展示器

这个视图虽然是个复杂的视图,但是分析优化即可。

如果是单个图就直接返回图片;如果是多个图片就返回 collectionView,并且设置大小为 contentSize 即可完整显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class PicturesView: ASDisplayNode {

    let imagePaths: [String]
    let picStyle: PicturesStyle
    let width: CGFloat

    lazy var layout = CollectionLayout().then { (layout) in
        layout.minimumInteritemSpacing = 2
        layout.minimumLineSpacing = 2
    }

    var collection: ASCollectionNode!
    lazy var imageNode: ASCollectionNode = NetworkImageNode().then { (node) in
        node.defaultImage = UIImage.init(named: "cardPlaceholder")
    }

    init(_ images: [String]) {

        imagePaths = images
        picStyle = PicturesStyle.init(images.count)

        switch picStyle {
        case .larage:
            imageNode.url = URL.init(string: images[0]!)
        case .small:
            collection = ASCollectionNode.init(collectionViewLayout: layout)
            width = (AppDefaults.screenW - 40 - 5) / 3
        }

        super.init()
        automaticallyManagesSubnodes = true
        view.isUserInteractionEnabled = false
    }

    override func didLoad() {
        super.didLoad()
        collection.delegate = self
        collection.dataSource = self
        collection.autoresizesSubviews = true
    }

    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
       guard picStyle == .small else {
           return ASInsetLayoutSpec.init(insets: UIEdgeInsets.init(space: 0), child: imageNode)
       }
        var size = CGSize.init(width: AppDefaults.screenW, height: 0)
        switch picStyle {
        case .larage:
            size.height = width
        case .small:
            let line = Int((imagePaths.count - 1) / 3 + 1)
            size.height = width * CGFloat(line) + 2 * CGFloat(line - 1)
        }
        collection.style.preferredSize = size
        return ASInsetLayoutSpec.init(insets: UIEdgeInsets.init(space: 0), child: collection)
    }
}

//下面是代理部分

extension PicturesView: ASCollectionDelegate, ASCollectionDataSource {

    func collectionNode(_ collectionNode: ASCollectionNode, constrainedSizeForItemAt indexPath: IndexPath) -> ASSizeRange {
        let size = CGSize.init(width: width, height: width)
        return ASSizeRange.init(min: CGSize.zero, max: size)
    }

    func collectionNode(_ collectionNode: ASCollectionNode, numberOfItemsInSection section: Int) -> Int {
        return imagePaths.count
    }

    func collectionNode(_ collectionNode: ASCollectionNode, nodeBlockForItemAt indexPath: IndexPath) -> ASCellNodeBlock {

        let path = imagePaths[indexPath.row]
        let cellNodeBlock = { () -> ASCellNode in
            let cellNode = CollectionCellNode.init(path, width: self.width)
            cellNode.neverShowPlaceholders = true
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
                cellNode.neverShowPlaceholders = false
            })
            return cellNode
        }
        return cellNodeBlock
    }

    func collectionNode(_ collectionNode: ASCollectionNode, didSelectItemAt indexPath: IndexPath) {
        tapClose(indexPath.row)
    }
}

插播一下,如何需要更新 UI 布局,可以生成一个类,继承UICollectionViewFlowLayout,重写下边的方法即可

1
2
3
4
5
// UICollectionView calls these four methods to determine the layout information.
// Implement -layoutAttributesForElementsInRect: to return layout attributes for for supplementary or decoration views, or to perform layout in an as-needed-on-screen fashion.
// Additionally, all layout subclasses should implement -layoutAttributesForItemAtIndexPath: to return layout attributes instances on demand for specific index paths.
// If the layout supports any supplementary or decoration view types, it should also implement the respective atIndexPath: methods for those types.
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect; // return an array layout attributes instances for all the views in the given rect

总结

到这里一个复杂的 View 封装完成了,基本上还是不错了。之所以选择这个例子,也是因为 collection 的特性比较支持九宫格。另外,如果不使用 collection ,强行布局,相信会写很多代码。而且,在 Texture 下,这种布局就更加不适合了。

除此之外想提一点是,组件应当最小化的拆分,本文是跟 Texture 相关的,所以就不展开详细阐述了。

后记

collection 方案省却了很多复杂的代码,使得我们可以使用开箱即用的现成组件。但是在后续的项目优化过程中,发现,collection view 并不适合在 tableview cell 中大量的使用。因为 collection view 有着 cell 复用等特性,所以在此场景下会进行大量的无用的操作。

我们的项目中最终使用了普通的图片堆叠的方式来处理了这种情况。最终的性能得到大幅提升。当然不可避免的是创建一些行组件函数,以实现代码的优化。