ReactNative 详解(一) 基础内容之视图相关

本文主要从前端的角度介绍 ReactNative 的用法,主要介绍 ReactNative 的基本用法(布局,样式,动画,手势,特殊API) 帮助大家快速掌握 ReactNaitve。而对于 ReactNative 封装的各种组件就不会过多深入了。(阅读建议:掌握 react 用法,熟悉前端)

视图层

ReactNative 本质上开发和前端没有任何区别,仅仅是用了 ReactNative 专用的标签(组件)和一套精简过的 css 来完成的绘制。常用的类比如下:

  • <div> -> <View>
  • <span> -> <Text>
  • <input>, <textarea> -> <TextInput>
  • <img> -> <Image>
  • 其他封装的组件

而,CSS 不再以 .css 文件的形式存在,而是变成一个通过 StyleSheet 创建的对象。引用的时候不再有 className='xxx' 的写法,而统一为:style={xxx}

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import * as React from 'react';
import {View, Text, StyleSheet} from 'react-native';
const Style = StyleSheet.create({
wrap: {
justifyContent: 'center',
height: '100%',
alignItems: 'center'
}
})
class App extends React.Component {
render() {
return (
<View style={Style.wrap}>
<Text>hello world</Text>
</View>
);
}
}

效果如下:

a.png

似不似很简单。

Style 样式

ReactNative 使用的是阉割版的 css,需要用 StyleSheet.create 来创建样式。没有样式文件一说,统统 js。大部分,常用的 css 样式你都可以在 ReactNative 中找到(注意写成驼峰形式)。这里列举几个和 css 表现不太一致的样式:

与 css 对比

差异如下:

  1. fontWeight:去掉了 bolder 属性;
  2. textAlignVertical:center 取代了 middle,并阉割了 baseline, sub 等值
  3. textDecorationLine:阉割了 overline, blink 取值
  4. textDecorationStyle: 阉割了 wavy 取值
  5. position:阉割了 static, fixed 取值 (注意:在 ReactNative 里面,absolute 定位不再相对于父辈非 static 的元素定位,变简单了,直接相对于父元素定位。)
  6. magrin: 只能定义一个参数,用以表示上、右、下、左4个方位的外补白
  7. marginHorizontal:CSS中没有对应的属性,相当于同时设置marginRight和marginLeft
  8. marginVertical:CSS中没有对应的属性,相当于同时设置marginTop和marginBottom
  9. padding 同 margin
  10. borderStyle:阉割了 none, hidden, double, groove, ridge, inset, outset 取值,且无方向分拆属性
  11. shadowColor:对应 CSS 中的 box-shadow 属性中的颜色定义
  12. shadowOffset:取值 {width: number, height: number} ,对应 CSS 中的 box-shadow 属性中的阴影偏移定义
  13. shadowRadius:在 CSS 中,阴影的圆角大小取决于元素的圆角定义,不需要额外定义
  14. shadowOpacity:对应 CSS 中的 box-shadow 属性中的阴影透明度定义
  15. transform: 写法变成数组,例如平移x,y [{translateX: number}, {translateY: number}]
  16. Flex 系列后面单独说
  17. overflow:阉割了 scroll, auto 取值
  18. elevation: Android 特有,css 无
  19. resizeMode:CSS 中没有对应的属性,可以参考 background-size 属性
  20. tintColor:iOS 特有 CSS中没有对应的属性,iOS 图像上特殊的色彩,改变不透明像素的颜色
  21. 其他各段特有属性,不再一一列举

取值:
在 ReactNative 中,没有 px 单位,统一为数字 width: 10 ,长度单位为 dp。
这样,就省略了有前端在移动端适配这一环节,当然如果你想用 fixedWidth 方式适配也不是不行,ReactNative 提供了获取相应的计算值方案,这里不深入讲解,有兴趣的可参考下面资料:

同时,width,height 支持百分比单位 (低版本,并不支持)。

还有一点要注意的就是,ReactNative 并不支持样式继承。
对于样式覆盖,我们可以给 style=[{}, {}] 一个数组,倒是可以。

flex 布局

在 ReactNative 不再有所谓的 display 属性。除了 position,布局上采用 flexbox 布局。熟悉前端的大家一定不陌生。但是在 ReactNative 中,默认的 flexDirection 方向为 column 纵向。且相应的属性仍有不少删减:

b.png

常用组件差别

我们来看下,标签(组件)的差别。
在 ReactNative 中,文本必须被包裹在 <Text> 中(而在前端这并不是强制的,但是我推荐文本至少要包裹在 span 之类的内联元素中,而不是暴露在 div 中,当初了拖拽选蓝的时候,你会发现这个规则非常的有帮助)
前端常用的 input, textarea 处理输入,到了 ReactNative 统一为 TextInput 组件。

TextInput

默认为单行输入框,这就和 input 是一样的了, 大部分属性和前端一致。
列举几个不一样的常用属性:

  • editable:如果为false,文本框是不可编辑的。默认值为true。
  • keyboardType:决定弹出何种软键盘类型,譬如numeric(纯数字键盘)
  • placeholderTextColor:占位字符串(placeholder)颜色
  • secureTextEntry:密码输入框效果
  • selection:取值 {start: number,end: number},设置选中文字的范围(指定首尾的索引值)。如果首尾为同一索引位置,则相当于指定光标的位置
  • selectTextOnFocus:如果为true,当获得焦点的时候,所有的文字都会被选中
  • autoFocus:如果为true,在componentDidMount后会获得焦点。默认值为false

可以看出,ReactNative 上文本输入框能力的强大,许多焦点设置都有对应的属性来实现,同时还额外提供了输入修正等能力。有兴趣的可以看文档。

那么,如果我想要多行文本输入 textarea 呢?
我们可以通过设置:multiline = true 来实现,同时我们还可以指定输入框最大行数:numberOfLines。另外对于多行文本输入,还提供了根据输入内容,自动转换成可点击URL的设定:dataDetectorTypes

有了这些,我们还需要获得和设置文本框的值。设置我们可以通过 value 属性。但是获取就没有那么容易了,在 ReactNative 中,TextInput 最终会被转换成 Native 原生节点。并不是DOM节点,所以我们无法通过 ref 手段来像前端那样直接获取节点属性。所以我们必须采用受控组件的方式,来通过 state 来获取值。同样我们也可以监听 onChangeText 事件,来实时得到文本变化的内容 (这也是唯一的获取手段)。

TextInput 支持事件主要有:

  • onChange:当文本框内容变化时调用此回调函数。回调参数为{ nativeEvent: { eventCount, target, text} }
  • onChangeText:onChange 的简化版,只有 text
  • onEndEditing:当文本输入结束后调用此回调函数
  • onKeyPress:当一个键被按下的时候调用此回调。传递给回调函数的参数为{ nativeEvent: { key: keyValue } },其中keyValue即为被按下的键。会在onChange之前调用。注意:在Android上只有软键盘会触发此事件,物理键盘不会触发。
  • onSubmitEditing:此回调函数当软键盘的确定/提交按钮被按下的时候调用此函数,所传参数为{nativeEvent: {text, eventCount, target}}。如果multiline={true},此属性不可用。

同样还有一些和焦点,选蓝有关的事件,也不一一介绍了。总之很全面,嘿嘿。

在前端,我们还会关注一个问题就是,键盘弹起遮挡当前内容。这个可以通过使用 KeyboardAvoidingView 组件来解决。
当然了,熟悉 Android 的,我们也可以在 native 端设置 android:windowSoftInputMode="adjustPan" 来解决哦。

键盘遮挡问题参考:

Image

基本格式:

1
<Image source={xxx}>

和前端不同,在 ReactNative 中,如果使用静态图片地址,地址必须是静态字符串,不能包含变量。如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 正确
<Image source={require('./my-icon.png')} />;
// 错误
var icon = this.props.active ? 'my-icon-active' : 'my-icon-inactive';
<Image source={require('./' + icon + '.png')} />;
// 正确
var icon = this.props.active
? require('./my-icon-active.png')
: require('./my-icon-inactive.png');
<Image source={icon} />;

这主要是因为,require 时在编译时期执行,而非运行使其执行。

如果加载网络图片,格式如下:

1
2
<Image source={{uri: 'https://facebook.github.io/react/logo-og.png'}}
style={{width: 400, height: 400}} />

注意,这里必须给图片尺寸(静态资源不需要)。同样 uri 后面可以跟,base64 的资源。

ReactNative 还提供了对网络图片,更加精细的加载请求(设置HTTP头部等),不过多介绍。
另外,对于混合资源等细节,请参阅文档。

思考一个问题,如果我们想加载一个背景图片呢?ReactNative 提供了 ImageBackground 组件来解决这一问题。

1
2
3
<ImageBackground source={...} style={{width: '100%', height: '100%'}}>
<Text>Inside</Text>
</ImageBackground>

想了想,常用的视图有关就这么多了呢。剩下那些扩展组件,需要使用哪儿个查哪儿个就好了。

改变视图

有了布局等内容,我们现在思考如何改变某一部分样式。在前端改变 css 或者 style 即可。而在 ReactNative 中,第一种修改方式就是讲组件样式作为 state 来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const Style = StyleSheet.create({
style1: {
width: 50,
height: 50,
backgroundColor: '#ff0000'
},
style2: {
width: 50,
height: 50,
backgroundColor: '#00ff00'
}
});
class App extends React.Component {
constructor(props) {
this.state = {
style: Style.style2
}
}
render() {
return (
<View>
<View style={this.state.style}></View>
<Button onPress={()=>{this.setState({style: Style.style2})}}/>
</View>
);
}
}

但是这里有个问题,我们知道触发 setState 会导致树 re-render。频繁的改变 setState 就会引起页面卡顿。这里我们就需要一种像直接操作 DOM 的方案,来改变一个组件的样式。

setNativeProp

在 React Native 中,setNativeProps就是等价于直接操作 DOM 节点的方法。setNativeProps 一般来说只是用来创建连续的动画,同时避免渲染组件结构和同步太多视图变化所带来的大量开销。
通过 ref 获得组件的引用,然后就可以直接使用了。距离如下:

1
2
3
4
5
// React16 语法
this.viewRef.current.setNativeProps({
width: 100,
height: 100
});

这里要注意,setNativeProps 只能直接作用在 RN 组件上,也就是说不能作用在自定义组件上面(这时候,请考虑 React.forwardRef 来转发 ref)。

setNativeProps 还可以用来直接改变 TextInput 的值:

1
this.textRef.setNativeProps({text: ''});

获取节点样式信息

在前端有时候我们需要获取元素的宽高,在屏幕中的位置信息等。这个要怎么计算呢?

屏幕宽高 Dimensions

获取屏幕宽高,React Native 提供了直接的获取方法:

1
let {width, height, scale, fontScale} = Dimensions.get('window');

同样,如果我们想监听屏幕变化,Dimensions.addEventListener('change', callback) 即可。

组件位置和大小

1.onLayout 事件属性

1
2
3
4
5
_onLayout(event){
let {x,y,width,height} = e.nativeEvent.layout
}
<View onLayout={(e) => this._onLayout}></View>

当组件重新渲染时,该方法就能重新获取到元素的宽高和位置信息,但是有时组件并没有重新render那么就获取不到正确的值,例如页面滚动,但是state没有发生变化,组件也就没有重新渲染。

2.元素自带measure方法

1
<View ref={(view) => this.myView = view}></View>

然后需要注意的是需要在componentDidMount方法里面添加一个定时器,定时器里面再进行测量,否则拿到的数据都为0.

1
2
3
4
5
6
7
componentDidMount(){
setTimeOut(() => {
this.myView.measure((x, y, width, height, left, top) => {
//todo
})
});
}

注意不能用在自定义组件上面哦,但是下面这个方法可以用在自定义组件上面。

3.使用UIManager measure方法
首先引入:

1
2
3
4
import {
UIManager,
findNodeHandle
} from 'react-native';

再加入:

1
<MyComponent ref={this.myComponent} />

最后测量一下即可:

1
2
3
UIManager.measure(findNodeHandle(this.myComponent),(x,y,width,height,pageX,pageY)=>{
//todo
})

同样的,这个也不能在 componentDidMount 后立即计算。

Animated 动画

在动画方面,ReactNative 使用 Animated 来做动画。Animated仅封装了四个可以动画化的组件:View、Text、Image和ScrollView。(Animated.createAnimatedComponent() 可以封装自定义组件),举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import {Animated} from 'react-native';
class App extends React.Component {
constructor(props) {
super(props);
// 一个动画变量
this.scaleValue = new Animated.Value(0);
}
fadeIn() {
let fade = Animated.timing(
this.scaleValue,
{
toValue: 1,
duration: 2000
}
);
// 也可以 fade.stop
fade.start();
}
render() {
return (
<View>
<Animated.View
style={{
width: 50,
height: 50,
transform: [{scale: this.scaleValue}],
backgroundColor: '#2ede2e',
}}></Animated.View>
<Button
title="改变样式"
onPress={() => {
this.fadeIn();
}}/>
</View>
);
}
}

Animated 主要是以声明的形式来定义动画的输入与输出 (Animated.Value 变量),在其中建立一个可配置的变化函数,然后使用简单的 start/stop 方法来控制动画按顺序执行。

Animated 提供的动画类型:

  • Animated.decay()以指定的初始速度开始变化,然后变化速度越来越慢直至停下。
  • Animated.spring()提供了一个简单的弹簧物理模型.
  • Animated.timing()使用easing 函数让数值随时间动起来。

同时还提供了组合动画的方式:

  • Animated.delay(time) 在给定延迟后开始动画。
  • Animated.parallel(animations, config?) 同时启动多个动画。
  • Animated.sequence(animations) 按顺序启动动画,等待每一个动画完成后再开始下一个动画。
  • Animated.stagger(time, animations) 按照给定的延时间隔,顺序并行的启动动画。
  • Animated.loop(animation, config?) 无限循环一个指定的动画

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Animated.sequence([
// decay, then spring to start and twirl
Animated.decay(position, {
// coast to a stop
velocity: { x: gestureState.vx, y: gestureState.vy }, // velocity from gesture release
deceleration: 0.997
}),
Animated.parallel([
// after decay, in parallel:
Animated.spring(position, {
toValue: { x: 0, y: 0 } // return to start
}),
Animated.timing(twirl, {
// and twirl
toValue: 360
})
])
]).start(); // start the sequence group

同事,对于 Aniamted.Value 还提供了加减乘除以及取余运算,插值器等,需要的自己看吧。

动画有点卡,试试 useNativeDriver

我在模拟器上,改变动画确实卡的要命。这个 RN 同样提供了一个方案:启动原生动化。在动画中启用原生驱动非常简单。只需在开始动画之前,在动画配置中加入一行useNativeDriver: true,如下所示:

1
2
3
4
5
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true // 加上这一行
}).start();

Animated的 API 是可序列化的(即可转化为字符串表达以便通信或存储)。通过启用原生驱动,我们在启动动画前就把其所有配置信息都发送到原生端,利用原生代码在 UI 线程执行动画,而不用每一帧都在两端间来回沟通。如此一来,动画一开始就完全脱离了 JS 线程,因此此时即便 JS 线程被卡住,也不会影响到动画了。

然而,并不是所有的属性都可以启用原生动画,只有 non-layout 类型的属性才可以。那这就很尴尬了,我想要实现一个折叠面板,也卡的我难受。咋弄呢?

LayoutAnimation

它常用来更新 flexbox 布局,因为它可以无需测量或者计算特定属性就能直接产生动画。(感觉和 Android 的 <LayoutAnimation> 有关呢)。使用也十分的简单:

1
2
3
4
_changeView() {
LayoutAnimation.spring();
this.setState({w: this.state.w + 15, h: this.state.h + 15})
}

只需要在改动之前,调用一下 LayoutAnimation 即可。同样,LayoutAnimation 也提供了其他动画方式(easeInEaseOut, linear, spring),和动画的基础配置。
这里有一点要注意的是,如果在 Android 需要加入如下内容:

1
2
3
// 在执行任何动画代码之前,比如在入口文件App.js中执行
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);

除了这些,ReactNative 还提供了滚动,滑动等事件值映射到动画值上面。Animated.Event 这样就可以将动画和手势结合起来。

总结

其实 RN 的动画效果并不是很好,我们看见只有动画脱离了 JS (useNativeDriver) 才好点,这其中和 js 到 native 的频繁通信有很大的关系。脱离通信就好多了。
联系到,如果我们想做一些,复杂的交互动画效果,同样会产生问题。频繁的通信,序列化与反序列化。
这里推荐一篇文章,其解决思路可以参考: