📸
【Flutter】無音カメラを実装してみた
この記事は
シャッター音のならない、カメラアプリを作りたーい
結論
カメラを起動後に撮影ボタンをタップすると、カメラを一時停止してスクリーンショットする。
これにより、無音でカメラ撮影ができました!
1. パッケージの追加
デバイス上のカメラ操作ができる
スクリーンショットを撮ってくれる 画像を端末に保存してくれる2. プレビューを実装
デザインは、BeRealっぽく🎵
2本指でズームできるようにします。
camera_preview.dart
class CameraPreviewWidget extends StatefulWidget {
const CameraPreviewWidget({
super.key,
required this.controller,
required this.minAvailableZoom,
required this.maxAvailableZoom,
this.borderRadius = 60,
});
final CameraController controller;
final double minAvailableZoom;
final double maxAvailableZoom;
final double borderRadius;
State<CameraPreviewWidget> createState() => _CameraPreviewWidgetState();
}
class _CameraPreviewWidgetState extends State<CameraPreviewWidget> {
double _currentScale = 1;
double _baseScale = 1;
int _pointers = 0;
Widget build(BuildContext context) {
return Listener(
onPointerDown: (_) => _pointers++,
onPointerUp: (_) => _pointers--,
child: ClipRRect(
borderRadius: BorderRadius.circular(widget.borderRadius),
child: CameraPreview(
widget.controller,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onScaleStart: _handleScaleStart,
onScaleUpdate: _handleScaleUpdate,
onTapDown: (TapDownDetails details) =>
onViewFinderTap(details, constraints),
);
},
),
),
),
);
}
void _handleScaleStart(ScaleStartDetails details) {
_baseScale = _currentScale;
}
Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
if (_pointers != 2) {
return;
}
_currentScale = (_baseScale * details.scale)
.clamp(widget.minAvailableZoom, widget.maxAvailableZoom);
await widget.controller.setZoomLevel(_currentScale);
}
void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
final Offset offset = Offset(
details.localPosition.dx / constraints.maxWidth,
details.localPosition.dy / constraints.maxHeight,
);
widget.controller.setExposurePoint(offset);
widget.controller.setFocusPoint(offset);
}
}
3. カメラを実装
コントローラの初期化をします。
camera_screen_v3.dart
final cameras = useState<List<CameraDescription>>([]);
final controller = useState<CameraController?>(null);
final initializeCameraController = useCallback(
({
required CameraDescription camera,
required ValueNotifier<CameraController?> controller,
required ValueNotifier<bool> cameraLoaded,
}) async {
try {
final cameraController = CameraController(
camera,
// 画質の設定
ResolutionPreset.high,
);
controller.value = cameraController;
await cameraController.initialize();
cameraLoaded.value = true;
} on CameraException catch (error) {
if (error.code == 'CameraAccessDenied') {}
rethrow;
}
},
[],
);
useEffect(
() {
availableCameras().then((availableCameras) {
cameras.value = availableCameras;
if (availableCameras.isEmpty) return;
initializeCameraController(
camera: availableCameras.first,
controller: controller,
cameraLoaded: cameraLoaded,
);
});
return () {
controller.value?.dispose();
};
},
[],
);
4. 撮影を実装
スクリーンショットパッケージのcaptureFromWidgetメソッド内で、Widgetを定義します。撮影時にWidget(ここではプレビュー)が計算して、画像データにしてくれます。
また、撮影時に計算してくれるので、プレビューでは角丸だったのが、保存された写真は長方形になってくれます。
camera_screen_v3.dart
final takePicture = useCallback(
() async {
try {
if (isTakingPicture.value) return;
final cameraController = controller.value;
if (cameraController == null ||
!cameraController.value.isInitialized) {
return;
}
isTakingPicture.value = true;
// カメラを一時停止
await cameraController.pausePreview();
// スクリーンショットの撮影
final capturedImage = await screenshotController.captureFromWidget(
CameraPreviewWidget(
controller: cameraController,
minAvailableZoom: minAvailableZoom.value,
maxAvailableZoom: maxAvailableZoom.value,
borderRadius: 0,
),
);
// 画像保存
await ImageGallerySaver.saveImage(capturedImage);
// カメラの再開
await cameraController.resumePreview();
} catch (e) {
debugPrint('写真撮影エラー: $e');
} finally {
isTakingPicture.value = false;
}
},
[
controller,
isTakingPicture,
minAvailableZoom,
maxAvailableZoom,
],
);
5. カメラの向き切り替えを実装
camera_screen_v3.dart
final switchCamera = useCallback(
({
required List<CameraDescription> cameras,
required CameraController currentController,
required ValueNotifier<CameraController?> controller,
required ValueNotifier<bool> cameraLoaded,
required ValueNotifier<bool> isChangingCamera,
}) async {
final description = controller.value!.description;
final cameraDescription = cameras.firstWhere((element) {
final direction =
description.lensDirection == CameraLensDirection.front
? CameraLensDirection.back
: CameraLensDirection.front;
return element.lensDirection == direction;
});
if (controller.value != null) {
return controller.value!.setDescription(cameraDescription);
} else {
return initializeCameraController(
camera: cameraDescription,
controller: controller,
cameraLoaded: cameraLoaded,
);
}
},
[controller.value],
);
5. 撮影後にアニメーション追加
seekbar.dart
class SeekBar extends StatefulWidget {
const SeekBar({super.key, required this.progress});
final double progress;
State<SeekBar> createState() => _SeekBarState();
}
class _SeekBarState extends State<SeekBar> {
Widget build(BuildContext context) {
return CircularSeekBar(
width: double.infinity,
height: 250,
progress: widget.progress,
barWidth: 8,
startAngle: 45,
sweepAngle: 270,
strokeCap: StrokeCap.butt,
progressGradientColors: const [
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.indigo,
Colors.purple,
],
dashWidth: 1,
dashGap: 2,
);
}
}
おわり
すべてのコード
アイコン作成
AIに生成してもらいました。
作品名:カメラのレンズ越しの美女
Discussion