React Native (Expo)で128 codeの読取／生成

## 必要な条件

このチュートリアルでは, 128 codeの生成や読み取ることができるAndroid アプリを作っていきたいと思います。 チュートリアルに沿っていくには、次の条件が必要になります：

テスト用のAndroidスマホ
Visual Studio Code（他のIDEでも良い）
Node (npm)
Reactの基礎知識

## 準備

### Expo CLI
パソコンのコマンドラインでExpo　CLIをインストールしましょう：`npm install -g expo-cli`。アカウントを持っていれば`expo login`実行します。アカウントを持っていなければ、`expo register`を実行します。

### Expoアプリ
Androidスマホで[Expoアプリ](https://play.google.com/store/apps/details?id=host.exp.exponent)をダウンロードします。ダウンロードが終わってから、アプリを開きます。「Profile」タブでログインが出来ます。

## アプリ作成

好きなIDEのターミナルで次のコードを実行します：
`expo init barcode-scanner`。「Choose a template」」が表示される時に「blank」を選びます。

`cd barcode-scanner`を実行して、必要なパッケージをインストールします：`npm i expo-barcode-scanner jsbarcode react-native-svg`

`npm start`を実行します。Expoアプで、「Scan QR Code」をクリックして、http://localhost:19002/ で表示されているQRコードをスキャンします。ダウンロードが終わってから、スマホでアプリが開きます。

開発中、コードを変更する時にスマホで表示されるまでに時間が掛かる可能性があります。その場合には、http://localhost:19002/ で「Run in web browser」をクリックするとパソコンのブラウザーで、アプリの様子が確認できます。

## App.jsx

App.jsxのコードをすべてを削除し、以下のコードに置き換えます。

``` App.jsx
import React, { useState } from "react";

const App = () => {
  const [mode, setMode] = useState<ModeType>("welcome");

  return (
    <>
    </>
  );
};

export default App;
```

## ホーム画面

プロジェクトのルートで、componentsフォルダを作りましょう。そのフォルダで、Welcome.jsxファイル作成します。

``` Welcome.jsx
import { StyleSheet, Text, View, Pressable } from "react-native";
import React from "react";

export default function Welcome({ setMode }) {
  return (
    <View style={styles.container}>
      <Pressable onPress={() => setMode("scan")} style={styles.button}>
        <Text style={styles.text}>バーコードを読み取る</Text>
      </Pressable>
      <Pressable onPress={() => setMode("generate")} style={styles.button}>
        <Text style={styles.text}>バーコードを生成する</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  button: {
    paddingVertical: 12,
    paddingHorizontal: 32,
    marginVertical: 20,
    borderRadius: 4,
    elevation: 3,
    backgroundColor: "blue",
  },
  text: {
    fontSize: 16,
    lineHeight: 21,
    fontWeight: "bold",
    letterSpacing: 0.25,
    color: "white",
  },
});
```

WelcomeコンポーネントをApp.jsxに追加します。

``` App.jsx
import React, { useState } from "react";
import Welcome from "./components/Welcome";

...
  return (
    <>
       {mode === "welcome" && <Welcome setMode={setMode} />}
    </>
  );
};
...
```

## バーコードの読取

バーコードスキャナーを作成していきます。componentsフォルダで、Scanner.jsxを作ります。

``` Scanner.jsx
import { StyleSheet, Text, View, Button, Pressable } from "react-native";
import React, { useState, useEffect } from "react";
import { BarCodeScanner } from "expo-barcode-scanner";

export default function Scanner({ setMode }) {
  const [hasPermission, setHasPermission] = useState(null || Boolean);
  const [scanned, setScanned] = useState(false);

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

  //バーコードがスキャンされた時に
  const handleBarCodeScanned = ({ type, data }) => {
    setScanned(true);
    alert(`バーコードタイプ： ${type}。 バーコードデータ： ${data}`);
  };

  return (
    <View style={styles.container}>
      {/* カメラアにクセスすることがまだ許可も拒否もされていない場合 */}
      {hasPermission === null && <Text>カメラの許可を要求する</Text>}
      {/* カメラアにクセスすることが拒否されている合 */}
      {hasPermission === false && <Text>カメラにアクセスできません</Text>}
      <Pressable style={styles.button} onPress={() => setMode("welcome")}>
        <Text style={styles.buttonText}>戻る</Text>
      </Pressable>
      {/* カメラアにクセスすることが許可されている場合 */}
      {hasPermission && (
        <BarCodeScanner
          onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
          style={StyleSheet.absoluteFillObject}
        />
      )}
      {/* スキャンが終わってから表示する */}
      {scanned && (
        <Button
          title={"別のバーコードを読み取る"}
          onPress={() => setScanned(false)}
        />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  button: {
    zIndex: 100,
    position: "absolute",
    bottom: 40,
    paddingVertical: 12,
    paddingHorizontal: 32,
    borderRadius: 4,
    elevation: 3,
    backgroundColor: "blue",
  },
  buttonText: {
    color: "white",
  },
});
```

ScannerコンポーネントをApp.jsxに追加します。

``` App.jsx
...
import Welcome from "./components/Welcome";
import Scanner from "./components/Scanner";

...
  return (
    <>
       {mode === "welcome" && <Welcome setMode={setMode} />}
       {mode === "scan" && <Scanner setMode={setMode} />}
    </>
  );
};
...
```

## バーコードの生成

componentsフォルダで、Generate.jsxを作ります。

``` Generate.jsx
import React, { useState } from "react";
import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import Barcode from "./Barcode";

export default function Generate({ setMode }) {
  const [inputText, onChangeInputText] = useState("");
  const [barcodeValue, setBarcodeValue] = useState("");

  //ローマ字や数字以外が入ってないことを確認する
  function onlyLettersAndNumbers(str) {
    return /^[A-Za-z0-9]*$/.test(str);
  }

  const generateBarcode = () => {
    if (onlyLettersAndNumbers(inputText)) {
      setBarcodeValue(inputText);
    } else {
      alert("ローマ字や数字を入力してください");
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.text}>10文字まで入力してください</Text>
      <TextInput
        style={styles.input}
        maxLength={10}
        value={inputText}
        onChangeText={onChangeInputText}
      />
      <Pressable style={styles.button} onPress={generateBarcode}>
        <Text style={styles.buttonText}>バーコードを生成する</Text>
      </Pressable>
      {barcodeValue ? <Barcode value={barcodeValue} /> : <Text></Text>}
      <Pressable style={styles.backButton} onPress={() => setMode("welcome")}>
        <Text style={styles.buttonText}>戻る</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "white",
    alignItems: "center",
    justifyContent: "center",
  },
  input: {
    height: 40,
    marginHorizontal: 12,
    marginVertical: 20,
    borderWidth: 1,
    padding: 10,
    width: 200,
    backgroundColor: "white",
  },
  button: {
    paddingVertical: 12,
    paddingHorizontal: 32,
    borderRadius: 4,
    elevation: 3,
    backgroundColor: "blue",
    marginBottom: 20,
  },
  buttonText: {
    color: "white",
  },
  text: {
    paddingHorizontal: 20,
  },
  backButton: {
    position: "absolute",
    bottom: 40,
    paddingVertical: 12,
    paddingHorizontal: 32,
    borderRadius: 4,
    elevation: 3,
    backgroundColor: "blue",
  },
});
```

GenerateコンポーネントをApp.jsxに追加します。

``` App.jsx
...
import Generate from "./components/Generate";
...
  return (
    <>
      {mode === "welcome" && <Welcome setMode={setMode} />}
      {mode === "scan" && <Scanner setMode={setMode} />}
      {mode === "generate" && <Generate setMode={setMode} />}
    </>
  );
};
...
```

componentsフォルダで、Barcode.jsxを作ります。

``` Barcode.jsx
import React, { useEffect, useState } from "react";
import Svg, { G } from "react-native-svg";
import JSBarcode from "jsbarcode";
import BarcodeText from "./barcode/BarcodeText";
import BarcodeChunk from "./barcode/BarcodeChunk";

const Barcode = ({ value }) => {
  const [encoding, setEncoding] = useState();
  const [maxHeight, setMaxHeight] = useState(0);
  const [width, setWidth] = useState(0);

  useEffect(() => {
    //すべてのオプションをマージする
    const merge = (old, replaceObj) => {
      return { ...old, ...replaceObj };
    };

    //エンコーディングの高さを計算する
    function getEncodingHeight(options) {
      return (
        options.height +
        options.fontSize +
        options.textMargin +
        options.marginTop +
        options.marginBottom
      );
    }

    //エンコーディング属性を計算する
    function calculateEncodingAttributes(encoding, barcodeOptions) {
      const options = merge(barcodeOptions, encoding.options);
      encoding.width = encoding.data.length * options.width;
      encoding.height = getEncodingHeight(options);
      encoding.barcodePadding = 0;
    }

    const createBarcode = () => {
      let barcode = {};

      //全てのオプションとデフォルトオプションがhttps://github.com/lindell/JsBarcode/wiki/Optionsで見える
      let options = { format: "CODE128", marginLeft: 10, marginRight: 10 };

      JSBarcode(barcode, value, options);
      let encoding = barcode.encodings[0];
      calculateEncodingAttributes(encoding, options);
      setEncoding(encoding);
      const totalWidth = encoding.width;
      setMaxHeight(encoding.height);
      setWidth(totalWidth + options.marginLeft + options.marginRight);
    };
    createBarcode();
  }, [value]);

  return (
    <Svg
      x={0}
      y={0}
      width={width}
      height={maxHeight}
      viewBox={`0 0 ${width} ${maxHeight}`}
      originX={0}
      originY={0}
    >
      {/* encodingが未定義ではない場合にのみ表示する */}
      {encoding && (
        <G
          x={encoding.options.marginLeft}
          y={encoding.options.marginTop}
          fill={encoding.options.lineColor}
        >
          {/* バーコード */}
          <BarcodeChunk
            binary={encoding.data}
            padding={0}
            options={encoding.options}
          />
          {/* バーコードの下に表示されるテキスト */}
          <BarcodeText
            text={encoding.text}
            width={encoding.width}
            padding={0}
            options={encoding.options}
          />
        </G>
      )}
    </Svg>
  );
};

export default Barcode;
```

最後にBarcodeTextとBarcodeChunkコンポーネントを作ります。

``` BarcodeText.jsx
import { Text } from "react-native-svg";

export default function BarcodeText(props) {
  const { text, width, options } = props;

  if (options.displayValue) {
    const y = options.height + options.textMargin + options.fontSize;
    const x = width / 2;
    return (
      <Text
        x={x}
        y={y}
        fontFamily={options.font}
        fontSize={options.fontSize}
        fontWeight={"bold"}
        textAnchor={"middle"}
      >
        {text}
      </Text>
    );
  } else {
    return null;
  }
}
```

``` BarcodeChunk.jsx
import { Rect } from "react-native-svg";

const BarcodeChunk = ({ binary, options }) => {
  let barWidth = 0;
  let x = 0;
  let bars = [];
  for (var b = 0; b < binary.length; b++) {
    x = b * options.width;

    if (binary[b] === "1") {
      barWidth++;
    } else if (barWidth > 0) {
      bars.push({
        x: x - options.width * barWidth,
        y: 0,
        width: options.width * barWidth,
        height: options.height,
      });
      barWidth = 0;
    }
  }

  if (barWidth > 0) {
    bars.push({
      x: x - options.width * (barWidth - 1),
      y: 0,
      width: options.width * barWidth,
      height: options.height,
    });
  }

  return (
    <>
      {bars.map((bar, i) => (
        <Rect
          key={i}
          x={bar.x}
          y={bar.y}
          width={bar.width}
          height={bar.height}
        />
      ))}
    </>
  );
};

export default BarcodeChunk;
```

これで、128 codeの生成や読み取ることができるAndroid アプリが完了です。Androidスマホで、バーコードを読み取るや生成してみてください！