React Native 动画与布局技术解析
1. 动画节点关系建立与更新标记
在 React Native 的动画系统中,首先要建立节点之间的父子关系,并为子节点设置更新标记。以下是相关代码:
[parentNode addChild:childNode]; // 建立与 JavaScript 层相同的父子关系
[childNode setNeedsUpdate]; // 为子节点设置 “needs update” 标记
这两步操作是动画节点处理的基础,前者确保了节点在结构上的一致性,后者为后续的更新操作提供了标记。
2. 绑定事件接收器
动画的起始点通常与
AnimatedProps
组件相关联。以下是
AnimatedProps
类的部分代码:
class AnimatedProps extends AnimatedNode {
__makeNative(): void {
if (!this.__isNative) {
this.__isNative = true;
for (const key in this._props) {
const value = this._props[key];
if (value instanceof AnimatedNode) {
value.__makeNative();
}
}
if (this._animatedView) {
this.__connectAnimatedView();
}
}
}
setNativeView(animatedView: any): void {
if (this._animatedView === animatedView) {
return;
}
this._animatedView = animatedView;
if (this.__isNative) {
this.__connectAnimatedView();
}
}
__connectAnimatedView(): void {
invariant(this.__isNative, 'Expected node to be marked as "native"');
const nativeViewTag: ?number = ReactNative.findNodeHandle(
this._animatedView,
);
NativeAnimatedHelper.API.connectAnimatedNodeToView(
this.__getNativeTag(),
nativeViewTag,
);
}
}
操作步骤:
-
__makeNative方法会递归地将所有节点标记为原生节点,并连接动画视图。 -
setNativeView方法用于设置动画视图,并在节点为原生节点时连接动画视图。 -
__connectAnimatedView方法通过调用connectAnimatedNodeToView方法建立节点与视图的连接。
原生层实现
原生模块
RCTNativeAnimatedModule
作为一个薄包装,将工作委托给
RCTNativeAnimatedNodesManager
。以下是相关代码:
RCT_EXPORT_METHOD(connectAnimatedNodeToView:(double)nodeTag
viewTag:(double)viewTag)
{
NSString *viewName = [self.bridge.uiManager
viewNameForReactTag:[NSNumber numberWithDouble:viewTag]];
[self addOperationBlock:^(RCTNativeAnimatedNodesManager
*nodesManager) {
[nodesManager connectAnimatedNodeToView:[NSNumber
numberWithDouble:nodeTag] viewTag:[NSNumber
numberWithDouble:viewTag] viewName:viewName];
}];
}
- (void)connectAnimatedNodeToView:(nonnull NSNumber *)nodeTag
viewTag:(nonnull NSNumber *)viewTag
viewName:(nonnull NSString *)viewName
{
RCTAnimatedNode *node = _animationNodes[nodeTag];
if ([node isKindOfClass:[RCTPropsAnimatedNode class]]) {
[(RCTPropsAnimatedNode *)node connectToView:viewTag
viewName:viewName bridge:_bridge];
}
[node setNeedsUpdate];
}
操作步骤:
-
RCTNativeAnimatedModule接收节点和视图的标签,并获取视图名称。 -
将操作块添加到
RCTNativeAnimatedNodesManager中,调用connectAnimatedNodeToView方法。 -
在
RCTNativeAnimatedNodesManager中,根据节点类型进行相应处理,并为节点设置更新标记。
3. 附加事件源
事件源通常绑定到
ScrollView
或
FlatList
上。以下是
FlatList
的相关代码:
<Animated.FlatList
data={this.state.data}
renderItem={this.renderItem}
onViewableItemsChanged={this.onViewableItemsChanged}
contentInset={{
top: this.state.loading ? 5: 0
}}
scrollEventThrottle={1}
onScroll={
Animated.event([{
nativeEvent: { contentOffset: { y: this.pullDownPos } }
}], { useNativeDriver: true })
}
onScrollBeginDrag={this.beginDrag}
onScrollEndDrag={this.endDrag}
ref={this.getScrollViewRef}
onMomentumScrollEnd={this.onReset}
/>
操作步骤:
-
Animated.event方法实例化一个AnimatedEvent对象。 -
通过设置
nativeEvent和useNativeDriver: true,将事件发送到原生层。
事件处理逻辑
在
createAnimatedComponent
中,会将
nativeEvent
对象和
onScroll
属性转换为原生层可理解的元数据。以下是相关代码:
_attachNativeEvents() {
const scrollableNode = this._component?.getScrollableNode
? this._component.getScrollableNode()
: this._component;
for (const key in this.props) {
const prop = this.props[key];
if (prop instanceof AnimatedEvent && prop.__isNative) {
prop.__attach(scrollableNode, key);
this._eventDetachers.push(() =>
prop.__detach(scrollableNode, key)
);
}
}
}
__attach(viewRef: any, eventName: string) {
this._attachedEvent = attachNativeEvent(
viewRef,
eventName,
this._argMapping,
);
}
function attachNativeEvent(
viewRef: any,
eventName: string,
argMapping: $ReadOnlyArray<?Mapping>,
): {detach: () => void} {
const traverse = (value, path) => {
if (value instanceof AnimatedValue) {
value.__makeNative();
eventMappings.push({
nativeEventPath: path,
animatedValueTag: value.__getNativeTag(),
});
} else if (typeof value === 'object') {
for (const key in value) {
traverse(value[key], path.concat(key));
}
}
};
traverse(argMapping[0].nativeEvent, []);
const viewTag = ReactNative.findNodeHandle(viewRef);
if (viewTag != null) {
eventMappings.forEach(mapping => {
NativeAnimatedHelper.API.addAnimatedEventToView(
viewTag,
eventName,
mapping,
);
});
}
return {
// ...
};
}
操作步骤:
-
_attachNativeEvents方法遍历组件的属性,找到AnimatedEvent并调用__attach方法。 -
__attach方法调用attachNativeEvent方法。 -
attachNativeEvent方法通过traverse函数提取事件路径和动画值的映射,并将其传递给原生层。
原生层事件附加实现
原生模块
RCTNativeAnimatedModule
同样将工作委托给
RCTNativeAnimatedNodesManager
。以下是相关代码:
RCT_EXPORT_METHOD(addAnimatedEventToView:(double)viewTag
eventName:(nonnull NSString *)eventName
eventMapping:(JS::NativeAnimatedModule::Event
Mapping &)eventMapping)
{
NSMutableDictionary *eventMappingDict =
[NSMutableDictionary new];
eventMappingDict[@"nativeEventPath"] = RCTConvertVecToArray(
eventMapping.nativeEventPath());
if (eventMapping.animatedValueTag()) {
eventMappingDict[@"animatedValueTag"] =
@(*eventMapping.animatedValueTag());
}
[self addOperationBlock:^(RCTNativeAnimatedNodesManager
*nodesManager) {
[nodesManager addAnimatedEventToView:[NSNumber
numberWithDouble:viewTag] eventName:eventName
eventMapping:eventMappingDict];
}];
}
- (void)addAnimatedEventToView:(nonnull NSNumber *)viewTag
eventName:(nonnull NSString *)eventName
eventMapping:(NSDictionary<NSString*, id> *)eventMapping
{
NSNumber *nodeTag = [RCTConvert NSNumber:eventMapping[
@"animatedValueTag"]];
RCTAnimatedNode *node = _animationNodes[nodeTag];
// 错误检查
NSArray<NSString *> *eventPath =
[RCTConvert NSStringArray:eventMapping[@"nativeEventPath"]];
RCTEventAnimation *driver =
[[RCTEventAnimation alloc] initWithEventPath:eventPath
valueNode:(RCTValueAnimatedNode *)node];
NSString *key = [NSString stringWithFormat:@"%@%@", viewTag,
RCTNormalizeAnimatedEventName(eventName)];
if (_eventDrivers[key] != nil) {
[_eventDrivers[key] addObject:driver];
} else {
NSMutableArray<RCTEventAnimation *> *drivers =
[NSMutableArray new];
[drivers addObject:driver];
_eventDrivers[key] = drivers;
}
}
操作步骤:
-
RCTNativeAnimatedModule接收视图标签、事件名称和事件映射,并重新组织参数。 -
将操作块添加到
RCTNativeAnimatedNodesManager中,调用addAnimatedEventToView方法。 -
在
RCTNativeAnimatedNodesManager中,创建RCTEventAnimation对象,并记录事件键。
4. 原生事件传输
手势驱动的动画从
RCTScrollView
的手势开始。以下是
RCTScrollView
的事件处理代码:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSTimeInterval now = CACurrentMediaTime();
[self updateClippedSubviews];
if (_allowNextScrollNoMatterWhat ||
(_scrollEventThrottle > 0 && _scrollEventThrottle < MAX
(0.017, now - _lastScrollDispatchTime))) {
RCT_SEND_SCROLL_EVENT(onScroll, nil);
_lastScrollDispatchTime = now;
_allowNextScrollNoMatterWhat = NO;
}
RCT_FORWARD_SCROLL_EVENT(scrollViewDidScroll : scrollView);
}
#define RCT_SEND_SCROLL_EVENT(_eventName, _userData) \
{ \
NSString *eventName = NSStringFromSelector(@selector
(_eventName)); \
[self sendScrollEventWithName:eventName scrollView:_
scrollView userData:_userData]; \
}
- (void)sendScrollEventWithName:(NSString *)eventName
scrollView:(UIScrollView *)scrollView
userData:(NSDictionary *)userData
{
if (![_lastEmittedEventName isEqualToString:eventName]) {
_coalescingKey++;
_lastEmittedEventName = [eventName copy];
}
RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc]
initWithEventName:eventName
reactTag:self.reactTag
scrollViewContentOffset:scrollView.contentOffset
scrollViewContentInset:scrollView.contentInset
scrollViewContentSize:scrollView.contentSize
scrollViewFrame:scrollView.frame
scrollViewZoomScale:scrollView.zoomScale
userData:userData
coalescingKey:_coalescingKey];
[_eventDispatcher sendEvent:scrollEvent];
}
操作步骤:
-
scrollViewDidScroll方法在滚动时检查节流条件,满足条件则发送滚动事件。 -
sendScrollEventWithName方法创建RCTScrollEvent对象,并通过事件调度器发送事件。
事件调度与处理
事件最终发送到
RCTNativeAnimatedModule
,再由其传递给
RCTNativeAnimatedNodesManager
进行处理。以下是相关代码:
// # RCTNativeAnimatedModule
- (void)eventDispatcherWillDispatchEvent:(id<RCTEvent>)event
{
RCTExecuteOnMainQueue(^{
[self->_nodesManager handleAnimatedEvent:event];
});
}
// # RCTNativeAnimatedNodesManager
- (void)handleAnimatedEvent:(id<RCTEvent>)event
{
if (_eventDrivers.count == 0) {
return;
}
NSString *key = [NSString stringWithFormat:@"%@%@",
event.viewTag, RCTNormalizeAnimatedEventName(event.eventName)];
NSMutableArray<RCTEventAnimation *> *driversForKey =
_eventDrivers
[key];
if (driversForKey) {
for (RCTEventAnimation *driver in driversForKey) {
[self stopAnimationsForNode:driver.valueNode];
[driver updateWithEvent:event];
}
[self updateAnimations];
}
}
操作步骤:
-
RCTNativeAnimatedModule在事件调度前将事件传递给RCTNativeAnimatedNodesManager。 -
RCTNativeAnimatedNodesManager根据事件找到相关的动画驱动,并调用updateWithEvent方法进行深度优先搜索,标记事件接收器。 -
调用
updateAnimations方法进行广度优先搜索,更新所有相关的动画节点。
事件接收器识别
updateWithEvent
方法通过深度优先搜索识别所有事件接收器。以下是相关代码:
- (void)updateWithEvent:(id<RCTEvent>)event
{
NSArray *args = event.arguments;
id currentValue = args[2];
for (NSString *key in _eventPath) {
currentValue = [currentValue valueForKey:key];
}
_valueNode.value = ((NSNumber *)currentValue).doubleValue;
[_valueNode setNeedsUpdate];
}
- (void)setNeedsUpdate
{
_needsUpdate = YES;
for (RCTAnimatedNode *child in _childNodes.
objectEnumerator) {
[child setNeedsUpdate];
}
}
操作步骤:
-
updateWithEvent方法从事件中提取值,并更新动画节点的值。 -
调用
setNeedsUpdate方法标记自身和所有子节点为需要更新。
动画节点更新
updateAnimations
方法通过广度优先搜索更新所有相关的动画节点。以下是相关代码:
// # RCTNativeAnimatedNodesManager
- (void)updateAnimations
{
[_animationNodes enumerateKeysAndObjectsUsingBlock:
^(NSNumber *key, RCTAnimatedNode *node, BOOL *stop) {
if (node.needsUpdate) {
[node updateNodeIfNecessary];
}
}];
}
// # RCTAnimatedNode
- (void)updateNodeIfNecessary
{
if (_needsUpdate) {
for (RCTAnimatedNode *parent in _parentNodes.
objectEnumerator) {
[parent updateNodeIfNecessary];
}
[self performUpdate];
}
}
// # RCTAdditionAnimatedNode
- (void)performUpdate
{
[super performUpdate];
NSArray<NSNumber *> *inputNodes = self.config[@"input"];
if (inputNodes.count > 1) {
RCTValueAnimatedNode *parent1 = (RCTValueAnimatedNode *)
[self.parentNodes objectForKey:inputNodes[0]];
RCTValueAnimatedNode *parent2 = (RCTValueAnimatedNode *)
[self.parentNodes objectForKey:inputNodes[1]];
if ([parent1 isKindOfClass:[RCTValueAnimatedNode class]] &&
[parent2 isKindOfClass:[RCTValueAnimatedNode class]]) {
self.value = parent1.value + parent2.value;
}
}
}
// # RCTPropsAnimatedNode
- (void)performUpdate
{
[super performUpdate];
if (!_connectedViewTag) {
return;
}
for (NSNumber *parentTag in self.parentNodes.keyEnumerator) {
RCTAnimatedNode *parentNode =
[self.parentNodes objectForKey:parentTag];
if (
[parentNode isKindOfClass:[RCTStyleAnimatedNode class]]
) {
[self->_propsDictionary addEntriesFromDictionary:
[(RCTStyleAnimatedNode *)parentNode
propsDictionary]];
} else if (
[parentNode isKindOfClass:[RCTValueAnimatedNode class]]
) {
NSString *property =
[self propertyNameForParentTag:parentTag];
id animatedObject =
[(RCTValueAnimatedNode *)parentNode animatedObject];
if (animatedObject) {
self->_propsDictionary[property] =
animatedObject;
} else {
CGFloat value =
[(RCTValueAnimatedNode *)parentNode value];
self->_propsDictionary[property] = @(value);
}
}
}
if (_propsDictionary.count) {
[self updateView];
}
}
- (void)updateView
{
if (_managedByFabric) {
// ...
} else {
[_bridge.uiManager
synchronouslyUpdateViewOnUIThread:_connectedViewTag
viewName:_connectedViewName
props:_propsDictionary];
}
}
操作步骤:
-
updateAnimations方法遍历所有注册的动画节点,调用updateNodeIfNecessary方法。 -
updateNodeIfNecessary方法通过广度优先搜索更新所有父节点,然后调用performUpdate方法进行实际更新。 -
performUpdate方法根据节点类型进行不同的更新操作,最终调用updateView方法更新视图。
5. 布局设计建议
在进行布局设计时,尤其是垂直布局,为了适应不同屏幕尺寸,可参考以下建议:
1.
避免使用绝对高度值
:使用相对值或灵活的布局属性,以确保在不同屏幕上都能正常显示。
2.
使组件可布局
:自定义组件应遵循标准组件的布局规则,尊重
flex
属性,可使用扩展运算符处理样式属性。
3.
避免过度抽象
:尽量扁平化组件层次结构,将相关组件放在一起,便于编程、推理和调试,同时也有利于动画代码的编写。
4.
充分利用空间
:结合固有尺寸和
flexGrow
属性,让具有固定尺寸的组件占据所需空间,利用
flexGrow
分配剩余空间。
5.
谨慎使用原始计算
:基于屏幕高度的原始计算可作为最后手段,因为它扩展性差且难以维护。
总结
通过以上步骤,我们详细介绍了 React Native 中动画节点的处理、事件绑定与传输以及布局设计的相关技术。这些技术对于实现流畅的动画效果和适应不同屏幕尺寸的布局至关重要。以下是整个流程的 mermaid 流程图:
graph TD;
A[建立节点父子关系并标记更新] --> B[绑定事件接收器];
B --> C[附加事件源];
C --> D[原生事件传输];
D --> E[识别事件接收器];
E --> F[更新动画节点];
F --> G[更新视图];
H[布局设计] --> I[遵循布局建议];
通过合理运用这些技术和建议,开发者可以创建出更加优质的 React Native 应用。
6. 动画技术相关总结
6.1 动画类型及特点
在 React Native 中,动画技术丰富多样,不同类型的动画有着各自的特点和应用场景:
| 动画类型 | 特点 | 应用场景 |
| ---- | ---- | ---- |
| 手势驱动动画 | 基于用户手势触发,如滑动、点击等,具有较强的交互性。通过计算手势的位置、速度等参数,实现动态的动画效果。 | 下拉刷新、滑动切换页面等交互场景。 |
| 布局动画 | 用于组件布局的变化,如展开、收缩等。可以设置动画的时长、延迟、动画曲线等参数,使布局变化更加平滑。 | 消息展开、列表项展开等场景。 |
| 值动画 | 通过改变某个值的大小来实现动画效果,如透明度、位置、缩放等。可以使用
Animated
库提供的方法,如
Animated.timing()
、
Animated.spring()
等。 | 加载指示器、元素的淡入淡出等场景。 |
6.2 动画实现的关键步骤
动画的实现通常需要经过以下几个关键步骤:
1.
节点关系建立
:通过
[parentNode addChild:childNode]
建立节点之间的父子关系,确保动画节点在结构上的一致性。
2.
事件绑定
:将事件源(如
ScrollView
的滚动事件)与动画节点绑定,通过
Animated.event()
方法实现。
3.
原生层交互
:将动画相关的操作传递到原生层,利用原生层的性能优势实现流畅的动画效果。例如,通过
NativeAnimatedHelper.API
调用原生方法。
4.
动画更新
:在事件触发时,更新动画节点的值,通过深度优先搜索和广度优先搜索更新相关节点,最终更新视图。
6.3 动画性能优化
为了提高动画的性能,可采取以下优化措施:
-
使用原生驱动
:在
Animated.event()
中设置
useNativeDriver: true
,将动画计算和渲染任务交给原生层处理,减少 JavaScript 线程的负担。
-
合理设置节流
:在滚动事件等频繁触发的事件中,设置合理的节流时间,避免过多的计算和渲染操作。
-
避免不必要的更新
:在
shouldComponentUpdate()
方法中进行判断,避免不必要的组件更新,减少渲染次数。
7. 网络编程相关要点
7.1 网络协议及特点
在 React Native 应用开发中,涉及到多种网络协议,不同协议有着不同的特点和应用场景:
| 协议名称 | 特点 | 应用场景 |
| ---- | ---- | ---- |
| HTTP/1.1 | 简单易用,支持多种请求方法(如 GET、POST、PUT 等),但存在队头阻塞问题。 | 简单的网络请求,如获取数据、提交表单等。 |
| HTTP/2 | 支持多路复用、二进制分帧、头部压缩等特性,提高了传输效率,减少了延迟。 | 对性能要求较高的网络请求,如大量数据的传输。 |
| TCP/IP | 是互联网的基础协议,提供可靠的、面向连接的传输服务。 | 各种网络通信场景,确保数据的可靠传输。 |
| TLS | 提供数据的加密传输,保障数据的安全性。 | 涉及敏感信息传输的场景,如登录、支付等。 |
7.2 网络请求处理
在进行网络请求时,需要考虑以下几个方面:
1.
异步操作
:使用
async/await
或
Promise
处理异步请求,避免阻塞主线程。例如:
async function loadData() {
try {
const response = await fetch('https://example.com/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
}
}
-
错误处理
:对网络请求可能出现的错误进行捕获和处理,如网络连接失败、服务器返回错误等。可以使用
try...catch语句进行错误捕获。 -
缓存策略
:采用本地缓存机制,减少对网络资源的频繁请求。例如,使用
AsyncStorage存储数据,在下次请求时先检查缓存中是否存在数据。
7.3 离线策略
为了提高应用在离线状态下的可用性,可采取以下离线策略:
-
本地缓存
:将常用的数据缓存到本地,如图片、文章内容等。在离线状态下,优先使用本地缓存的数据。
-
离线模式提示
:当检测到网络连接断开时,向用户提示当前处于离线模式,并提供相应的操作建议。
-
数据预加载
:在网络连接正常时,预加载一些可能会用到的数据,以应对离线情况。
8. 组件开发与设计
8.1 组件构建原则
在 React Native 中,组件是构建应用的基本单元,组件的设计和开发应遵循以下原则:
-
单一职责原则
:每个组件只负责一个特定的功能或任务,提高组件的可维护性和复用性。
-
高内聚低耦合
:组件内部的功能应紧密相关,组件之间的依赖关系应尽量简单。
-
可配置性
:组件应提供一些可配置的属性,方便在不同的场景下使用。
8.2 组件通信方式
组件之间的通信是开发中的重要环节,常见的通信方式有:
-
props 传递
:父组件通过
props
向子组件传递数据和方法。
-
事件回调
:子组件通过触发事件,将数据传递给父组件。
-
状态管理库
:使用
Redux
、
MobX
等状态管理库,实现组件之间的全局状态共享。
8.3 高阶组件的应用
高阶组件(HOC)是一种函数,它接收一个组件作为参数,并返回一个新的组件。高阶组件可以用于代码复用、状态管理、性能优化等方面。例如:
function withMetaAndControls(WrappedComponent) {
return function MetaAndControlsComponent(props) {
// 可以在这里添加额外的逻辑
return <WrappedComponent {...props} />;
};
}
9. 总结与展望
9.1 技术总结
通过对 React Native 动画、布局、网络编程和组件开发等方面的介绍,我们了解到 React Native 提供了丰富的功能和工具,能够帮助开发者快速构建高质量的移动应用。在动画方面,通过合理的节点关系建立、事件绑定和原生层交互,实现了流畅的动画效果;在布局设计上,遵循布局建议可以使应用适应不同的屏幕尺寸;网络编程中,处理好异步操作、错误处理和离线策略可以提高应用的稳定性和可用性;组件开发中,遵循组件构建原则和使用合适的通信方式可以提高代码的可维护性和复用性。
9.2 未来发展趋势
随着技术的不断发展,React Native 也在不断演进。未来可能会出现以下发展趋势:
-
性能优化
:进一步优化动画性能和网络请求性能,提高应用的响应速度和流畅度。
-
新特性支持
:支持更多的原生功能和特性,如增强现实(AR)、虚拟现实(VR)等。
-
生态系统完善
:开发更多的第三方库和工具,丰富 React Native 的生态系统,提高开发效率。
9.3 开发者建议
对于开发者来说,要不断学习和掌握新的技术和方法,结合实际项目需求,合理运用 React Native 提供的功能和工具。在开发过程中,注重代码的质量和性能优化,遵循最佳实践,提高应用的稳定性和用户体验。同时,积极参与开源社区,与其他开发者交流分享经验,共同推动 React Native 的发展。
通过以上的总结和展望,希望开发者能够更好地掌握 React Native 技术,创造出更加优秀的移动应用。以下是整个 React Native 开发流程的 mermaid 流程图,涵盖动画、网络编程和组件开发等方面:
graph TD;
A[动画开发] --> B[节点关系建立];
B --> C[事件绑定];
C --> D[原生层交互];
D --> E[动画更新];
F[网络编程] --> G[异步操作];
G --> H[错误处理];
H --> I[离线策略];
J[组件开发] --> K[组件构建原则];
K --> L[组件通信方式];
L --> M[高阶组件应用];
N[应用开发] --> A;
N --> F;
N --> J;
O[性能优化] --> A;
O --> F;
O --> J;
这个流程图展示了 React Native 开发的主要环节以及它们之间的关系,开发者可以根据这个流程进行项目的开发和优化。
超级会员免费看
50

被折叠的 条评论
为什么被折叠?



