在iOS中渲染vue与事件处理

上一节我们已经完成了在iOS中集成Vue,并成功拿到了创建Node的数据回调,这一节我们来完成Node的建立与渲染,并完成事件支持。

「第一步: 定义Node节点的数据结构」
具体定义如下:

@interface DomNode : NSObject
/// DomNode的标识符
@property (nonatomic, copy)NSString *ref;
/// 节点的类型(这里暂时定义四种,满足Demo的需要就可以了)
@property (nonatomic, assign)DomNodeType type;
/// 节点的渲染属性,需要在渲染的时候展示出来的(其中有一部分是与布局属性重合的:即在布局属性里面也需要在渲染属性里面)
@property (nonatomic, strong)DomAttribute *attribute;
/// 节点的布局属性,用于Flex布局计算
@property (nonatomic, strong)DomStyle *style;
/// 父节点
@property (nonatomic, weak)DomNode *parent;
/// 子节点
@property (nonatomic, strong)NSMutableArray<DomNode *> *children;
@property (nonatomic, strong)NSArray<NSString *> *events;
@property (nonatomic, assign, getter=isDirty, readonly) BOOL dirty;
@property (nonatomic, assign, readonly)CGSize size;
@property (nonatomic, assign, readonly)CGPoint point;
@property (nonatomic, assign, readonly)CGRect rect;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)initWithRef:(NSString *)ref
                       type:(DomNodeType)type
                  attribute:(DomAttribute *)attribute
                      style:(DomStyle *)style
                     events:(NSArray<NSString *> *)events;

- (void)appendNode:(DomNode *)node;
- (void)fill;
- (void)makeDirty;
- (YGNodeRef)getYGNode;
- (BOOL)requireUpdate;

在这个数据结构中DomStyle是用于参与布局计算的, DomAttribute用于渲染。
他们的具体数据结构如下:

@interface DomStyle : NSObject
@property (nonatomic, assign) YGDirection direction;
@property (nonatomic, assign) YGFlexDirection flexDirection;
@property (nonatomic, assign) YGJustify justifyContent;
@property (nonatomic, assign) YGAlign alignSelf;
@property (nonatomic, assign) YGAlign alignItems;
@property (nonatomic, assign) YGPositionType positionType;
@property (nonatomic, assign) YGWrap flexWrap;
@property (nonatomic, assign) YGOverflow overflow;
@property (nonatomic, assign) YGDisplay display;
@property (nonatomic, assign) int flex;
@property (nonatomic, assign) int flexGrow;
@property (nonatomic, assign) int flexShrink;
@property (nonatomic, assign) DomEdge position;
@property (nonatomic, assign) DomEdge margin;
@property (nonatomic, assign) DomEdge padding;
@property (nonatomic, strong) DomBorder *border;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat maxWidth;
@property (nonatomic, assign) CGFloat minWidth;
@property (nonatomic, assign) CGFloat maxHeight;
@property (nonatomic, assign) CGFloat minHeight;

- (instancetype)initWithData:(NSDictionary *)data;
- (void)updateStyleWithData:(NSDictionary * _Nullable)data;
- (void)fill:(YGNodeRef)ygNode;
@end

style中的数据结构比较简单,需要注意的是在初始化相关属性时,需要与Yoga定义的YGNodeRef中的数据结构初始化值一致,因为我们在fill方法会把所有支持的属性全部同步到YGNodeRef
updateStyleWithDatainitWithData所传递进来的则是从vue中拿到的回调数据,并将他们解析成对应的属性值。
具体的实现代码,我会附加在最后。

@interface DomAttribute : NSObject
@property (nonatomic, strong) NSString *color;
@property (nonatomic, strong) NSString *backgroundColor;
@property (nonatomic, assign) NSInteger fontSize;
@property (nonatomic, strong) NSString *fontFamily;
@property (nonatomic, strong) NSString *value;
@property (nonatomic, strong) NSString *imageNamed;
@property (nonatomic, assign) NSInteger maxNumberLine;
@property (nonatomic, strong) DomBorder *border;

- (instancetype)initWithData:(NSDictionary *)data;
- (void)updateAttributeWithData:(NSDictionary * _Nullable)data;
@end

这里需要注意的是,某些数据不仅参与计算,还参与渲染,比如: border
其他的数据结构定义的实现代码,我会附加在最后。

「第二:构建渲染树」
定义好Node所需要的数据结构之后,我们就可以将回调数据解析成一个Node Tree了。

- (void)_handleCallNativeCallback:(NSString *)instanceId data:(NSDictionary * _Nonnull)data {
    if(!data) return;
    NSDictionary *info = data[@"0"];
    if(!info || ![info isKindOfClass:[NSDictionary class]]) return;
    NSString *method = info[@"method"];
    if(method.length == 0) return;
    if([method isEqualToString:@"createBody"]) {
        [self _createBody:instanceId data:info];
    } else if([method isEqualToString:@"addElement"]) {
        [self _addElement:instanceId data:info];
    } else if([method isEqualToString:@"updateAttrs"]) {
        [self _updateAttrs:info];
    } else if([method isEqualToString:@"updateStyle"]) {
        [self _updateStyles:info];
    } else if([method isEqualToString:@"createFinish"]) {
        [self _createFinished];
    } else {
        NSLog(@"data: %@", data);
    }
}

具体方法实现代码,附加在后面。
通过对callNative的处理,在createFinished时构建好Node Tree。

「第三:完成布局前的准备工作」

构建好Node Tree,就可以通知Yoga,可以开始计算布局了。
在通知Yoga之后,需要将属性映射到YGNodeRef Tree

- (void)fill {
    [self.style fill:_ygNode];
    for(DomNode *child in _children) {
        [child fill];
    }
    _dirty = NO;
}

通过从根节点Node深度遍历调用fill方法,将数据映射到YGNodeRef,这里需要注意的是,具体的fill方法是在style中实现的,因为只有style里面的属性会参与计算。
具体的实现代码如下:

- (void)fill:(YGNodeRef)ygNode {
    YGNodeStyleSetDirection(ygNode, _direction);
    YGNodeStyleSetDisplay(ygNode, _display);
    YGNodeStyleSetFlexDirection(ygNode, _flexDirection);
    YGNodeStyleSetJustifyContent(ygNode, _justifyContent);
    YGNodeStyleSetAlignSelf(ygNode, _alignSelf);
    YGNodeStyleSetAlignItems(ygNode, _alignItems);
    YGNodeStyleSetPositionType(ygNode, _positionType);
    YGNodeStyleSetFlexWrap(ygNode, _flexWrap);
    YGNodeStyleSetOverflow(ygNode, _overflow);
    YGNodeStyleSetFlex(ygNode, _flex);
    YGNodeStyleSetFlexGrow(ygNode, _flexGrow);
    YGNodeStyleSetFlexShrink(ygNode, _flexShrink);
    if(_width >= 0) YGNodeStyleSetWidth(ygNode, _width);
    if(_height >= 0) YGNodeStyleSetHeight(ygNode, _height);
    if(_minWidth >= 0) YGNodeStyleSetMinWidth(ygNode, _minWidth);
    if(_minHeight >= 0) YGNodeStyleSetMinHeight(ygNode, _minHeight);
    if(_maxWidth >= 0) YGNodeStyleSetMaxWidth(ygNode, _maxWidth);
    if(_maxHeight >= 0) YGNodeStyleSetMinWidth(ygNode, _maxHeight);
    YGNodeStyleSetBorder(ygNode, YGEdgeAll, _border.width);
    /// Padding
    if(self.padding.left >= 0)     YGNodeStyleSetPadding(ygNode, YGEdgeLeft, self.padding.left);
    if(self.padding.top >= 0)      YGNodeStyleSetPadding(ygNode, YGEdgeTop, self.padding.top);
    if(self.padding.right >= 0)    YGNodeStyleSetPadding(ygNode, YGEdgeRight, self.padding.right);
    if(self.padding.bottom >= 0)   YGNodeStyleSetPadding(ygNode, YGEdgeBottom, self.padding.bottom);
    /// Margin
    if(self.margin.left >= 0)      YGNodeStyleSetMargin(ygNode, YGEdgeLeft, self.margin.left);
    if(self.margin.top >= 0)       YGNodeStyleSetMargin(ygNode, YGEdgeTop, self.margin.top);
    if(self.margin.right >= 0)     YGNodeStyleSetMargin(ygNode, YGEdgeRight, self.margin.right);
    if(self.margin.bottom >= 0)    YGNodeStyleSetMargin(ygNode, YGEdgeBottom, self.margin.bottom);
    /// Position
    if(self.position.left >= 0)    YGNodeStyleSetPosition(ygNode, YGEdgeLeft, self.position.left);
    if(self.position.top >= 0)     YGNodeStyleSetPosition(ygNode, YGEdgeTop, self.position.top);
    if(self.position.right >= 0)   YGNodeStyleSetPosition(ygNode, YGEdgeRight, self.position.right);
    if(self.position.bottom >= 0)  YGNodeStyleSetPosition(ygNode, YGEdgeBottom, self.position.bottom);
}

构建好YGNodeRef Tree之后就可以进行布局的计算了

CGSize screenSize = self.view.bounds.size;
YGNodeCalculateLayout(ygNode, screenSize.width, screenSize.height, YGNodeStyleGetDirection(ygNode));

通过调用以上接口,计算好每个元素的位置与大小。
这里需要注意的是,screenSize并不是一定要传递屏幕大小,我们需要渲染到的目标视图是多大,就传递多大。
在这里我们刚好使用了整个屏幕

「第四:开始渲染」
完成布局计算后,就开始对Node进行渲染了,代码很简单:
由于是测试代码,所以只是简单的完成了渲染,没有进行优化。
实际上这里应该将不同节点在原生对应的元素定义出来,通过元素内部的方法进行循环渲染,使代码结构更简单。

- (void)_render:(DomNode *)node superView:(UIView *)superView {
    if(!node) return;
    for(DomNode *child in node.children) {
        UIView *childView = NULL;
        if(child.type == DomNodeTypeLabel) {
            UILabel *label = [[UILabel alloc] init];
            label.font = [UIFont systemFontOfSize:child.attribute.fontSize];
            label.textColor = [UIColor colorWithHexString:child.attribute.color alpha:1.0f];
            label.text = child.attribute.value;
            childView = label;
        } else if(child.type == DomNodeTypeView) {
            UIView *view = [[UIView alloc] init];
            view.backgroundColor = [UIColor colorWithHexString:child.attribute.backgroundColor alpha:1.0f];
            childView = view;
        } else if(child.type == DomNodeTypeButton) {
            UIButton *button = [[UIButton alloc] init];
            [button setTitle:child.attribute.value forState:UIControlStateNormal];
            [button setTitleColor:[UIColor colorWithHexString:child.attribute.color alpha:1.0f] forState:UIControlStateNormal];
            button.titleLabel.font = [UIFont systemFontOfSize:child.attribute.fontSize];
            childView = button;
        }
        childView.frame = child.rect;
        childView.backgroundColor = [UIColor colorWithHexString:child.attribute.backgroundColor alpha:1.0f];
        [superView addSubview:childView];
        childView.node = child;
        if(child.events.count > 0) {
            for(NSString *event in child.events) {
                if([event isEqualToString:@"click"]) {
                    childView.userInteractionEnabled = YES;
                    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_clickEvent:)];
                    [childView addGestureRecognizer:tap];
                }
            }
        }
        
        if(child.children.count > 0) {
            [self _render:child superView:childView];
        }
    }
}

完成渲染之后,是这样一个效果:

在iOS中渲染vue与事件处理

「第五:处理事件」
样式的渲染不是一层不变的,最容易想到的就是事件会改变数据的状态,那么事件怎么传递给vue呢。
vue-weex-framework在加载之后,会在globalObject上挂载一个方法__WEEX_CALL_JavaSCRIPT__,通过JSContext来调用这个方法,将事件与事件挂载的元素id传递过去,就完成了在vue内部的事件调用。
代码如下:

- (void)sendEvent:(NSString *)ref event:(NSString *)event {
    NSLog(@"IOS Context收到事件: %@, %@", ref, event);
    NSDictionary *params = @{
        @"module": @"",
        @"method": @"fireEvent",
        @"args": @[
            ref,
            event
        ]
    };
    NSArray *args = @[@"1", @[params]];
    [[_context globalObject] invokeMethod:@"__WEEX_CALL_JavaScript__" withArguments:args];
}

完成了事件的渲染,我们来看看具体的效果

在iOS中渲染vue与事件处理

「这里有一个点需要注意一下:
1.当数据发生变化的时候,怎么让原生感知它的变化呢,这里我使用了CADisplayLink,每一帧都去检测一下Node Tree是否已经发生改变,如果有节点发生改变,就需要重新计算。
庆幸的是Yoga在内部是有缓存的,当我们标记了某一个节点需要重新计算后,Yoga会去判断哪些相关节点需要重新计算,不需要计算的则不会再计算了。
这样就会大大减少数据更新计算布局的时间了。
2.如果使用div来显示文本,在数据发生改变时不会调用updateAttrs,需要使用text标签显示会发生改变的文本信息

到这里,我们基本上完成了从vue到渲染成原生的所有步骤,当然里面还有一些细节是没有处理好的,比如在加载vue模板的时候还可以传递一个json数据进去作为从原生代入的初始数据。
整体的骨架已经有了,感兴趣的朋友优化骨架完善细节就是接下来。

「总结:」
这个小系列分为三个小节,实例了一个有基本骨架结构的渲染vue代码的引擎:
1.完成从vue开发到打包成非浏览器环境使用的代码,完成vue-js-framework打包
2.将打包好的framework与vue模板代码集成到iOS当中
3.完成渲染与事件处理

写到最后:

本文章以iOS平台为宿主环境,很容易的你能想到将这个引擎扩展到android,或者更多的平台。


「附加资料:」
iOS-Vue-Demo: https://Github.com/czqasngit/iOS-Vue-Demo


vue: https://cn.vuejs.org/

weex-framework: 

https://github.com/apache/incubator-weex


webpack: 

https://webpack.js.org/


原文始发于微信公众号(程序猿搬砖):在iOS中渲染vue与事件处理

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/71963.html

(0)

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!