上一节我们已经完成了在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
。
updateStyleWithData
与initWithData
所传递进来的则是从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];
}
}
}
完成渲染之后,是这样一个效果:
「第五:处理事件」
样式的渲染不是一层不变的,最容易想到的就是事件会改变数据的状态,那么事件怎么传递给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];
}
完成了事件的渲染,我们来看看具体的效果
**「这里有一个点需要注意一下:
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/
全部0条评论
快来发表一下你的评论吧 !