界面开发
Styles
在讲解 React Native 的样式之前,需要先介绍下 FaceBook 本身推荐的模式,它强烈的推荐重用带样式的组件而不是样式本身。并且 React Native 是不支持样式继承的,如它在文档中所述:
你并没有被允许去为整个节点树设置一个默认的譬如字体这样的属性,推荐的方式是通过创建一个带有一致的字体样式的组件并且在整个应用中通过重用组件的方式来完成样式的统一与继承。
在 Web 的 React 开发中,往往会使用独立的样式文件,譬如 CSS、SASS 或者 LESS 样式表来存储声明的样式。但是 React Native 使用了一套截然不同的机制,所有的样式必须要用 JavaScript 进行编写(当然,后面也会提到有第三方库可以帮你自动编译 SASS 到 JS 中),并且强制规定所有的样式必须显性引用到组件的 Style 对象中。毫无疑问,这种方式会银帆很多不良的反应,毕竟它与目前流行的基于 CSS 的样式方式相悖。 传统的独立的 CSS 毫无疑问存在着一些问题,譬如所有的 CSS 的规则与类名都是处于全局作用域中,也就是说虽然只打算为某个组件添加样式也是有可能无意间影响到其他的组件。譬如如果你引入了 Twitter 最新的 BootStrap 的样式库,等于你引入了 600 多个全局变量。因为 CSS 没有显式地关联到 HTML 元素,也就给冗余或者无效代码地消除带来了很大的困难。
Declaring & Manipulating Styles
类似于 SASS 或者 LESS 这样的 CSS 预处理器也在尝试着解决 CSS 中不好的部分,但是还是有很多根本性的问题遗留下来。React Native 实现了部分可用的 CSS 的样式,专注于保证样式的 API 较少但是可用度高。譬如 Position 相关的属性就差别很大,此外,React Native 中也不支持伪类、动画与选择器。所有支持的属性可以查看这里。
Inline Styles(内联样式)
内联样式是句法上最简单的使用方法,不过很明显并不是最佳的使用方法,其语法形式类似于 React Web:
<Text>
The quick <Text style={{fontStyle: "italic"}}>brown</Text> fox
jumped over the lazy <Text style={{fontWeight: "bold"}}>dog</Text>.
</Text>
内联样式可以让你快速地实验,不过,这种方式还是要尽可能地避免,毕竟这种方式会非常的低效。内联样式对象可能在每次渲染的时候被重新创建。即使你有可能在 Props 或者 State 发生变化时修正样式值,也要避免使用内联样式。
Styling With Objects
对于内联样式最简单的修正就是将这些样式对象提取出来,作为单独的 JS 对象:
var italic = {
fontStyle: 'italic'
};
var bold = {
fontWeight: 'bold'
};
...
render() {
return (
<Text>
The quick <Text style={italic}>brown</Text> fox
jumped over the lazy <Text style={bold}>dog</Text>.
</Text>
);
}
不过,ReactNative 官方是推荐我们使用StyleSheet.create
来创建一个样式对象,而不是仅仅使用简单的 JS 的朴素对象。StyleSheet.create
是可选的一种方式但是能够提供很多的好处,它通过将样式对象转化为一个内部表的引用来保证了值是不可变对象并且是不透明的。将它放置到文件末尾,可以保证在整个应用的生命周期中只会创建一次而不是每次渲染的时候都会重新创建。由此可见,StyleSheet.create
是一点语法糖,同时,也方便了你进行 Prop 的类型的验证。通过StyleSheet.create
创建的样式对象可以用 View.propTypes.Style 与 Text.propTypes.Style 类型进行验证。
Style Concatenation
虽然 React 强调了重用带样式的组件而不是样式本身,但是很多时候不可避免的会重用样式。譬如,如果你已经定义了一个叫 button 的样式和一个叫 accentText 的样式,但是需要将它们合并到一个叫 AccentButton 的组件中,最初定义的样式如下所示:
var styles = Stylesheet.create({
button: {
borderRadius: '8px',
backgroundColor: '#99CCFF'
},
accentText: {
fontSize: '18px',
fontWeight: 'bold'
}
});
然后希望创建一个对象同时用这两个属性:
var AccentButton = React.createClass({
render: function() {
return (
<Text style={[styles.button, styles.accentText]}>
{this.props.children}
</Text>
);
}
});
可以看出,这个 style 属性可以接收一个数组作为对象,也可以将自定义的内联样式放进去:
var AccentButton = React.createClass({
render: function() {
return (
<Text style={[styles.button, styles.accentText, {color: '#FFFFFF'}]}>
{this.props.children}
</Text>
);
}
});
如果存在冲突的话,就是为一个属性定义了两个值的时候,React Native 会自动解决该冲突,它会自动选择数组中最右边的定义作为有效定义,并且错误值,也就是 false,null,undefined 这些值会被忽略。这个特性可以被用于处理条件样式,譬如,如果有个<Button />
组件,希望能在被触摸时展示额外的样式,则可以使用:
<View style={[styles.button, this.state.touching && styles.highlight]} />
Organization and Inheritance
在一个大型的项目中会存在着大量的组件以及各种各样的样式定义,而这时候我们就需要来考虑怎么组织编排这些样式文件。
Exporting Style Objects
随着样式定义的日渐复杂,我们会希望样式文件独立于主的 JS 文件。一个常用方法就是为每个组件建立一个独立的文件夹。如果我们的组件叫<ComponentName />
,可以创建一个叫 ComponentName 的文件夹,如下所示:
- ComponentName
|- index.js
|- styles.js
而在 styles.js 中,可以创建一个样式表,然后将它暴露出来:
'use strict';
var React = require('react-native');
var {
StyleSheet,
} = React;
var styles = Stylesheet.create({
text: {
color: '#FF00FF',
fontSize: 16
},
bold: {
fontWeight: 'bold'
}
});
module.exports = styles;
在 index.js 中,只需要将样式导入即可:
var styles = require('./styles.css');
接下来就可以在我们的组件中使用了:
'use strict';
var React = require('react-native');
var styles = require('./styles.css');
var {
View,
Text,
StyleSheet
} = React;
var ComponentName = React.createClass({
render: function() {
return (
<Text style={[styles.text, styles.bold]}>
Hello, world
</Text>
);
}
});
Passing Styles as Props
You can also pass styles as properties. The propType View.propTypes.style
ensures that only valid styles are passed as props.
You can use this pattern to create extensible components, which can be more effectively controlled and styled by their parents. For example, a component might take in an optional style prop:
'use strict';
var React = require('react-native');
var {
View,
Text
} = React;
var CustomizableText = React.createClass({
propTypes: {
style: Text.propTypes.Style
},
getDefaultProps: function() {
return {
style: {}
};
},
render: function() {
return (
<Text style={[myStyles.text, this.props.style]}>Hello, world</Text>
);
}
});
Reusing and Sharing Styles
We typically prefer to reuse styled components, rather than reusing styles, but there are clearly some instances in which you will want to share styles between components. In this case, a common pattern is to organize your project roughly like so:
- js
|- components
|- Button
|- index.js
|- styles.js
|- styles
|- styles.js
|- colors.js
|- fonts.js
By having separate directories for components and for styles, you can keep the intended use of each file clear based on context. A component’s folder should contain its React class, as well as any component-specific files. Shared styles should be kept out of component folders. Shared styles may include things such as your palette, fonts, standardized margin and padding, and so on.
styles/styles.js
requires the other shared styles files, and exposes them; then your components can require styles.js
and use shared files as needed. Or, you may prefer to have components require specific stylesheets from the styles/
directory instead.
Because we’ve now moved our styles into Javascript, organizing your styles is really a question of general code organization; there’s no single correct approach here.
Css Polyfill
很多时候我们 Web 的知识并不能直接应用到 React Native 中,本部分对于常用的 Web 技巧在 React Native 上的具体应用做一个说明。
Utils(辅助类)
Media Query
在 CSS 种,Media Query 是非常常用的技巧之一,而在 React Native 种,并不能够直接运用上 CSS 的一些属性,不过可以使用如下方式:
var Dimensions = require('Dimensions');
var { width, height } = Dimensions.get('window');
Background
BackgroundImage
背景图片是在 Web 之中常用的属性,往往我们需要一个自动填充满的背景图片,其效果即:
var styles = StyleSheet.create({
bgImageWrapper: {
position: "absolute",
top: 0,
bottom: 0,
left: 0,
right: 0,
},
bgImage: {
flex: 1,
resizeMode: "stretch",
},
welcome: {
fontSize: 20,
textAlign: "center",
margin: 10,
},
});
<View style={{ flex: 1 }}>
<View style={styles.bgImageWrapper}>
<Image source={require("image!background")} style={styles.bgImage} />
</View>
<Text style={styles.welcome}>Welcome to React Native!</Text>
</View>;
SASS
Animation
Graphic
Shapes
Layout & Position
One of the biggest changes when working with styling in React Native is positioning. CSS supports a proliferation of positioning techniques. Between float
, absolute positioning, tables, block layout, and more, it’s easy to get lost! React Native’s approach to positioning is more focused, relying primarily on flexbox
as well as absolute positioning, along with the familiar properties of margin
and padding
. In this section, we’ll look at how layouts are constructed in React Native, and finish off by building a layout in the style of a Mondrian painting.
Flexbox
Flexbox is a CSS3 layout mode. Unlike existing layout modes such as block
and inline
, flexbox gives us a direction-agnostic way of constructing layouts. (That’s right: finally, vertically centering is easy!) React Native leans heavily on flexbox. If you want ot read more about the general specification, the MDN documentation is a good place to start.
With React Native, the following props are related to flexbox:
- flex
- flexDirection
- flexWrap
- alignSelf
- alignItems
Additionally, these related values impact layout:
- height
- width
- margin
- border
- padding
If you have worked with flexbox on the web before, there won’t be many surprises here. Because flexbox is so important to constructing layouts in React Native, though, we’ll spend some time now exploring how it works.
The basic idea behind flexbox is that you should be able to create predictably structured layouts even given dynamically sized elements. Since we’re designing for mobile, and need to accommodate multiple screen sizes and orientations, this is a useful feature.
We’ll start with a parent ``, and some children.
<View style={styles.parent}>
<Text style={styles.child}> Child One </Text>
<Text style={styles.child}> Child Two </Text>
<Text style={styles.child}> Child Three </Text>
</View>
To start, we’ve applied some basic styles to the views, but haven’t touched the positioning yet.
var styles = StyleSheet.create({
parent: {
backgroundColor: '#F5FCFF',
borderColor: '#0099AA',
borderWidth: 5,
marginTop: 30
},
child: {
borderColor: '#AA0099',
borderWidth: 2,
textAlign: 'center',
fontSize: 24,
}
});
Next, we will set flex on both the parent and the child. By setting the flex property, we are explicitly opting-in to flexbox behavior. flex takes a number. This number determines the relative weight each child gets; by setting it to 1 for each child, we weight them equally. We also set flexDirection: column so that the children are laid out vertically. If we switch this to flexDirection: row, the children will be laid out horizontally instead.
var styles = StyleSheet.create({
parent: {
flex: 1,
flexDirection: 'column',
backgroundColor: '#F5FCFF',
borderColor: '#0099AA',
borderWidth: 5,
marginTop: 30
},
child: {
flex: 1,
borderColor: '#AA0099',
borderWidth: 2,
textAlign: 'center',
fontSize: 24,
}
});
If we set alignItems, the children will no longer expand to fill all available space in both directions. Because we have set flexDirection: row, they will expand to fill the row. However, now they will only take up as much vertical space as they need. Then, the alignItems value determines where they are positioned along the cross-axis. The cross-axis is the axis orthogonal to the flexDirection. In this case, the cross axis is vertical. flex-start places the children at the top, center centers them, and flex-end places them at the bottom. Let’s see what happens when we set alignItems:
var styles = StyleSheet.create({
parent: {
flex: 1,
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: '#F5FCFF',
borderColor: '#0099AA',
borderWidth: 5,
marginTop: 30
},
child: {
flex: 1,
borderColor: '#AA0099',
borderWidth: 2,
textAlign: 'center',
fontSize: 24,
}
});
Absolute Position
In addition to flexbox, React Native supports absolute positioning. It works much as it does on the web. You can enable it by setting the position property: position: absolute Then, you can control the component’s positioning with the familiar properties of left, right, top, and bottom. An absolutely positioned child will apply these coordinates relative to its parent’s position, so you can lay out a parent element using flexbox and then use absolute position for a child within it. There are some limitations to this. We don’t have z-index, for instance, so layering views on top of each other is a bit complicated. The last view in a stack typically takes precedence. Absolute positioning can be very useful. For instance, if you want to create a container view that sits below the iOS status bar, absolute positioning makes this easy:
container: {
position: 'absolute',
top: 30,
left: 0,
right: 0,
bottom: 0
}
Mixed Example
Let’s try using these positioning techniques to create a more complicated layout. Say we want to mimic a Mondrian painting. Here’s the end result:
To start with, we create a parent style to act as the container. We will use absolute positioning on the parent, because it’s most appropriate: we want it to fill all available space, except with a 30 pixel offset at the top, due to the iOS status bar. We’ll also set its flexDirection to column.
parent: {
flexDirection: 'column',
position: 'absolute',
top: 30,
left: 0,
right: 0,
bottom: 0
}
Looking back at the image, we can divide the layout up into larger blocks. These divisions are in many ways arbitrary, so we’ll pick an option and roll with it. Here’s one way we can segment the layout: We start by cutting the layout into a top and bottom block:
<View style={styles.parent}>
<View style={styles.topBlock}>
</View>
<View style={styles.bottomBlock}>
</View>
</View>
Then we add in the next layer. This includes both a “left column” and “bottom right” sector, as well as the actual
<View style={styles.parent}>
<View style={styles.topBlock}>
<View style={styles.leftCol}>
</View>
<View style={[styles.cellThree, styles.base]} />
</View>
<View style={styles.bottomBlock}>
<View style={[styles.cellFour, styles.base]}/>
<View style={[styles.cellFive, styles.base]}/>
<View style={styles.bottomRight}>
</View>
</View>
</View>
The final markup contains all seven cells:
<View style={styles.parent}>
<View style={styles.topBlock}>
<View style={styles.leftCol}>
<View style={[styles.cellOne, styles.base]} />
<View style={[styles.base, styles.cellTwo]} />
</View>
<View style={[styles.cellThree, styles.base]} />
</View>
<View style={styles.bottomBlock}>
<View style={[styles.cellFour, styles.base]}/>
<View style={[styles.cellFive, styles.base]}/>
<View style={styles.bottomRight}>
<View style={[styles.cellSix, styles.base]} />
<View style={[styles.cellSeven, styles.base]} />
</View>
</View>
</View>
Now let’s add the styles that make it work. (Show stylesheet.)··
// Mondrian/style.js
'use strict';
var React = require('react-native');
var {
StyleSheet,
} = React;
var styles = StyleSheet.create({
parent: {
flexDirection: 'column',
position: 'absolute',
top: 30,
left: 0,
right: 0,
bottom: 0
},
base: {
borderColor: '#000000',
borderWidth: 5
},
topBlock: {
flexDirection: 'row',
flex: 5
},
leftCol: {
flex: 2
},
bottomBlock: {
flex: 2,
flexDirection: 'row'
},
bottomRight: {
flexDirection: 'column',
flex: 2
},
cellOne: {
flex: 1,
borderBottomWidth: 15
},
cellTwo: {
flex: 3
},
cellThree: {
backgroundColor: '#FF0000',
flex: 5
},
cellFour: {
flex: 3,
backgroundColor: '#0000FF'
},
cellFive: {
flex: 6
},
cellSix: {
flex: 1
},
cellSeven: {
flex: 1,
backgroundColor: '#FFFF00'
}
});
module.exports = styles;