资讯专栏INFORMATION COLUMN

一步一步开发安卓下的react-native应用系列之进阶篇

xioqua / 3096人阅读

摘要:首先我们打开命令行,切换到项目根目录下,输入安装完成后,请注意,需要把目录下的所有字体文件拷贝到目录下,如果没有该目录,请自行创建。

        看过我前面文章的朋友们现在应该能正常运行自己的第一个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 eact-native-vector-iconsFonts目录下的所有字体文件拷贝到androidappsrcmainassetsfonts目录下,如果没有该目录,请自行创建。所有你需要使用自定义的字体都需要拷贝到该目录下。
使用该模块很简单,比如我们需要加载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系统下,图片文件会被编译到androidappsrcmain es目录下,并且自动更名为icon_images.png,可能会导致找不到图片,不过我编译后没有这个现象,也许可能是RN版本问题。
2:有代码洁癖的人可能不愿意在js目录中混入图片,那可以采用这种方法。在androidappsrcmain es目录下,新建一个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 eact-native-image-pickerandroiduild.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:修改应用程序名称,请修改androidappsrcmain esvaluesstrings.xm文件,然后将HelloWorld改成你喜欢的名称,可以是中文,你安装到手机上的应用名称就是这里定义的。

        2:修改应用程序名称,请修改androidappsrcmain es下以mipmap-开头的所有文件夹下的ic_launcher.png文件,覆盖它即可,注意你要先删除手机上的应用程序,然后再编译才会生效。

        好了,码了这么多字,希望对大家有所帮助,喜欢的就支持下,呵呵。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/18992.html

相关文章

  • 步一开发安卓下的react-native应用系列前言

    摘要:这里是目录一步一步开发安卓下的应用系列之环境搭建篇一步一步开发安卓下的应用系列之第一个应用一步一步开发安卓下的应用系列之进阶篇怎么开发原生模块打包分发你的实现在线升级,包括热更新篇篇篇         公司今年效益惨淡,手头上没什么事可作,于是琢磨着自己做点什么,想了想,如今RN那么火热,那就整个APP出来玩玩吧。因为之前没怎么学过reactjs,更没有安卓系统开发经验,所以从过完年开...

    lewif 评论0 收藏0
  • 步一开发安卓下的react-native应用系列第一个RN应用

    摘要:闭上眼睛,心中默念一百遍遍马力马力轰,再睁开眼,如果你是安卓及以上系统,你就能在你手机上看到你第一个应用了图,如果是以下,嘿嘿,一个血红血红的界面,不过没关系,我们来纠正它。         前期准备工作已经完成,接下来将正式进入开发了,请深呼吸下,呵呵。我们首先写个Hello World工程来练练手。        在命令行上点右键,选择以管理员身份运行。建议每次运行命令行的时候都用...

    Donald 评论0 收藏0
  • Java进阶

    摘要:探索专为而设计的将探讨进行了何种改进,以及这些改进背后的原因。关于最友好的文章进阶前言之前就写过一篇关于最友好的文章反响很不错,由于那篇文章的定位就是简单友好,因此尽可能的摒弃复杂的概念,只抓住关键的东西来讲,以保证大家都能看懂。 周月切换日历 一个可以进行周月切换的日历,左右滑动的切换月份,上下滑动可以进行周,月不同的视图切换,可以进行事件的标记,以及节假日的显示,功能丰富 Andr...

    sushi 评论0 收藏0
  • 区块链技术学习指引

    摘要:引言给迷失在如何学习区块链技术的同学一个指引,区块链技术是随比特币诞生,因此要搞明白区块链技术,应该先了解下比特币。但区块链技术不单应用于比特币,还有非常多的现实应用场景,想做区块链应用开发,可进一步阅读以太坊系列。 本文始发于深入浅出区块链社区, 原文:区块链技术学习指引 原文已更新,请读者前往原文阅读 本章的文章越来越多,本文是一个索引帖,方便找到自己感兴趣的文章,你也可以使用左侧...

    Cristic 评论0 收藏0

发表评论

0条评论

xioqua

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<