博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
一步一步开发安卓下的react-native应用系列之进阶篇
阅读量:6230 次
发布时间:2019-06-21

本文共 14947 字,大约阅读时间需要 49 分钟。

        看过我前面文章的朋友们现在应该能正常运行自己的第一个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 (
{this.props.label}
) }}export default CustemText

        在app.js中使用,请注意,如果属性为数字或者bool值,需要写在大括号中,比如fontSize属性,如果为字符串,则直接书写即可,比如color和label属性。

...import CustomText from './components/CustomText';...export default class HelloWorld extends Component {  render() {    return (
)}...

使用自定义字体文件

        这里我们结合你可能会用到的矢量字体库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 (
) }}...

使用本地图片

        使用网络图片比较简单,直接引用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 (
) }}...

这么做的优点就是不需要考虑不同操作系统的问题,统一进行处理。但是在打包时,根据一些朋友的反馈,在android系统下,图片文件会被编译到android\app\src\main\res目录下,并且自动更名为icon_images.png,可能会导致找不到图片,不过我编译后没有这个现象,也许可能是RN版本问题。

2:有代码洁癖的人可能不愿意在js目录中混入图片,那可以采用这种方法。在android\app\src\main\res目录下,新建一个drawable目录,然后把icon.png文件拷贝到该目录下,注意这个目录下同名文件不同格式的文件只能有一个,比如你有了icon.png就不能再有icon.jpg了,否则会报错。然后在代码中引用:

请注意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.SceneConfigs.HorizontalSwipeJump.gestures=null;//不允许滑动返回 return Navigator.SceneConfigs.HorizontalSwipeJump; }} renderScene={(route, navigator) => { let Component = route.component; return
}} /> ) }}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 (            
} renderSelectedIcon={() =>
} selected={ this.state.selectedTab === 'home' } onPress={() => this.changeTab('home')}>
} renderSelectedIcon={() =>
} selected={this.state.selectedTab=='find'} onPress={() => this.changeTab('find')} >
} renderSelectedIcon={() =>
} selected={this.state.selectedTab =='user'} onPress={() => this.changeTab('user')}>
) }}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=
}else{ Photo=; } } return (
{Photo}
{(()=> this.state.changePhoto?
this._saveImage()}/>
:
this._fromGallery()}/>
this._fromCamera()}/>
)()}
); }}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://kvxna.baihongyu.com/

你可能感兴趣的文章
Homebrew简介和基本使用
查看>>
如何将DWG批量转成高清晰JPG图片
查看>>
以太坊web3.js文档翻译及说明
查看>>
list集合练习笔记
查看>>
SqlServer2008 R2数据库主从搭建
查看>>
一个程序猿试用有道云笔记VIP功能体验
查看>>
简单对接快递100
查看>>
Etherscan以太坊API官方文档中文版
查看>>
wamp 无法打开localhost:The requested URL / was not...
查看>>
ERC827以太坊通证标准
查看>>
PropertyPlaceholderConfigurer ---Spring管理配置文件
查看>>
初学Python:写码时应该缩进使用 tab 还是空格?
查看>>
10.15 iptables filter表案例, iptables nat表应用
查看>>
java B2B2C Springboot电子商城系统-路由网关(zuul)
查看>>
重磅课程|《CNCF x Alibaba 云原生技术公开课》正式开讲!
查看>>
java反射+注解实现Entity类与Dto类相互转换
查看>>
LVM讲解和磁盘故障小案例
查看>>
年后跳槽怕面试不过关?搞懂并发编程,轻松应对80%的面试场景
查看>>
Spring Cloud 终于按捺不住推出了自己的服务网关 Gateway
查看>>
【更新】Infragistics Ultimate UI for WPF v18.2(二):分类图
查看>>