AWS
reactnative
axios
react-native-camera

React Nativeでのカメラ機能と画像のアップロードについて

本エントリでは作成したアプリで使用したいくつかのテクニックの一つのカメラ機能について記載します。

React Native(以下 RN と記載)でカメラを使う場合幾つか方法があるようですが筆者はreact-native-cameraを使用しました。

react-native-cameraは本家サイトを見るとバーコードを読めたり動画とれたり顔認識なども出来るようですが本アプリではそこまで必要なかったのでシンプルにカメラ機能のみを使用しました。

以下カメラ機能を実装しているコンポーネントのソースです


HomeCamera.js

import React, {Component} from 'react';

import {Platform, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import { NavigationActions } from "react-navigation";
import {RNCamera} from 'react-native-camera'

const PendingView = () => (
<View
style={{
flex: 1,
backgroundColor: 'lightgreen',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Text>Waiting</Text>
</View>
);

export default class HomeCamera extends Component<Props> {
//コンストラクタ
constructor(props){
super(props); //必ず呼ぶ
this.state = {
url: null
}
}
render() {
let { url } = this.state; // ...(4)
return (
<View style={styles.container}>
<RNCamera
style={styles.preview}
type={RNCamera.Constants.Type.back}
captureAudio={false}
flashMode={RNCamera.Constants.FlashMode.on}
permissionDialogTitle={'Permission to use camera'}
permissionDialogMessage={'We need your permission to use your camera phone'}
>
{({ camera, status }) => {
if (status !== 'READY') return <PendingView />;
return (
<View style={{ flex: 0, flexDirection: 'row', justifyContent: 'center' }}>
<TouchableOpacity onPress={() => this.takePicture(camera)} style={styles.capture}>
<Text style={{ fontSize: 14 }}> SNAP </Text>
</TouchableOpacity>
</View>
);
}}
</RNCamera>
</View>
);
}

takePicture = async function(camera) {
const options = { quality: 0.5, base64: true ,fixOrientation:true};
const data = await camera.takePictureAsync(options);
// eslint-disable-next-linse
this.props.navigation.navigate("Conf",{url:data.uri,base64:data.base64});
};
}

const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
backgroundColor: 'aliceblue',
},
preview: {
flex: 1,
justifyContent: 'flex-end',
alignItems: 'center',
},
capture: {
flex: 0,
backgroundColor: '#fff',
borderRadius: 5,
padding: 15,
paddingHorizontal: 20,
alignSelf: 'center',
margin: 20,
},
});

ほぼサンプルのままですがポイントはオプションです。

const options = { quality: 0.5, base64: true ,fixOrientation:true};

base64形式にしたのは、後々AWSに写真をアップロードしたいのでbase64形式で受け取った方が便利だからです。変換する必要なく機能として備わっているのはとても助かりました。

それとfixOrientationを設定したのはオプション無しだと撮影した写真が傾いていたためです。本オプションを設定することで問題が解消されました。ただ公式によると本機能はAndroidでしか効かないということでiOSへ対応するなどということになったら他のライブラリを使用することになりそうです。

またcaptureAudioがデフォルトではONになっているようで何もしないと画面にWARNINGが出てきました。ですのでfalseにしています。

撮影したのちに画像を表示また投稿する画面に遷移するため

this.props.navigation.navigate("Conf",{url:data.uri,base64:data.base64});

としています。この遷移の際に次のコンポーネントに引数として渡すことができます。

ではカメラで撮影した後に遷移される写真を表示、またAWSに転送するコンポーネントに移ります。以下ソースです。


HomeConf.js

import React, {Component} from 'react';

import {Platform, StyleSheet, Text,View,TouchableOpacity,Image, TextInput} from 'react-native';
import { NavigationActions } from "react-navigation";
import axios from 'axios';

export default class HomeConf extends Component<Props> {
//コンストラクタ
constructor(props){
super(props); //必ず呼ぶ
this.state = {
path: null,
inputValue: "You can change me!"
}
}

onPressPostBtn(path) {

var fileSelect = path;
//console.log('path:' + fileSelect);
axios.post('https://XXXXXXX.execute-api.XXXXXXXX.amazonaws.com/dev/image_upload',{base64:fileSelect,Text:this.state.inputValue},{
headers: {Accept: 'application/json','Content-Type': 'application/json'}})
.then((response) => {
console.log(response)
})
.catch((err) => {
console.log(err)
});
}

_onChangeText = (text) => {
this.setState({ inputValue:text });
}

render() {
const { navigation } = this.props;
const purl = navigation.getParam('url', 'NO-URL');
const pbase64 = navigation.getParam('base64', 'NO-BASE64');
const path = JSON.stringify(purl);
const base64 = JSON.stringify(pbase64);
return (
<View style={styles.container}>
{base64 &&
<Image source={{uri: 'data:image/jpeg;base64,' + base64}} style={{ width: 250, height: 250,margin: 20}} />}
<TextInput
style={styles.input}
onChangeText={this._onChangeText}
underlineColorAndroid='transparent'/>
<TouchableOpacity onPress={() => this.onPressPostBtn(base64)} style={styles.capture}>
<Text style={{ fontSize: 14 }}> POST</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => this.props.navigation.navigate("Home")} style={styles.capture}>
<Text style={{ fontSize: 14 }}> Home</Text>
</TouchableOpacity>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
backgroundColor: 'aliceblue',
alignItems: 'center'
},
input: {
height: 40,
width: 230,
borderBottomWidth: 1,
borderBottomColor: '#008080',
margin: 7
},
capture: {
flex: 0,
borderRadius: 5,
padding: 15,
backgroundColor: '#fff',
paddingHorizontal: 20,
alignSelf: 'center',
margin: 20,
}
});

撮影した後に遷移される際に引数として渡された画像のuriと画像そのもののBase64データと描画の前に

const purl = navigation.getParam('url', 'NO-URL');

const pbase64 = navigation.getParam('base64', 'NO-BASE64');
const path = JSON.stringify(purl);
const base64 = JSON.stringify(pbase64);

として取り出しています。取り出したデータはbase64の方はImageにそのまま入れています(uriはテストようだったので現段階のソースに記載はありません)。

画像の下にテキストボックスを配置しコメントを入力してもらって、POSTをタップするとコメントと画像が転送する機能を提供しています。実現する為にイベントハンドラに引数としてBase64データを渡しています。テキストはstateに持っています。

※※※※※※※※※※※※※※※※※※※※※※※※※

ここで個人的にハマったので記載します。

当初このイベントハンドラは

と記載していました。

<TouchableOpacity onPress={this.props.navigation.navigate("Home")} style={styles.capture}>

上記の書き方だとTextInputに値が入力されている間onChangeTextでstateが更新され続けるので再描画され、その度にTouchableOpacityのonPressが発火して、結果何度も画像が転送されるという事象に悩まされました。調べた結果現在のように書くと良いという記事を見つけて助けてもらいました。実は恥ずかしながらこの引数なしの関数でreturnするという理屈が未だにきちんと理解できていないのですが。。。

※※※※※※※※※※※※※※※※※※※※※※※※※

転送はaxiosを使っています。当初上手くいかずFetch APIなどで実現していたこともあったのですが、axiosで実現できたので本アプリの通信回りをaxiosで統一出来ました。

別のエントリでも記載しますが、最初AWSのAPI Gatewayでバイナリがサポートされたとのことでそちらを使おうと思ったのですが僕には上手くいきませんでした。調べたところBase64で転送する発想を見つけて試していました。最初はテキストデータなのでtext/plainで試行錯誤していたのですがこれも上手くいかず。更に調査をするとどうやらtext/plainだとAWSの方で何やら加工するらしく。。。なのでJSONで転送すると良いという記載を見つけて試しました。最初は難航しましたがその理由はAPI Gatewayと転送の際に付与しているheaderの整合性でした。

非常に困りましたが、結論として特段事情がない限りは転送は全てJSONで行うのが良さそうです。

以上でカメラ機能とバイナリ転送についての篇を閉じます。