📸

Expoでカメラアプリ機能を実装してみた

2024/12/12に公開

Expo Camera App

このプロジェクトは、Expo と React Native を使用してモバイルカメラアプリケーションを実装したものです。

完成品

カメラの動作検証には実機が必要なのでお持ちのiPhoneかAndroidを使って実験してみてください🧪

環境構築

必要条件

  • Bun がインストールされていること
  • Expo Go アプリ(iOS/Android)がインストールされていること

セットアップ手順

  1. プロジェクトのクローン(または新規作成):
# 新規プロジェクトを作成する場合
bunx create-expo-app expo-camera
cd expo-camera

# または既存のプロジェクトをクローンした場合
cd expo-camera
bun install
  1. 必要なパッケージのインストール:
bun add expo-camera expo-media-library
  1. アプリの起動:
bun start

カメラ機能の実装

主な機能

  • カメラのプレビュー表示
  • フロント/バックカメラの切り替え
  • フラッシュモードの切り替え
  • 写真撮影とギャラリーへの保存

必要な権限

アプリを実行するには以下の権限が必要です:

  • カメラへのアクセス権限
  • メディアライブラリへの保存権限

実装例

App.tsx にカメラ機能を実装しています。主な実装内容:

  1. カメラのパーミッション取得
const [permission, requestPermission] = useCameraPermissions();
const [mediaPermission, requestMediaPermission] = MediaLibrary.usePermissions();
  1. カメラの設定
const [facing, setFacing] = useState<CameraType>('back');
const [flash, setFlash] = useState<FlashMode>('off');
  1. 写真撮影機能
const takePicture = async () => {
  if (!cameraRef.current) return;
  try {
    const photo = await cameraRef.current.takePictureAsync();
    await MediaLibrary.saveToLibraryAsync(photo.uri);
  } catch (error) {
    console.error(error);
  }
};

トラブルシューティング

  1. パーミッションエラー
  • アプリの設定から、カメラとメディアライブラリへのアクセスを許可してください。
  1. カメラが起動しない場合
  • Expo Go アプリを再起動してください。
  • デバイスを再起動してください。

開発環境のヒント

  • 開発中は実機での動作確認を推奨します(エミュレータでもカメラ機能は使用可能ですが、実機の方がスムーズです)。
  • Expo Go アプリを使用することで、簡単にアプリのテストが可能です。

参考リンク

example

iPhoneとAndroidの端末に撮影した画像を保存する機能はこのコードで実装できます。

全体のコード
App.tsx
import { CameraView, CameraType, useCameraPermissions, FlashMode } from 'expo-camera';
import { useState, useRef } from 'react';
import { Button, StyleSheet, Text, TouchableOpacity, View, Alert } from 'react-native';
import * as MediaLibrary from 'expo-media-library';

export default function App() {
  const [facing, setFacing] = useState<CameraType>('back');
  const [flash, setFlash] = useState<FlashMode>('off');
  const [zoom, setZoom] = useState(0);
  const [permission, requestPermission] = useCameraPermissions();
  const [mediaPermission, requestMediaPermission] = MediaLibrary.usePermissions();
  const cameraRef = useRef<any>(null);

  if (!permission || !mediaPermission) {
    return <View />;
  }

  if (!permission.granted || !mediaPermission.granted) {
    return (
      <View style={styles.container}>
        <Text style={styles.message}>カメラとメディアライブラリのアクセス許可が必要です</Text>
        <Button onPress={() => {
          requestPermission();
          requestMediaPermission();
        }} title="許可する" />
      </View>
    );
  }

  const toggleCameraFacing = () => {
    setFacing(current => (current === 'back' ? 'front' : 'back'));
  };

  const toggleFlash = () => {
    setFlash(current => {
      switch (current) {
        case 'off': return 'on';
        case 'on': return 'auto';
        default: return 'off';
      }
    });
  };

  const handleZoom = (direction: 'in' | 'out') => {
    setZoom(currentZoom => {
      const newZoom = direction === 'in'
        ? Math.min(currentZoom + 0.1, 1)
        : Math.max(currentZoom - 0.1, 0);
      return Number(newZoom.toFixed(1));
    });
  };

  const takePicture = async () => {
    if (!cameraRef.current) return;

    try {
      const photo = await cameraRef.current.takePictureAsync();
      await MediaLibrary.saveToLibraryAsync(photo.uri);
      Alert.alert('成功', '写真を保存しました!');
    } catch (error) {
      Alert.alert('エラー', '写真の撮影または保存に失敗しました');
    }
  };

  return (
    <View style={styles.container}>
      <CameraView
        ref={cameraRef}
        style={styles.camera}
        facing={facing}
        zoom={zoom}
        flash={flash}
      />
      <View style={styles.buttonContainer}>
        <TouchableOpacity style={styles.circleButton} onPress={toggleCameraFacing}>
          <Text style={styles.buttonText}>📷</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.circleButton} onPress={toggleFlash}>
          <Text style={styles.buttonText}>{flash === 'off' ? '🔦' : '💡'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.captureButton} onPress={takePicture}>
          <View style={styles.captureButtonInner} />
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  camera: {
    flex: 1,
  },
  buttonContainer: {
    position: 'absolute',
    bottom: 40,
    flexDirection: 'row',
    width: '100%',
    justifyContent: 'space-evenly',
    alignItems: 'center',
    paddingHorizontal: 20,
  },
  circleButton: {
    width: 60,
    height: 60,
    borderRadius: 30,
    backgroundColor: 'rgba(255, 255, 255, 0.3)',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 1,
    borderColor: 'white',
  },
  buttonText: {
    fontSize: 24,
  },
  message: {
    fontSize: 18,
    textAlign: 'center',
    marginBottom: 20,
    color: 'black',
  },
  captureButton: {
    width: 70,
    height: 70,
    borderRadius: 35,
    backgroundColor: 'rgba(255, 255, 255, 0.3)',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 2,
    borderColor: 'white',
  },
  captureButtonInner: {
    width: 60,
    height: 60,
    borderRadius: 30,
    backgroundColor: 'white',
  },
});

Expoは、app.jsonを設定すればネイティブのコードをいじらなくも権限の設定できるっぽい?

権限の許可

公式の解説を参考にしつつカメラ使用の権限の許可をする。

app.json
{
  "expo": {
    "name": "expo-camera",
    "slug": "expo-camera",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "light",
    "newArchEnabled": true,
    "splash": {
      "image": "./assets/splash-icon.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "ios": {
      "supportsTablet": true
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      }
    },
    "web": {
      "favicon": "./assets/favicon.png"
    },
    "plugins": [
      [
        "expo-camera",
        {
          "cameraPermission": "カメラへのアクセスを許可しますか?",
          "microphonePermission": "マイクへのアクセスを許可しますか?",
          "recordAudioAndroid": true
        }
      ]
    ]
  }
}

iOSとAndroidでそれぞれ動作検証してみた。慣れている人なら、SwiftUIとJetpack Comopseで実装した方が早いと思います。Flutterと比較するとカメラの実装は割と早く簡単できたかも?

https://x.com/JBOY83062526/status/1867003304129204461
https://x.com/JBOY83062526/status/1867003650629046779

Jboy王国メディア

Discussion