看过我前面文章的朋友们如今应该能正常运行本身的第一个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组件都放在此目录下。整个项目目录结构以下:node
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,将内容改为:react
require('./src/app');
并把原来的index.android.js中的代码拷贝到src/app.js中。接下来咱们全部的js代码编写都将在src目录下进行。另外开发过程当中咱们时刻要时刻关注下package server是否报错中止,若是中止就在窗口中运行react-native start以从新启动改服务。android
reactjs之因此大受欢迎,其中一个很重要的缘由就是其组件化设计思想,虽然angularjs经过指令也能够实现其组件化设计思想,但仍是没有reactjs来的优雅(原谅我有点逼格的用了这个词汇)。RN源自于reactjs,天然也继承了其组件化的设计。其实自定义组件原本很简单,没什么特别要讲的,不过我这里有个特殊用途 因此就单独拿出来讲一下吧。
RN中是没有全局字体属性能够设置的,因此若是咱们要统一设定字体属性,仍是比较麻烦的,网上也有一些方案你们能够搜搜,我这里就用一个自定义Text组件来实现全局修改字体属性,主要就是fontSize属性和fontFamily属性。咱们将这个组件命名为CustomText.js,存放在components目录下。ios
CustomText.jsgit
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属性。angularjs
... 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来说。首先咱们打开命令行,切换到项目根目录下,输入:github
npm install --save-dev react-native-vector-icons
安装完成后,请注意,须要把node_modules\react-native-vector-icons\Fonts目录下的全部字体文件拷贝到android\app\src\main\assets\fonts目录下,若是没有该目录,请自行建立。全部你须要使用自定义的字体都须要拷贝到该目录下。
使用该模块很简单,好比咱们须要加载FontAwesome矢量字体,则这么引用:npm
... 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种方式:json
1:根据facebook的建议,本地图片建议放到js文件相对目录下,好比你能够在src目录下再建一个images目录,而后把你的图片放到该目录下。引用的话比较简单,好比你在app.js中引用images目录下的icon.png文件,你能够这么写:react-native
... 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文件,覆盖它便可,注意你要先删除手机上的应用程序,而后再编译才会生效。
好了,码了这么多字,但愿对你们有所帮助,喜欢的就支持下,呵呵。