yutatanaka.tokyo logo

yutatanaka.tokyo

Published on

【expo44SDK】Expoの最新SDKで関数コンポーネントでexpo-cameraを使う

Authors

概要

expo + React-NativeのiOS/Android両用アプリを作成する際に、OCRリーダーを組み込む必要が発生。


過去の類似の事例をググったが、

関数コンポーネントではなくクラスコンポーネントを使ったものだった

ので、自分なりに最新のSDKにあわせて改変したものを作ってみることにしました。

細かい部分はかなり端折ったが、以下のクラスを使っていただくことで、撮影後の画像のオブジェクトを取得できるところまでは処理できます。

あとは、取得した画像をAPIにポストすることで、QRリーダーとしてもOCRリーダーとしても実装が可能になる

と思います。


アップロードした画像は例えば

Amazon S3

などのクラウドストレージに溜めておくと管理が利用ですが、そのあたりの開発は本記事とは直接関係ないので省きます。

必要なライブラリ


import { useState, useEffect } from "react";
import { StyleSheet, Text, View } from "react-native";
import { Button, Icon, useToast, Box, Center, Image } from "native-base";
import { Camera } from "expo-camera";
import { Ionicons } from "@expo/vector-icons";
import Loading from "../components/Loading";

// 以下の2つは手製の共用ライブラリのため、コメントアウト
// 使いやすいものを適宜使っていただければOK

//import NetworkLib from "../network/NetworkLib";
//import AlertService from "../util/AlertService";

const CommonCamera = () => {
  const [hasPermission, setHasPermission] = useState(null);
  const [type, setType] = useState(Camera.Constants.Type.back);
  const [camera, setCamera] = useState(null);
  const [photo, setPhoto] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const toast = useToast();

  class Upload {
    load = () => {
      if (photo == null) {
        return;
      }
      setIsLoading(true);

      let requestBody = {
        ocrImageString: "data:image/jpg;base64," + photo.base64,
      };

      // ネットワーク処理を行うライブラリで、リクエストを投げる
      // NetworkLib(requestBody, "POST", "ocr_or_qr/image/upload")
    };
  }

  useEffect(() => {
    (async () => {
      const { status } = await Camera.requestCameraPermissionsAsync();
      setHasPermission(status === "granted");
    })();
  }, []);

  const takePicture = async () => {
    // 失敗のtoastがもし出ていた場合は、消す
    toast.closeAll();

    if (!camera) {
      return;
    }
    // あとでこの文字列をアップロードする
    let photo = await camera.takePictureAsync({
      quality: 0.1, // アップロードのため画質を落とす
      base64: true,
    });
    // オブジェクトを詰める
    setPhoto(photo);

    toast.show({
    placement: "top",
    render: () => {
        return <Box bg="green.500" px="10" py="10" rounded="lg" mb={5}>
                <Text style={styles.fail}>画像の読み込みに成功しました</Text>
            </Box>;
    }
    })
  };

  if (hasPermission === null) {
    return <View />;
  }
  if (hasPermission === false) {
    return <Text>No access to camera</Text>;
  }

  return (
    <View
      style={{
        flex: 1,
        flexDirection: "column",
        justifyContent: "flex-end",
      }}
    >
      <Camera
        style={styles.flexOne}
        type={type}
        ref={(ref) => {
          setCamera(ref);
        }}
      >
        <View style={styles.layerTop}>
          <Text style={styles.description}>
            画像を中心に当てて{"\n"}読み取ってください
          </Text>
        </View>
        <View style={styles.layerCenter}>
          <View style={styles.layerLeft} />
          <View style={styles.focused} />
          <View style={styles.layerRight} />
        </View>
        <View style={styles.layerBottom}>
          <Button icon onPress={takePicture} style={styles.button}>
            <Icon as={Ionicons} name="camera" size="50" color="white" />
          </Button>
        </View>
      </Camera>

      <Loading loading={isLoading} />
    </View>
  );
};

export default CommonCamera;

const opacity = "rgba(0, 0, 0, .6)";

const styles = StyleSheet.create({
  flexOne: {
    flex: 1,
  },
  icon: {
    fontSize: 100,
  },
  description: {
    // fontSize: iPadSize() ? 45 : 25,  // 端末により調整も可
    fontSize: 25,
    marginTop: "35%",
    textAlign: "center",
    color: "white",
  },
  layerTop: {
    flex: 1,
    backgroundColor: opacity,
  },
  layerCenter: {
    flex: 1,
    flexDirection: "row",
  },
  layerLeft: {
    flex: 1,
    backgroundColor: opacity,
  },
  focused: {
    flex: 8,
  },
  layerRight: {
    flex: 1,
    backgroundColor: opacity,
  },
  layerBottom: {
    flex: 1,
    backgroundColor: opacity,
  },
  button: {
    position: "absolute",
    bottom: 30,
    paddingTop: 5,
    zIndex: 1,
    alignSelf: "center",
    height: 60,
    width: "80%",
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    borderRadius: 5,
  },
  fail: {
    color: "#ffffff",
    fontSize: 25,
  },
});

Loadingを管理するLoading.tsxは以下のような感じとなります。


import * as React from "react";
import { StyleSheet, Text, View } from "react-native";
import { Modal, Spinner, Center } from "native-base";

interface Props {
  loading: boolean;
}

const Loading = (props: Props) => {
  return (
    <Center>
      <Modal isOpen={props.loading}>
        <Spinner size="lg" color="primary.500" />
      </Modal>
    </Center>
  );
};
export default Loading;


まとめ

expoを久しぶりに触ったけど、もうClass Componentには戻れない。Functional Component最高。



あとswiftと比較すると、expo+React-NativeはすごくUIドリブンですね。直感的に、超短時間にプロトタイプなどが開発できますね。


でもアプリの申請や運用にかかる手間は、ネイティブアプリと変わらないので、利用する用途を考えないと後で苦労するかもと思いました。