📸
Expoでカメラアプリ機能を実装してみた
Expo Camera App
このプロジェクトは、Expo と React Native を使用してモバイルカメラアプリケーションを実装したものです。
カメラの動作検証には実機が必要なのでお持ちのiPhoneかAndroidを使って実験してみてください🧪
環境構築
必要条件
セットアップ手順
- プロジェクトのクローン(または新規作成):
# 新規プロジェクトを作成する場合
bunx create-expo-app expo-camera
cd expo-camera
# または既存のプロジェクトをクローンした場合
cd expo-camera
bun install
- 必要なパッケージのインストール:
bun add expo-camera expo-media-library
- アプリの起動:
bun start
カメラ機能の実装
主な機能
- カメラのプレビュー表示
- フロント/バックカメラの切り替え
- フラッシュモードの切り替え
- 写真撮影とギャラリーへの保存
必要な権限
アプリを実行するには以下の権限が必要です:
- カメラへのアクセス権限
- メディアライブラリへの保存権限
実装例
App.tsx
にカメラ機能を実装しています。主な実装内容:
- カメラのパーミッション取得
const [permission, requestPermission] = useCameraPermissions();
const [mediaPermission, requestMediaPermission] = MediaLibrary.usePermissions();
- カメラの設定
const [facing, setFacing] = useState<CameraType>('back');
const [flash, setFlash] = useState<FlashMode>('off');
- 写真撮影機能
const takePicture = async () => {
if (!cameraRef.current) return;
try {
const photo = await cameraRef.current.takePictureAsync();
await MediaLibrary.saveToLibraryAsync(photo.uri);
} catch (error) {
console.error(error);
}
};
トラブルシューティング
- パーミッションエラー
- アプリの設定から、カメラとメディアライブラリへのアクセスを許可してください。
- カメラが起動しない場合
- 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と比較するとカメラの実装は割と早く簡単できたかも?
Discussion