Texture ASTableNode 實現iOS直播聊天消息界面

近幾年直播一火再火,如今的直播已經再也不是主播們唱唱歌了,連老羅都已經開始直播帶貨,一再刷新抖音直播在線人數了。html

IMG_1885

但今天咱們不是來講怎麼作直播的,是來看看直播場景裏的聊天消息界面是如何實現的。node

估計不少人要失望了😀😀bash

要實現聊天消息界面,不可不用 UITableView。當幾年前我開始自學開發 iOS APP 時,我就開始使用 AsyncDisplayKit,如今已經改名爲:Texture。app

Keeps the most complex iOS user interfaces smooth and responsive. Texture is an iOS framework built on top of UIKit that keeps even the most complex user interfaces smooth and responsive. It was originally built to make Facebook's Paper possible, and goes hand-in-hand with pop's physics-based animations — but it's just as powerful with UIKit Dynamics and conventional app designs. More recently, it was used to power Pinterest's app rewrite.less

As the framework has grown, many features have been added that can save developers tons of time by eliminating common boilerplate style structures common in modern iOS apps. If you've ever dealt with cell reuse bugs, tried to performantly preload data for a page or scroll style interface or even just tried to keep your app from dropping too many frames you can benefit from integrating Texture.函數

參考Texture 官網佈局

之後把每次用到的 Nodes 心得寫出來,今天來講一說使用 ASTableNode測試

初始化 ASTableNode

建立 ASTableNodeUITableView 同樣,比較簡單。flex

@interface TestViewController () <ASTableDataSource, ASTableDelegate>

@property (nonatomic, strong) ASTableNode *tableNode;

@property (nonatomic, strong) NSMutableArray *dataSource;

@end
複製代碼

初始化:ui

_tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain];
    
_tableNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

_tableNode.backgroundColor = [UIColor.clearColor colorWithAlphaComponent:0.0];

_tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone;

[self.view addSubnode:_tableNode];

_tableNode.frame = CGRectMake(0, self.view.bounds.size.height - 300, 300, 200);

// 填充測試數據
_dataSource = [NSMutableArray arrayWithArray:@[
        @{@"type": @"TEXT", @"text": @"你好", @"nickname": @"yemeishu"},
        @{@"type": @"TEXT", @"text": @"你好,這個主播不錯哦~", @"nickname": @"yemeishu"},
        @{@"type": @"TEXT", @"text": @"如今直播還能夠帶貨了", @"nickname": @"yemeishu"}
]];

_tableNode.delegate = self;
_tableNode.dataSource = self;
_tableNode.view.allowsSelection = NO;
複製代碼

UITableView 同樣,實現 dataSorcedelegate (這裏暫時不寫對 Node 操做):

#pragma mark - ASTableNode
- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSDictionary *message = self.dataSource[(NSUInteger) indexPath.row];
    return ^{
        return [[MessageNode alloc] initWithMessage: message];
    };
}

- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section
{
    return self.dataSource.count;
}
複製代碼

如今能夠建立 ASCellNode 子類。

編寫 ASCellNode 子類

ASCellNode, as you may have guessed, is the cell class of Texture. Unlike the various cells in UIKit, ASCellNode can be used with ASTableNodes, ASCollectionNodes and ASPagerNodes, making it incredibly flexible.

ASCellNode 核心函數主要有四個,咱們的重點在 layoutSpecThatFits 上。

  • -init – Thread safe initialization.

  • -layoutSpecThatFits: – Return a layout spec that defines the layout of your cell.

  • -didLoad – Called on the main thread. Good place to add gesture recognizers, etc.

  • -layout – Also called on the main thread. Layout is complete after the call to super which means you can do any extra tweaking you need to do.

具體 MessageNode 類直接看代碼,只要將每一個人聊天的信息發給 MessageNode 填充內容:

@interface MessageNode : ASCellNode
- (instancetype)initWithMessage:(NSDictionary *)message;
@end
複製代碼

這裏爲了簡單實現效果,只是顯示消息者姓名和消息內容。

#import "MessageNode.h"

@interface ZJMessageNode()
@property (strong, nonatomic) ASButtonNode *textNode;
@end

@implementation MessageNode {

}
- (instancetype)initWithMessage:(NSDictionary *)message {
    self = [super init];
    if (self) {
        _textNode = [[ASButtonNode alloc] init];
        NSString* nickname = @"";
        NSString* text = @"";
        if ([message[@"type"] isEqual: @"TEXT"]) {
            nickname = [NSString stringWithFormat:@"[%@]:",message[@"nickname"]];
            text = message[@"text"];
        } else {
            nickname = @"其餘人";
            text = @"其餘消息";
        }
        NSMutableAttributedString* string = [[NSMutableAttributedString alloc] initWithString:@""];

        NSAttributedString* nameString = [[NSAttributedString alloc] initWithString:nickname attributes:@{
                NSFontAttributeName : [UIFont systemFontOfSize:14.0],
                NSForegroundColorAttributeName: UIColorMakeWithHex(@"#FF9900")
        }];

        NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
        paragraphStyle.lineSpacing = 5.0;
        NSAttributedString* textString = [[NSAttributedString alloc] initWithString: text attributes:@{
                NSFontAttributeName : [UIFont systemFontOfSize:14.0],
                NSForegroundColorAttributeName: UIColor.ZJ_tintColor,
                NSParagraphStyleAttributeName: paragraphStyle
        }];
        [string appendAttributedString:nameString];
        [string appendAttributedString:textString];
        _textNode.titleNode.attributedText = string;
        _textNode.titleNode.maximumNumberOfLines = 3;

        _textNode.backgroundImageNode.image = [UIImage as_resizableRoundedImageWithCornerRadius:8
                                                                                    cornerColor:UIColor.clearColor
                                                                                      fillColor: [UIColor colorWithRed:26/255.0 green:26/255.0 blue:26/255.0 alpha:0.5]];
        [self addSubnode:_textNode];
    }

    return self;
}
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize {
    [_textNode.titleNode setTextContainerInset:UIEdgeInsetsMake(9, 14.5, 9, 8.5)];
    ASAbsoluteLayoutSpec *absoluteSpec = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[_textNode]];
    // ASAbsoluteLayoutSpec's .sizing property recreates the behavior of ASDK Layout API 1.0's "ASStaticLayoutSpec"
    absoluteSpec.sizing = ASAbsoluteLayoutSpecSizingSizeToFit;

    return [ASInsetLayoutSpec
            insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10)
                                child:absoluteSpec];
}
@end
複製代碼

好了,讓咱們運行下 Demo,看看效果:

是否是和抖音上的聊天界面效果差很少:

IMG_1877

解析 MessageNode

  • ASButtonNode

Demo 中主要用一個 ASButtonNode 主要有這幾點考慮。

  1. 參考不少直播聊天消息顯示界面,每一個聊天體主要以 Text 文本爲主,並且把關鍵的信息用不一樣的顏色和大小作區分,因此用 NSMutableAttributedString 比較合適。因此在 MessageNode 中主要以 ASTextNode 爲主。在本文中,爲了演示,主要拿暱稱和消息內容,用函數 appendAttributedString 拼接在一塊兒。
  2. 要爲 ASTextNode 添加一個半透明、圓角的「背景」層,因此須要增長一個 ASImageNode
  3. 若是對於複雜的消息,須要在姓名以前增長一個相似 VIP 等級圖標等,這也就有可能還須要一個 ASImageNode

因此要能知足以上三點要求,最好的 Node 就是 ASButtonNode

若是咱們直接在 MessageNode 放三個元素 (一個 ASTextNode,兩個 ASImageNode) 也能知足須要,但元素間的佈局和定位就很差設計了,無形增長代碼量和難度。

  • ASAbsoluteLayoutSpec

因爲使用了 ASTableNode,對每個消息體的最大寬度默認都和 ASTableNode 同樣。因此在佈局時,若是咱們採用其餘的 ASLayoutSpec 的佈局方式,呈現的結果就很難像直播窗口那樣了,可以實時根據文本的長度顯示,不至於每一個消息體都是等寬的,很差看。

因此本文推薦使用 ASAbsoluteLayoutSpec

Within ASAbsoluteLayoutSpec you can specify exact locations (x/y coordinates) of its children by setting their layoutPosition property. Absolute layouts are less flexible and harder to maintain than other types of layouts.

ASAbsoluteLayoutSpec has one property:

sizing. Determines how much space the absolute spec will take up. Options include: Default, and Size to Fit. Note that the Size to Fit option will replicate the behavior of the old ASStaticLayoutSpec.

這裏我設定 sizing 爲:

absoluteSpec.sizing = ASAbsoluteLayoutSpecSizingSizeToFit;
複製代碼

最後就是給各個消息體設定一個 EdgeInsets,分開點,省得每一個消息體都是挨着的:

[ASInsetLayoutSpec
            insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10)
                                child:absoluteSpec];
複製代碼

參考: Layout Specs

總結

這是一篇簡單使用 ASTableNode 記錄。固然還有不少 ASTableNode 屬性和方法都沒介紹和使用,在接下來的編碼過程當中再分享出來,還有包括各類各樣的 Layout 佈局的使用。

敬請關注!

相關文章
相關標籤/搜索