这两天尝试用 React Native 写了一个 APP,总体感觉是个好东西。目前没有 macOS 环境,没法测试 iOS 上的表现,但是安卓上是不错的。
在设计界面的过程中,如何正确排版、正确使用 FlexBox 模型就成了一个关键问题。其实这里的 FlexBox 跟 CSS3 中新加的那个 FlexBox (Flexible Box) 基本上是一样的,所以通过 MDN 上的这篇文章可以了解 FlexBox 的基础知识,例如主轴和侧轴、每个轴的元素排列等。另外,SegmentFault 上有一篇关于 React Native 布局的文章也不错,介绍了 FlexBox 排列元素时,遇到多个 flex 元素或有固定大小的(不 flex 的)元素,会怎样安排他们的大小。
假定读者阅读了上面两篇文章,明白了基础知识。接下来我结合 APP 中的实例来讲一下使用 FlexBox 过程中遇到的问题、解决方法和对 FlexBox 模型的理解。
考虑如图所示的 UI,上面是一个固定高度的标题栏,其中包含左右各一个固定大小的按钮,中间的文字填满剩余空间;下面是很长的内容,可以上下滚动,滚动时标题栏一直在最上面不动。
首先整个界面的顶层 <View> 标签,其 style 应该包括:{flex: 1, flexDirection: 'column',}
,这样内部元素就会上下排布。标题栏我额外定义了一个 Header 类(class Header extends Component {}
),因此 <View> 的第一个子元素就是 <Header style={...}>(标题栏子元素)</Header>
。这里要注意的是,给 Header 定义 style 本身没有任何效果,应该在 Header 类的 render() 方法中,将其实际顶级元素的 style 附加上 this.props.style,即 <View style={[header_styles.basic, this.props.style]}>...</View>
。
然后看 <Header> 对应的 <View> 的 style。由于 <Header> 本身是固定高度的,因此可以肯定它不是 {flex: 1} 的。但是我们又想使用 FlexBox 中的排版功能,对标题栏中的元素横向排版,所以在顶层 <View> 中再嵌套一层 <View>,它的 style 应该是这样的:
container: {
flex: 1,
flexDirection: 'row', // 子元素横向分布
justifyContent: '', // 这个无所谓,因为子元素要占满整行
alignItems: 'center', // 竖向居中线排列
},
接下来看内层 <View> 的子元素。首先是左上角、右上角的图标,这里我用的是 react-native-vector-icons,就是两个 <Icon> 元素,它们都是固定大小的(其实就是一个特殊字符),因此我们需要中间的标题 <Text style={{flex: 1}}> 以填满剩余空间。这里就看出来,{flex: 1} 的意思就是这个元素的长宽可以任意放大(或缩小),以适应排版的需要。
这样 <Header> 部分就搞定了,接下来看正文的滚动部分,这里用的是 React Native 的 <ScrollView> 元素。通过文档中看出,ScrollView 是由一个外层短元素包含一个内层长容器,外层短元素可以是固定高度(height)的或 flex 填满剩余高度的,但内层容器应该是内容的实际高度,而不应该是 flex 的。这里文档写的很迷茫(“把 flex 从视图栈向下传递”,并没有看明白哪里是栈、哪里是下……),我是踩了很多坑才搞明白这是什么意思。主要代码如下:
<ScrollView style={{flex: 1}}
contentContainerStyle={{...}}>
<View>...很长的内容...</View>
</ScrollView>
这个 <ScrollView> 在顶层 flex <View> 中,将其设为 {flex: 1} 即可填满除 <Header> 以外的剩余屏幕高度。但是 contentContainerStyle 控制的内层样式绝不应该有 {flex: 1},否则会被缩小到与外层元素一样高,裁剪掉多余的内容,因此无法实现滚动。这里与 <Header> 中类似,内部长内容可能还有更复杂的排版(比如进一步上下/左右分割),所以可以在 <View> 子元素的 style 上下功夫,比如设为 {flex: 1, flexDirection: 'row',}
即可将内部元素再左右排列。
下面提供一些我的 APP 中实现上述布局的代码(省略了逻辑和内容,只保留排版相关的代码,最终效果类似于 Material Design),供读者参考:
class Header extends Component {
render() {
return <View style={header_styles.header}>
<View style={header_styles.container}>
{this.props.children}
</View>
</View>;
}
}
const header_styles = StyleSheet.create({
header: {
backgroundColor: "#2196F3",
height: 60,
paddingLeft: 20,
paddingRight: 20,
shadowRadius: 2,
shadowOffset: {width:0, height:2},
shadowOpacity: 0.7,
shadowColor: 'black',
elevation: 2,
},
container: {
flex: 1,
flexDirection: 'row',
flexWrap: 'nowrap',
justifyContent: 'flex-start',
alignItems: 'center',
},
});
class Single extends Component {
render() {
return <View style={single_styles.container}>
<Header>
<TouchableOpacity>
<Icon
name="arrow-left"
size={20}
color="#E3F2FD"
/>
</TouchableOpacity>
<Text style={single_styles.title} numberOfLines={2}>
...
</Text>
(注意:我的标题栏中只用了左侧图标,没有右侧图标。)
</Header>
<ScrollView style={single_styles.scroll}
contentContainerStyle={single_styles.scroll_container}>
<View>
...
</View>
<View style={single_styles.list}>
<SingleLeft />
<SingleRight />
</View>
</ScrollView>
</View>;
}
}
const single_styles = StyleSheet.create({
container: {
flex: 1,
},
scroll_container: {
},
list: {
flexDirection: 'row',
alignItems: 'flex-start',
},
scroll: {
flex: 1,
},
title: {
fontSize: 20,
color: '#E3F2FD',
marginLeft: 10,
flex: 1,
flexWrap: 'wrap',
},
});