一步一步開發安卓下的react-native應用系列之進階篇

        看過我前面文章的朋友們如今應該能正常運行本身的第一個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文件,覆蓋它便可,注意你要先刪除手機上的應用程序,而後再編譯纔會生效。

        好了,碼了這麼多字,但願對你們有所幫助,喜歡的就支持下,呵呵。

相關文章
相關標籤/搜索