看过我前面文章的朋友们现在应该能正常运行自己的第一个RN应用了,那都是小儿科,现在我们来做点进阶一点的东西。这篇文章有一些属于干货性的东西,请仔细阅读。特别需要注意我加粗的部分。
首先我们来看下js文件结构,在项目初始化成功后,根目录下有2个js文件,index.android.js和index.ios.js,这2个文件分别是android和ios的入口文件。这里我简单说下RN对js文件的命名约束,如果你开发的文件只用于android系统,就需要存成.android.js文件,如果是只用于ios系统,就需要存成.ios.js文件。如果是2个系统通用的,就只要存成.js就行了,系统再编译时会根据你的编译选项,只打包对应的js文件。由于我现在只作安卓应用,所以我写的js文件,不管是不是安卓专用的,我都保存成了.js文件,就不再加android前缀了,请大家注意。而且我新建了一个src目录,我自己写的js组件都放在此目录下。整个项目目录结构如下:
HelloWorld
-__tests__
-android
-ios
-node_modules
-src
-images
icon.png
-components
-CustomText.js
-app.js
-index.js
-home.js
-find.js
-user.js
-.babelrc
-.buckconfig
-.flowconfig
-.gitattributes
-.gitignore
-.watchmanconfig
-app.json
-index.android.js
-index.ios.js
-package.json
先修改下index.android.js,将内容改成:
require('./src/app');
并把原来的index.android.js中的代码拷贝到src/app.js中。接下来我们所有的js代码编写都将在src目录下进行。另外开发过程中我们时刻要时刻关注下package server是否报错停止,如果停止就在窗口中运行react-native start以重新启动改服务。
自定义组件
reactjs之所以大受欢迎,其中一个很重要的原因就是其组件化设计思想,虽然angularjs通过指令也可以实现其组件化设计思想,但还是没有reactjs来的优雅(原谅我有点逼格的用了这个词汇)。RN源自于reactjs,自然也继承了其组件化的设计。其实自定义组件本来很简单,没什么特别要讲的,不过我这里有个特殊用途 所以就单独拿出来说一下吧。
RN中是没有全局字体属性可以设置的,所以如果我们要统一设定字体属性,还是比较麻烦的,网上也有一些方案大家可以搜搜,我这里就用一个自定义Text组件来实现全局修改字体属性,主要就是fontSize属性和fontFamily属性。我们将这个组件命名为CustomText.js,存放在components目录下。
CustomText.js
import React, { Component } from 'react';
import {
Text
} from 'react-native';
class CustemText extends Component
{
constructor(props){
super(props);
}
render(){
let styles = {
fontSize:12,
color:'#000'
}
for(let item in this.props){
if(item !== 'label'){
styles[item] = this.props[item];
}
}
return (<Text style={styles}>{this.props.label}</Text>)
}
}
export default CustemText
在app.js中使用,请注意,如果属性为数字或者bool值,需要写在大括号中,比如fontSize属性,如果为字符串,则直接书写即可,比如color和label属性。
...
import CustomText from './components/CustomText';
...
export default class HelloWorld extends Component {
render() {
return (<View>
<CustomText fontSize={18} color='#ccc' label='字号18文字'/>
<CustomText color='blue' label='蓝色文字'/>
</View>)
}
...
使用自定义字体文件
这里我们结合你可能会用到的矢量字体库react-native-vector-icons来讲。首先我们打开命令行,切换到项目根目录下,输入:
npm install --save-dev react-native-vector-icons
安装完成后,请注意,需要把node_modules\react-native-vector-icons\Fonts目录下的所有字体文件拷贝到android\app\src\main\assets\fonts目录下,如果没有该目录,请自行创建。所有你需要使用自定义的字体都需要拷贝到该目录下。
使用该模块很简单,比如我们需要加载FontAwesome矢量字体,则这么引用:
...
import Icon from 'react-native-vector-icons/FontAwesome';
...
export default class HelloWorld extends Component {
render() {
return (<View>
<Icon name='user' size={25}/>
</View>)
}
}
...
使用本地图片
使用网络图片比较简单,直接引用URI地址即可,使用本地图片则需要特别说明下,因为网上很多资料是错误的。引用本地图片有2种方式:
1:根据facebook的建议,本地图片建议放到js文件相对目录下,比如你可以在src目录下再建一个images目录,然后把你的图片放到该目录下。引用的话比较简单,比如你在app.js中引用images目录下的icon.png文件,你可以这么写:
...
import Icon from 'react-native-vector-icons/FontAwesome';
...
export default class HelloWorld extends Component {
render() {
return (<View>
<Image source={require('./images/icon.png')} style={{width:144,height:144}}/>
</View>)
}
}
...
这么做的优点就是不需要考虑不同操作系统的问题,统一进行处理。但是在打包时,根据一些朋友的反馈,在android系统下,图片文件会被编译到android\app\src\main\res目录下,并且自动更名为icon_images.png,可能会导致找不到图片,不过我编译后没有这个现象,也许可能是RN版本问题。
2:有代码洁癖的人可能不愿意在js目录中混入图片,那可以采用这种方法。在android\app\src\main\res目录下,新建一个drawable目录,然后把icon.png文件拷贝到该目录下,注意这个目录下同名文件不同格式的文件只能有一个,比如你有了icon.png就不能再有icon.jpg了,否则会报错。然后在代码中引用:
<Image source={require('icon')} style={{width:144,height:144}}/>
请注意source的写法,新版RN的写法不是require('image!icon') ,而是require('icon'),不要加后缀.png。我在项目中就是使用这种方法加载本地图片的。
使用导航控件
在项目中多多少少会使用导航控件,这样界面组织比较直观,这一节我们就来学习如何使用Navigator控件。首先需要安装依赖模块,命令行下切换到项目所在目录里,运行:
npm install --save-dev react-native-tab-navigator
照着样子写就行,具体API请查询官方文档或RN中文网,这里就不再详说了:
app.js
import React, { Component } from 'react';
import {
AppRegistry,
Navigator,
View
} from 'react-native';
import Index from './index';//导航首页
export default class HelloWorld extends Component {
render(){
let defaultName = 'Index';
let defaultComponent = Index;
return (
<Navigator
initialRoute={{ name: defaultName, component:defaultComponent}}
configureScene={(route) => {
Navigator.SceneConfigs.HorizontalSwipeJump.gestures=null;//不允许滑动返回
return Navigator.SceneConfigs.HorizontalSwipeJump;
}}
renderScene={(route, navigator) => {
let Component = route.component;
return <Component {...route.params} navigator={navigator} />
}} />
)
}
}
AppRegistry.registerComponent('HelloWorld', () => HelloWorld);
index.js
import React, { Component } from 'react';
import {
BackAndroid,
StyleSheet,
View,
TouchableHighlight,
Navigator
} from 'react-native';
import TabNavigator from 'react-native-tab-navigator';
import Ionicons from 'react-native-vector-icons/Ionicons';
import Home from './home';
import Find from './find';
import User from './user';
class Index extends Component{
constructor(props) {
super(props);
this.state = {
selectedTab:'home',
index:0
};
}
componentDidMount() {
const { navigator } = this.props;
//注册点击手机上的硬返回按钮事件
BackAndroid.addEventListener('hardwareBackPress', () => {
return this.onBackAndroid(navigator)
});
}
componentWillUnmount() {
BackAndroid.removeEventListener('hardwareBackPress');
}
onBackAndroid(navigator){
const routers = navigator.getCurrentRoutes();
if (routers.length > 1) {
navigator.pop();
return true;
}
return false;
}
changeTab(tab){//改变导航时
this.setState({ selectedTab:tab});
}
render(){
return (
<View>
<TabNavigator>
<TabNavigator.Item
title="首页"
titleStyle={{color:'gray'}}
selectedTitleStyle={{color:'#666666'}}
renderIcon={() => <Ionicons name='ios-home-outline' size={25} color='gray'/>}
renderSelectedIcon={() => <Ionicons name='ios-home' size={25} color='#666666'/>}
selected={ this.state.selectedTab === 'home' }
onPress={() => this.changeTab('home')}>
<Home navigator={navigator}/>
</TabNavigator.Item>
<TabNavigator.Item
title="发现"
titleStyle={{color:'gray'}}
selectedTitleStyle={{color:'#666666'}}
renderIcon={() => <Ionicons name='ios-document-outline' size={25} color='gray'/>}
renderSelectedIcon={() => <Ionicons name='ios-document' size={25} color='#666666'/>}
selected={this.state.selectedTab=='find'}
onPress={() => this.changeTab('find')}
>
<Find navigator={navigator}/>
</TabNavigator.Item>
<TabNavigator.Item
title="我的"
titleStyle={{color:'gray'}}
selectedTitleStyle={{color:'#666666'}}
renderIcon={() => <Ionicons name='ios-person-outline' size={25} color='gray'/>}
renderSelectedIcon={() => <Ionicons name='ios-person' size={25} color='#666666'/>}
selected={this.state.selectedTab =='user'}
onPress={() => this.changeTab('user')}>
<User navigator={navigator}/>
</TabNavigator.Item>
</TabNavigator>
</View>
)
}
}
export default Index;
然后你自己分别实现home.js,find.js以及user.js即可,这里就不再详述了。在这里需要说明以下onPress箭头函数(ES6语法),新版的RN用箭头函数来执行方法,而不是this.changeTab.bind(this),用箭头函数有个很大的好处是你不用担心上下文中this的指向问题,它永远指向当前的组件对象。
图片裁剪及手势事件的使用
RN中自带的图片处理组件CameraRoll并不好用,我这里用react-native-image-picker这个工具,同样在命令行下运行npm install --save-dev react-native-image-picker,一般情况下会报错,提示缺少fs依赖,所以我们要先运行npm install --save-dev fs,然后再运行npm install --save-dev react-native-image-picker。详细的配置步骤请参考官方安装手册,有个特别的地方需要注意的事官方手册没有提到,请打开node_modules\react-native-image-picker\android\build.gradle文件,然后修改buildToolsVersion为你实际build tools版本。。直接上代码,代码比较长,我就不直接解释了,自己慢慢看慢慢查资料吧,有什么问题可以在评论里问我。CustomButton是自定义的一个按钮组件,代码实现比较简单,这里就不再贴出了。
user.js
import React, { Component } from 'react';
import {
StyleSheet,
View,
Image,
TouchableOpacity,
ToastAndroid,
Dimensions,
PanResponder,
ImageEditor,
ImageStore
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import Ionicons from 'react-native-vector-icons/Ionicons';
import CustomButton from './components/CustomButton';
import ImagePicker from 'react-native-image-picker';
let {height, width} = Dimensions.get('window');
class User extends Component{
constructor(props) {
super(props);
this.unmounted = false;
this.camera = null;
this._clipWidth = 200;
this._boxWidth = 20;
this._maskResponder = {};
this._previousLeft = 0;
this._previousTop = 0;
this._previousWidth = this._clipWidth;
this._backStyles = {
style: {
left: this._previousLeft,
top: this._previousTop
}
};
this._maskStyles = {
style: {
left: -(width-this._clipWidth)/2,
top: -(width-this._clipWidth)/2
}
};
this.state = {
token:null,
username:null,
photo:null,
switchIsOn: true,
uploading:false,
uploaded:false,
changePhoto:false,
scale:1,
width:0,
height:0
}
}
componentWillMount() {
this._maskResponder = PanResponder.create({
onStartShouldSetPanResponder: ()=>true,
onMoveShouldSetPanResponder: ()=>true,
onPanResponderGrant: ()=>false,
onPanResponderMove: (e, gestureState)=>this._maskPanResponderMove(e, gestureState),
onPanResponderRelease: (e, gestureState)=>this._maskPanResponderEnd(e, gestureState),
onPanResponderTerminate: (e, gestureState)=>this._maskPanResponderEnd(e, gestureState),
});
}
_updateNativeStyles() {
this._maskStyles.style.left = -(width-this._clipWidth)/2+this._backStyles.style.left;
this._maskStyles.style.top = -(width-this._clipWidth)/2+this._backStyles.style.top;
this.refs['BACK_PHOTO'].setNativeProps(this._backStyles);
this.refs['MASK_PHOTO'].setNativeProps(this._maskStyles);
}
_maskPanResponderMove(e, gestureState){
let left = this._previousLeft + gestureState.dx;
let top = this._previousTop + gestureState.dy;
this._backStyles.style.left = left;
this._backStyles.style.top = top;
this._updateNativeStyles();
}
_maskPanResponderEnd(e, gestureState) {
this._previousLeft += gestureState.dx;
this._previousTop += gestureState.dy;
}
componentWillUnMount() {
this.unmounted = true;
}
_saveImage(){
let photoURI=this.state.photo.uri;
let left = -Math.floor(this._backStyles.style.left)+(width-this._clipWidth)/2;
let top = -Math.floor(this._backStyles.style.top)+(width-this._clipWidth)/2;
if(left<0 || top<0 || left+this._clipWidth>width || top+this._clipWidth>height){
ToastAndroid.show('超出裁剪区域,请重新选择', ToastAndroid.SHORT);
return;
}
this.setState({uploading:true});
ImageEditor.cropImage(
photoURI,
{offset:{x:left,y:top},size:{width:this._clipWidth, height:this._clipWidth}},
(croppedURI)=>{
ImageStore.getBase64ForTag(
croppedURI,
(base64)=>{
//这里即可获得base64编码的字符串,将此字符串上传带服务器处理,保存后并生成图片地址返回即可,详细代码后面结合node.js再做讲解。
},
(err)=>true
);
},
(err)=>true
);
}
_fromGallery() {
let options = {
storageOptions: {
skipBackup: true,
path: 'images'
},
maxWidth:width,
mediaType: 'photo', // 'photo' or 'video'
videoQuality: 'high', // 'low', 'medium', or 'high'
durationLimit: 10, // video recording max time in seconds
allowsEditing: true // 当用户选择过照片之后是否允许再次编辑图片
};
console.log(ImagePicker);
ImagePicker.launchImageLibrary(options, (response) => {
if (!(response.didCancel||response.error)) {
Image.getSize(response.uri, (w, h)=>{
this.setState({
changePhoto:true,
photo: response,
width: w,
height: w*h/width
});
this._updateNativeStyles();
})
}
});
}
_fromCamera() {
let options = {
storageOptions: {
skipBackup: true,
path: 'images'
},
maxWidth:width,
mediaType: 'photo', // 'photo' or 'video'
videoQuality: 'high', // 'low', 'medium', or 'high'
durationLimit: 10, // video recording max time in seconds
allowsEditing: true // 当用户选择过照片之后是否允许再次编辑图片
};
ImagePicker.launchCamera(options, (response) => {
if (!(response.didCancel||response.error)) {
Image.getSize(response.uri, (w, h)=>{
this.setState({
changePhoto:true,
photo: response,
width:w,
height:w*h/width
});
this._updateNativeStyles();
})
}
});
}
render() {
let Photo,Uploading;
if(this.state.photo){
if(this.state.changePhoto){
Photo=<View style={styles.row}>
<View style={{width:width,height:width,overflow:'hidden'}}>
<Image ref='BACK_PHOTO' source={{uri:this.state.photo.uri,scale:this.state.scale}} resizeMode='cover' style={{width:this.state.width,height:this.state.height,opacity:0.5}}/>
<View ref='MASK' {...this._maskResponder.panHandlers} style={{position:'absolute',left:(width-this._clipWidth)/2,top:(width-this._clipWidth)/2,width:this._clipWidth,height:this._clipWidth,opacity:0.8}}>
<Image ref='MASK_PHOTO' source={this.state.photo} resizeMode='cover' style={{width:this.state.width,height:this.state.height}}/>
</View>
</View>
</View>
}else{
Photo=<Image source={this.state.photo} resizeMode='cover' style={{width:width,height:width}}/>;
}
}
return (
<View style={styles.wrap}>
<View style={styles.body}>
<View style={[styles.row, {paddingBottom:30}]}>
<View style={{height:width,width:width}}>
{Photo}
</View>
</View>
{(()=> this.state.changePhoto?
<View>
<View style={styles.row1}>
<View style={{flex:1}}>
<CustomButton title='保存' onPress={()=>this._saveImage()}/>
</View>
</View>
</View>
:
<View>
<View style={styles.row1}>
<View style={{flex:1}}>
<CustomButton title='从相册选择' onPress={()=>this._fromGallery()}/>
</View>
</View>
<View style={styles.row1}>
<View style={{flex:1}}>
<CustomButton title='拍一张照片' onPress={()=>this._fromCamera()}/>
</View>
</View>
</View>
)()}
</View>
</View>
);
}
}
var styles = StyleSheet.create({
wrap:{
flex:1,
flexDirection:'column',
backgroundColor:'whitesmoke',
alignItems:'stretch',
justifyContent:'center'
},
body:{
flex:1,
flexDirection:'column',
alignItems:'stretch',
justifyContent:'flex-start'
},
row:{
flex:0,
flexDirection:'row',
alignItems:'center',
backgroundColor:'#fff'
},
row1:{
flex:0,
padding:10,
flexDirection:'row',
backgroundColor:'#fff',
alignItems:'stretch',
justifyContent:'center'
}
});
export default User
其它
1:修改应用程序名称,请修改android\app\src\main\res\values\strings.xm文件,然后将HelloWorld改成你喜欢的名称,可以是中文,你安装到手机上的应用名称就是这里定义的。
2:修改应用程序名称,请修改android\app\src\main\res\下以mipmap-开头的所有文件夹下的ic_launcher.png文件,覆盖它即可,注意你要先删除手机上的应用程序,然后再编译才会生效。
好了,码了这么多字,希望对大家有所帮助,喜欢的就支持下,呵呵。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。