【ReactNative】選択した画像に任意の要素を被せて保存する

11 min read読了の目安(約10300字

はじめに

幾度かのバージョンアップによって随分と使いやすくなったReact Nativeですが、
ReactJSが分かれば学習しやすい」という性質上、あまり「ネイティブモジュール」を触らずにキャリアを積まれる方も多いかと思います。
私自身もその例に漏れず、開発したアプリの多くは「APIから取得したデータを加工・表示する」ものや「地図に情報をプロットする」といった機能がほとんどです。

今回は一歩踏み込んで、簡単な「画像加工」をやってみようと思い立ったので、
その学習内容と成果物を記事にしたいと思います。

環境

  • react-native@0.64

記事執筆時点でReactNativeの最新バージョンである0.64を使用しています。

完成品

以下、完成したアプリのイメージです。

save-capture

任意の画像を選択して、その画像を正方形にトリミングします。
トリミング後の画像にテキストを被せて表示させて、「保存」ボタンを押下することで、
端末内に 「テキストが被った状態の画像」 を保存します。

イメージとしては画像に対してフレームを付けたり、署名などの文字情報を付けたりするアプリに近いです。

各種ライブラリのインストール

各種ライブラリのインストール方法について紹介します。
画像に関連する機能なので、ネイティブモジュール側にも多少変更を加える必要があります。

今回は@react-native-community/camerarollreact-native-view-shotreact-native-crop-image-pickerの3種類のライブラリを使用します。

@react-native-community/cameraroll

https://github.com/react-native-cameraroll/react-native-cameraroll

カメラロール(画像ストレージ)に関するライブラリです。
カメラロールから画像データ一覧を取得したり、新たに画像をカメラロールに保存することができます。
「カメラロール」という名前からiOSのみに対応したライブラリのように思いますが、しっかりAndroidにも対応しています。

インストール

yarn add @react-native-community/cameraroll
yarn add fbjs

fbjsは公式インストール手順には存在しませんでしたが、内部的には参照しているようで、
これを入れないとビルドエラーとなりました。
※以降のバージョンでは改善される見込み?

iOS

pod installを行います。

cd ios
pod install
  • Info.plistNSPhotoLibraryUsageDescriptionNSPhotoLibraryAddUsageDescriptionを追加
    以下のように権限へアクセス時の文言を設定します。
ios/{App Name}/Info.plist
<dict>
  <!-- 略 -->
  <key>NSPhotoLibraryUsageDescription</key>
  <string>アプリ上での画像の加工のために使用します。</string>
  <key>NSPhotoLibraryAddUsageDescription</key>
  <string>アプリ上での画像の加工のために使用します。</string>
</dict>

Android

  • 権限の追加
    加工した画像を書き込むためapp/src/main/AndroidManifest.xmlに以下の権限を追加する必要があります。
app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

さらに実際に画像を書き込みする段階で、ソースコード上で以下のように許可状態となっているかチェックする必要があります。

コードを表示する
import { PermissionsAndroid } from "react-native";

// 権限の有無をチェック
const hasAndroidPermission = async () => {
  const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;

  const hasPermission = await PermissionsAndroid.check(permission);
  if (hasPermission) {
    return true;
  }

  const status = await PermissionsAndroid.request(permission);
  return status === 'granted';
}

react-native-view-shot

https://github.com/gre/react-native-view-shot

キャプチャに関するライブラリです。
コンポーネントでラップする形で任意の領域を指定しておき、能動的にキャプチャを取得することができます。

インストール

yarn add react-native-view-shot

iOS

pod installを行います。

cd ios
pod install

react-native-image-crop-picker

https://github.com/ivpusic/react-native-image-crop-picker

画像の取得・切り取りに関するライブラリです。
端末内に保存されている画像を選択するライブラリではreact-native-image-pickerがメジャーですが、
こちらは 「選択した画像を任意サイズで切り取る」 機能が付いています。
主には「SNSのプロフィール画像」のように、元画像から正方形の任意サイズで切り出したい時などに役立ちます。

インストール

yarn add react-native-image-crop-picker

Android

  • android/build.gradleに下記の記述を追加
android/build.gradle
allprojects {
  repositories {
    // ...略

    // 以下2行を追加
    maven { url 'https://maven.google.com' }
    maven { url 'https://www.jitpack.io' }
  }
}
  • android/app/build.gradleに下記の記述を追加
android/app/build.gradle
android {
  // ...略
  defaultConfig {
    // ...略
    
    // 以下1行を追加
    vectorDrawables.useSupportLibrary true
  }
}

上記に加えて公式のインストール手順にはUse Android SDK >= 26の記述がありますが、
本記事はReactNative@0.64以降を対象としているためデフォルトで条件を満たしています。

  • カメラを使用する場合の権限設定(オプション)
    その場でカメラを使って撮影した画像を使う場合はapp/src/main/AndroidManifest.xmlに以下の権限を追加する必要があります。
app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.front" android:required="false" />

iOS

pod installを行います。

cd ios
pod install
  • Info.plistNSPhotoLibraryUsageDescriptionを追加
    以下のように権限へアクセス時の文言を設定します。
    おそらく@react-native-community/camerarollで同じ設定をしているためスキップしても問題ないです。
ios/{App Name}/Info.plist
<dict>
  <!-- 略 -->
  <key>NSPhotoLibraryUsageDescription</key>
  <string>アプリ上での画像の加工のために使用します。</string>
</dict>
  • カメラ等を使用する場合は追加でNSCameraUsageDescriptionNSMicrophoneUsageDescriptionを設定します。

実装

ライブラリのインストールが完了したら、いよいよ実装です。
先にコード全体を載せておきます。

コード全体を表示
App.tsx
import React, {useCallback, useRef, useState} from 'react';
import {
  Platform,
  Button,
  SafeAreaView,
  StatusBar,
  View,
  Image,
  PermissionsAndroid,
  Text,
} from 'react-native';
import ImagePicker, {
  Image as CroppedImage,
} from 'react-native-image-crop-picker';
import ViewShot from 'react-native-view-shot';
import CameraRoll from '@react-native-community/cameraroll';

const App: React.FC = () => {
  // 選択中の画像
  const [selectedImage, setSelectedImage] = useState<CroppedImage | null>(null);
  // 画像選択ボタン押下時処理
  const onPressSelectPhoto = useCallback(() => {
    // crop-image-pickerを起動
    ImagePicker.openPicker({
      width: 300,
      height: 300,
      cropping: true,
    })
      .then(image => {
        // 正常に取得できたらstateにセット
        setSelectedImage(image);
      })
      .catch(err => {
        console.log(err);
      });
  }, []);

  return (
    <SafeAreaView>
      <StatusBar />
      <View
        style={{
          width: '100%',
          height: '100%',
          justifyContent: 'center',
          alignItems: 'center',
        }}>
        <DecoratedImage img={selectedImage} />
        <Button
          title={`画像${selectedImage ? '再' : ''}選択`}
          onPress={onPressSelectPhoto}
        />
      </View>
    </SafeAreaView>
  );
};

/**
 * 装飾して表示させる画像
 * @param props
 * @returns
 */
const DecoratedImage: React.FC<{img: CroppedImage | null}> = props => {
  // ViewShotのref
  const viewShot = useRef<ViewShot>(null);
  // 画像保存ボタン押下時処理
  const onPressSave = useCallback(async () => {
    // OSがAndroidかつ権限が付与されていない場合は終了
    if (Platform.OS === 'android' && !(await hasAndroidPermission())) {
      return;
    }
    // ViewShotからキャプチャーを取得
    const uri = await viewShot?.current?.capture?.();
    if (uri) {
      // 取得した画像をカメラロールに保存
      CameraRoll.save(uri);
    }
  }, []);
  const {img} = props;
  if (!img) return null;

  return (
    <View style={{margin: 4}}>
      <ViewShot ref={viewShot}>
        <Text
          style={{
            position: 'absolute',
            top: 10,
            left: 10,
            fontWeight: 'bold',
            color: 'white',
            backgroundColor: '#0f83fd',
            zIndex: 1,
          }}>
          ほげほげ
        </Text>
        <Image
          source={{uri: img.path}}
          style={{width: img.width, height: img.height}}
          height={img.height}
          width={img.width}
        />
      </ViewShot>
      <Button title="保存する" onPress={onPressSave} />
    </View>
  );
};

// 権限の有無をチェック
const hasAndroidPermission = async () => {
  const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;

  const hasPermission = await PermissionsAndroid.check(permission);
  if (hasPermission) {
    return true;
  }

  const status = await PermissionsAndroid.request(permission);
  return status === 'granted';
};

export default App;

以下、個別に要点となる箇所を解説していきます。

画像にテキストを被せた要素をViewShotを使ってキャプチャ・保存

コード中ではDecoratedImageと名前が付けられたコンポーネントです。
コンポーネント全体をViewShotで囲っているのがポイントで、このViewShotrefを取得しておいて

  • 任意のタイミングでrefからViewShotを取得しcapture()を実行
  • capture()が正常に完了したらCameraRoll.save()で端末に保存

を行なっています。
また、画像に対してposition:absolute<Text>を被せるように表示していて、
この状態でキャプチャを撮ることで擬似的に画像を加工した状態としています。


/**
 * 装飾して表示させる画像
 * @param props
 * @returns
 */
const DecoratedImage: React.FC<{img: CroppedImage | null}> = props => {
  // ViewShotのref
  const viewShot = useRef<ViewShot>(null);
  // 画像保存ボタン押下時処理
  const onPressSave = useCallback(async () => {
    // OSがAndroidかつ権限が付与されていない場合は終了
    if (Platform.OS === 'android' && !(await hasAndroidPermission())) {
      return;
    }
    // ViewShotからキャプチャーを取得
    const uri = await viewShot?.current?.capture?.();
    if (uri) {
      // 取得した画像をカメラロールに保存
      CameraRoll.save(uri);
    }
  }, []);
  const {img} = props;
  if (!img) return null;

  return (
    <View style={{margin: 4}}>
      <ViewShot ref={viewShot}>
        <Text
          style={{
            position: 'absolute',
            top: 10,
            left: 10,
            fontWeight: 'bold',
            color: 'white',
            backgroundColor: '#0f83fd',
            zIndex: 1,
          }}>
          ほげほげ
        </Text>
        <Image
          source={{uri: img.path}}
          style={{width: img.width, height: img.height}}
          height={img.height}
          width={img.width}
        />
      </ViewShot>
      <Button title="保存する" onPress={onPressSave} />
    </View>
  );
};

crop-image-pickerで任意のサイズで画像を選択

コード中ではAppと名前が付けられたコンポーネントです。
ImagePicker.openPicker()で画像選択を行い、そのオプションで「切り取りの有無、サイズ」を指定しています。
react-native-crop-image-pickerでは複数の画像を切り取ったり、切り取らずにそのままのサイズで画像を選択したりもできます。

ここで切り取った画像はいったんstateに格納され、先ほどのDecoratedImageに渡されます。

const App: React.FC = () => {
  // 選択中の画像
  const [selectedImage, setSelectedImage] = useState<CroppedImage | null>(null);
  // 画像選択ボタン押下時処理
  const onPressSelectPhoto = useCallback(() => {
    // crop-image-pickerを起動
    ImagePicker.openPicker({
      width: 300,
      height: 300,
      cropping: true,
    })
      .then(image => {
        // 正常に取得できたらstateにセット
        setSelectedImage(image);
      })
      .catch(err => {
        console.log(err);
      });
  }, []);

  return (
    <SafeAreaView>
      <StatusBar />
      <View
        style={{
          width: '100%',
          height: '100%',
          justifyContent: 'center',
          alignItems: 'center',
        }}>
        <DecoratedImage img={selectedImage} />
        <Button
          title={`画像${selectedImage ? '再' : ''}選択`}
          onPress={onPressSelectPhoto}
        />
      </View>
    </SafeAreaView>
  );
};

まとめ

今回はReactNativeアプリで簡単な画像加工アプリを実装した際に使用したライブラリと、そのコードについてポイントを紹介しました。
この内容をブラッシュアップすることでInstagramSNOWのようなアプリにある「フレーム」や「フィルター」が扱えるようになるかと思います。

多少ネイティブの知識が必要になってきますが、ReactNativeで作れるアプリの幅が広がるので、今後も継続的に学習内容を共有していきたいと思います。