在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

在这个数据结构中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-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

**「这里有一个点需要注意一下:

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/

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分