| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| import 'dart:math'; |
| |
| import 'package:camera_platform_interface/camera_platform_interface.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:video_player/video_player.dart'; |
| |
| import 'camera_controller.dart'; |
| import 'camera_preview.dart'; |
| |
| /// Camera example home widget. |
| class CameraExampleHome extends StatefulWidget { |
| /// Default Constructor |
| const CameraExampleHome({super.key}); |
| |
| @override |
| State<CameraExampleHome> createState() { |
| return _CameraExampleHomeState(); |
| } |
| } |
| |
| /// Returns a suitable camera icon for [direction]. |
| IconData getCameraLensIcon(CameraLensDirection direction) { |
| switch (direction) { |
| case CameraLensDirection.back: |
| return Icons.camera_rear; |
| case CameraLensDirection.front: |
| return Icons.camera_front; |
| case CameraLensDirection.external: |
| return Icons.camera; |
| } |
| // This enum is from a different package, so a new value could be added at |
| // any time. The example should keep working if that happens. |
| // ignore: dead_code |
| return Icons.camera; |
| } |
| |
| void _logError(String code, String? message) { |
| // ignore: avoid_print |
| print('Error: $code${message == null ? '' : '\nError Message: $message'}'); |
| } |
| |
| class _CameraExampleHomeState extends State<CameraExampleHome> |
| with WidgetsBindingObserver, TickerProviderStateMixin { |
| CameraController? controller; |
| XFile? imageFile; |
| XFile? videoFile; |
| VideoPlayerController? videoController; |
| VoidCallback? videoPlayerListener; |
| bool enableAudio = true; |
| double _minAvailableExposureOffset = 0.0; |
| double _maxAvailableExposureOffset = 0.0; |
| double _currentExposureOffset = 0.0; |
| late AnimationController _flashModeControlRowAnimationController; |
| late Animation<double> _flashModeControlRowAnimation; |
| late AnimationController _exposureModeControlRowAnimationController; |
| late Animation<double> _exposureModeControlRowAnimation; |
| late AnimationController _focusModeControlRowAnimationController; |
| late Animation<double> _focusModeControlRowAnimation; |
| double _minAvailableZoom = 1.0; |
| double _maxAvailableZoom = 1.0; |
| double _currentScale = 1.0; |
| double _baseScale = 1.0; |
| |
| // Counting pointers (number of user fingers on screen) |
| int _pointers = 0; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _ambiguate(WidgetsBinding.instance)?.addObserver(this); |
| |
| _flashModeControlRowAnimationController = AnimationController( |
| duration: const Duration(milliseconds: 300), |
| vsync: this, |
| ); |
| _flashModeControlRowAnimation = CurvedAnimation( |
| parent: _flashModeControlRowAnimationController, |
| curve: Curves.easeInCubic, |
| ); |
| _exposureModeControlRowAnimationController = AnimationController( |
| duration: const Duration(milliseconds: 300), |
| vsync: this, |
| ); |
| _exposureModeControlRowAnimation = CurvedAnimation( |
| parent: _exposureModeControlRowAnimationController, |
| curve: Curves.easeInCubic, |
| ); |
| _focusModeControlRowAnimationController = AnimationController( |
| duration: const Duration(milliseconds: 300), |
| vsync: this, |
| ); |
| _focusModeControlRowAnimation = CurvedAnimation( |
| parent: _focusModeControlRowAnimationController, |
| curve: Curves.easeInCubic, |
| ); |
| } |
| |
| @override |
| void dispose() { |
| _ambiguate(WidgetsBinding.instance)?.removeObserver(this); |
| _flashModeControlRowAnimationController.dispose(); |
| _exposureModeControlRowAnimationController.dispose(); |
| super.dispose(); |
| } |
| |
| @override |
| void didChangeAppLifecycleState(AppLifecycleState state) { |
| final CameraController? cameraController = controller; |
| |
| // App state changed before we got the chance to initialize. |
| if (cameraController == null || !cameraController.value.isInitialized) { |
| return; |
| } |
| |
| if (state == AppLifecycleState.inactive) { |
| cameraController.dispose(); |
| } else if (state == AppLifecycleState.resumed) { |
| _initializeCameraController(cameraController.description); |
| } |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return Scaffold( |
| appBar: AppBar( |
| title: const Text('Camera example'), |
| ), |
| body: Column( |
| children: <Widget>[ |
| Expanded( |
| child: Container( |
| decoration: BoxDecoration( |
| color: Colors.black, |
| border: Border.all( |
| color: |
| controller != null && controller!.value.isRecordingVideo |
| ? Colors.redAccent |
| : Colors.grey, |
| width: 3.0, |
| ), |
| ), |
| child: Padding( |
| padding: const EdgeInsets.all(1.0), |
| child: Center( |
| child: _cameraPreviewWidget(), |
| ), |
| ), |
| ), |
| ), |
| _captureControlRowWidget(), |
| _modeControlRowWidget(), |
| Padding( |
| padding: const EdgeInsets.all(5.0), |
| child: Row( |
| children: <Widget>[ |
| _cameraTogglesRowWidget(), |
| _thumbnailWidget(), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |
| |
| /// Display the preview from the camera (or a message if the preview is not available). |
| Widget _cameraPreviewWidget() { |
| final CameraController? cameraController = controller; |
| |
| if (cameraController == null || !cameraController.value.isInitialized) { |
| return const Text( |
| 'Tap a camera', |
| style: TextStyle( |
| color: Colors.white, |
| fontSize: 24.0, |
| fontWeight: FontWeight.w900, |
| ), |
| ); |
| } else { |
| return Listener( |
| onPointerDown: (_) => _pointers++, |
| onPointerUp: (_) => _pointers--, |
| child: CameraPreview( |
| 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 { |
| // When there are not exactly two fingers on screen don't scale |
| if (controller == null || _pointers != 2) { |
| return; |
| } |
| |
| _currentScale = (_baseScale * details.scale) |
| .clamp(_minAvailableZoom, _maxAvailableZoom); |
| |
| await CameraPlatform.instance |
| .setZoomLevel(controller!.cameraId, _currentScale); |
| } |
| |
| /// Display the thumbnail of the captured image or video. |
| Widget _thumbnailWidget() { |
| final VideoPlayerController? localVideoController = videoController; |
| |
| return Expanded( |
| child: Align( |
| alignment: Alignment.centerRight, |
| child: Row( |
| mainAxisSize: MainAxisSize.min, |
| children: <Widget>[ |
| if (localVideoController == null && imageFile == null) |
| Container() |
| else |
| SizedBox( |
| width: 64.0, |
| height: 64.0, |
| child: (localVideoController == null) |
| ? ( |
| // The captured image on the web contains a network-accessible URL |
| // pointing to a location within the browser. It may be displayed |
| // either with Image.network or Image.memory after loading the image |
| // bytes to memory. |
| kIsWeb |
| ? Image.network(imageFile!.path) |
| : Image.file(File(imageFile!.path))) |
| : Container( |
| decoration: BoxDecoration( |
| border: Border.all(color: Colors.pink)), |
| child: Center( |
| child: AspectRatio( |
| aspectRatio: |
| localVideoController.value.size != null |
| ? localVideoController.value.aspectRatio |
| : 1.0, |
| child: VideoPlayer(localVideoController)), |
| ), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| /// Display a bar with buttons to change the flash and exposure modes |
| Widget _modeControlRowWidget() { |
| return Column( |
| children: <Widget>[ |
| Row( |
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| children: <Widget>[ |
| IconButton( |
| icon: const Icon(Icons.flash_on), |
| color: Colors.blue, |
| onPressed: controller != null ? onFlashModeButtonPressed : null, |
| ), |
| // The exposure and focus mode are currently not supported on the web. |
| ...!kIsWeb |
| ? <Widget>[ |
| IconButton( |
| icon: const Icon(Icons.exposure), |
| color: Colors.blue, |
| onPressed: controller != null |
| ? onExposureModeButtonPressed |
| : null, |
| ), |
| IconButton( |
| icon: const Icon(Icons.filter_center_focus), |
| color: Colors.blue, |
| onPressed: |
| controller != null ? onFocusModeButtonPressed : null, |
| ) |
| ] |
| : <Widget>[], |
| IconButton( |
| icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), |
| color: Colors.blue, |
| onPressed: controller != null ? onAudioModeButtonPressed : null, |
| ), |
| IconButton( |
| icon: Icon(controller?.value.isCaptureOrientationLocked ?? false |
| ? Icons.screen_lock_rotation |
| : Icons.screen_rotation), |
| color: Colors.blue, |
| onPressed: controller != null |
| ? onCaptureOrientationLockButtonPressed |
| : null, |
| ), |
| ], |
| ), |
| _flashModeControlRowWidget(), |
| _exposureModeControlRowWidget(), |
| _focusModeControlRowWidget(), |
| ], |
| ); |
| } |
| |
| Widget _flashModeControlRowWidget() { |
| return SizeTransition( |
| sizeFactor: _flashModeControlRowAnimation, |
| child: ClipRect( |
| child: Row( |
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| children: <Widget>[ |
| IconButton( |
| icon: const Icon(Icons.flash_off), |
| color: controller?.value.flashMode == FlashMode.off |
| ? Colors.orange |
| : Colors.blue, |
| onPressed: controller != null |
| ? () => onSetFlashModeButtonPressed(FlashMode.off) |
| : null, |
| ), |
| IconButton( |
| icon: const Icon(Icons.flash_auto), |
| color: controller?.value.flashMode == FlashMode.auto |
| ? Colors.orange |
| : Colors.blue, |
| onPressed: controller != null |
| ? () => onSetFlashModeButtonPressed(FlashMode.auto) |
| : null, |
| ), |
| IconButton( |
| icon: const Icon(Icons.flash_on), |
| color: controller?.value.flashMode == FlashMode.always |
| ? Colors.orange |
| : Colors.blue, |
| onPressed: controller != null |
| ? () => onSetFlashModeButtonPressed(FlashMode.always) |
| : null, |
| ), |
| IconButton( |
| icon: const Icon(Icons.highlight), |
| color: controller?.value.flashMode == FlashMode.torch |
| ? Colors.orange |
| : Colors.blue, |
| onPressed: controller != null |
| ? () => onSetFlashModeButtonPressed(FlashMode.torch) |
| : null, |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| Widget _exposureModeControlRowWidget() { |
| final ButtonStyle styleAuto = TextButton.styleFrom( |
| // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 |
| // ignore: deprecated_member_use |
| primary: controller?.value.exposureMode == ExposureMode.auto |
| ? Colors.orange |
| : Colors.blue, |
| ); |
| final ButtonStyle styleLocked = TextButton.styleFrom( |
| // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 |
| // ignore: deprecated_member_use |
| primary: controller?.value.exposureMode == ExposureMode.locked |
| ? Colors.orange |
| : Colors.blue, |
| ); |
| |
| return SizeTransition( |
| sizeFactor: _exposureModeControlRowAnimation, |
| child: ClipRect( |
| child: Container( |
| color: Colors.grey.shade50, |
| child: Column( |
| children: <Widget>[ |
| const Center( |
| child: Text('Exposure Mode'), |
| ), |
| Row( |
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| children: <Widget>[ |
| TextButton( |
| style: styleAuto, |
| onPressed: controller != null |
| ? () => |
| onSetExposureModeButtonPressed(ExposureMode.auto) |
| : null, |
| onLongPress: () { |
| if (controller != null) { |
| CameraPlatform.instance |
| .setExposurePoint(controller!.cameraId, null); |
| showInSnackBar('Resetting exposure point'); |
| } |
| }, |
| child: const Text('AUTO'), |
| ), |
| TextButton( |
| style: styleLocked, |
| onPressed: controller != null |
| ? () => |
| onSetExposureModeButtonPressed(ExposureMode.locked) |
| : null, |
| child: const Text('LOCKED'), |
| ), |
| TextButton( |
| style: styleLocked, |
| onPressed: controller != null |
| ? () => controller!.setExposureOffset(0.0) |
| : null, |
| child: const Text('RESET OFFSET'), |
| ), |
| ], |
| ), |
| const Center( |
| child: Text('Exposure Offset'), |
| ), |
| Row( |
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| children: <Widget>[ |
| Text(_minAvailableExposureOffset.toString()), |
| Slider( |
| value: _currentExposureOffset, |
| min: _minAvailableExposureOffset, |
| max: _maxAvailableExposureOffset, |
| label: _currentExposureOffset.toString(), |
| onChanged: _minAvailableExposureOffset == |
| _maxAvailableExposureOffset |
| ? null |
| : setExposureOffset, |
| ), |
| Text(_maxAvailableExposureOffset.toString()), |
| ], |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Widget _focusModeControlRowWidget() { |
| final ButtonStyle styleAuto = TextButton.styleFrom( |
| // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 |
| // ignore: deprecated_member_use |
| primary: controller?.value.focusMode == FocusMode.auto |
| ? Colors.orange |
| : Colors.blue, |
| ); |
| final ButtonStyle styleLocked = TextButton.styleFrom( |
| // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 |
| // ignore: deprecated_member_use |
| primary: controller?.value.focusMode == FocusMode.locked |
| ? Colors.orange |
| : Colors.blue, |
| ); |
| |
| return SizeTransition( |
| sizeFactor: _focusModeControlRowAnimation, |
| child: ClipRect( |
| child: Container( |
| color: Colors.grey.shade50, |
| child: Column( |
| children: <Widget>[ |
| const Center( |
| child: Text('Focus Mode'), |
| ), |
| Row( |
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| children: <Widget>[ |
| TextButton( |
| style: styleAuto, |
| onPressed: controller != null |
| ? () => onSetFocusModeButtonPressed(FocusMode.auto) |
| : null, |
| onLongPress: () { |
| if (controller != null) { |
| CameraPlatform.instance |
| .setFocusPoint(controller!.cameraId, null); |
| } |
| showInSnackBar('Resetting focus point'); |
| }, |
| child: const Text('AUTO'), |
| ), |
| TextButton( |
| style: styleLocked, |
| onPressed: controller != null |
| ? () => onSetFocusModeButtonPressed(FocusMode.locked) |
| : null, |
| child: const Text('LOCKED'), |
| ), |
| ], |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| /// Display the control bar with buttons to take pictures and record videos. |
| Widget _captureControlRowWidget() { |
| final CameraController? cameraController = controller; |
| |
| return Row( |
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| children: <Widget>[ |
| IconButton( |
| icon: const Icon(Icons.camera_alt), |
| color: Colors.blue, |
| onPressed: cameraController != null && |
| cameraController.value.isInitialized && |
| !cameraController.value.isRecordingVideo |
| ? onTakePictureButtonPressed |
| : null, |
| ), |
| IconButton( |
| icon: const Icon(Icons.videocam), |
| color: Colors.blue, |
| onPressed: cameraController != null && |
| cameraController.value.isInitialized && |
| !cameraController.value.isRecordingVideo |
| ? onVideoRecordButtonPressed |
| : null, |
| ), |
| IconButton( |
| icon: cameraController != null && |
| (!cameraController.value.isRecordingVideo || |
| cameraController.value.isRecordingPaused) |
| ? const Icon(Icons.play_arrow) |
| : const Icon(Icons.pause), |
| color: Colors.blue, |
| onPressed: cameraController != null && |
| cameraController.value.isInitialized && |
| cameraController.value.isRecordingVideo |
| ? (cameraController.value.isRecordingPaused) |
| ? onResumeButtonPressed |
| : onPauseButtonPressed |
| : null, |
| ), |
| IconButton( |
| icon: const Icon(Icons.stop), |
| color: Colors.red, |
| onPressed: cameraController != null && |
| cameraController.value.isInitialized && |
| cameraController.value.isRecordingVideo |
| ? onStopButtonPressed |
| : null, |
| ), |
| IconButton( |
| icon: const Icon(Icons.pause_presentation), |
| color: |
| cameraController != null && cameraController.value.isPreviewPaused |
| ? Colors.red |
| : Colors.blue, |
| onPressed: |
| cameraController == null ? null : onPausePreviewButtonPressed, |
| ), |
| ], |
| ); |
| } |
| |
| /// Display a row of toggle to select the camera (or a message if no camera is available). |
| Widget _cameraTogglesRowWidget() { |
| final List<Widget> toggles = <Widget>[]; |
| |
| void onChanged(CameraDescription? description) { |
| if (description == null) { |
| return; |
| } |
| |
| onNewCameraSelected(description); |
| } |
| |
| if (_cameras.isEmpty) { |
| _ambiguate(SchedulerBinding.instance)?.addPostFrameCallback((_) async { |
| showInSnackBar('No camera found.'); |
| }); |
| return const Text('None'); |
| } else { |
| for (final CameraDescription cameraDescription in _cameras) { |
| toggles.add( |
| SizedBox( |
| width: 90.0, |
| child: RadioListTile<CameraDescription>( |
| title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), |
| groupValue: controller?.description, |
| value: cameraDescription, |
| onChanged: onChanged, |
| ), |
| ), |
| ); |
| } |
| } |
| |
| return Row(children: toggles); |
| } |
| |
| String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); |
| |
| void showInSnackBar(String message) { |
| ScaffoldMessenger.of(context) |
| .showSnackBar(SnackBar(content: Text(message))); |
| } |
| |
| void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { |
| if (controller == null) { |
| return; |
| } |
| |
| final CameraController cameraController = controller!; |
| |
| final Point<double> point = Point<double>( |
| details.localPosition.dx / constraints.maxWidth, |
| details.localPosition.dy / constraints.maxHeight, |
| ); |
| CameraPlatform.instance.setExposurePoint(cameraController.cameraId, point); |
| CameraPlatform.instance.setFocusPoint(cameraController.cameraId, point); |
| } |
| |
| Future<void> onNewCameraSelected(CameraDescription cameraDescription) async { |
| if (controller != null) { |
| return controller!.setDescription(cameraDescription); |
| } else { |
| return _initializeCameraController(cameraDescription); |
| } |
| } |
| |
| Future<void> _initializeCameraController( |
| CameraDescription cameraDescription) async { |
| final CameraController cameraController = CameraController( |
| cameraDescription, |
| kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, |
| enableAudio: enableAudio, |
| imageFormatGroup: ImageFormatGroup.jpeg, |
| ); |
| |
| controller = cameraController; |
| |
| // If the controller is updated then update the UI. |
| cameraController.addListener(() { |
| if (mounted) { |
| setState(() {}); |
| } |
| }); |
| |
| try { |
| await cameraController.initialize(); |
| await Future.wait(<Future<Object?>>[ |
| // The exposure mode is currently not supported on the web. |
| ...!kIsWeb |
| ? <Future<Object?>>[ |
| CameraPlatform.instance |
| .getMinExposureOffset(cameraController.cameraId) |
| .then( |
| (double value) => _minAvailableExposureOffset = value), |
| CameraPlatform.instance |
| .getMaxExposureOffset(cameraController.cameraId) |
| .then((double value) => _maxAvailableExposureOffset = value) |
| ] |
| : <Future<Object?>>[], |
| CameraPlatform.instance |
| .getMaxZoomLevel(cameraController.cameraId) |
| .then((double value) => _maxAvailableZoom = value), |
| CameraPlatform.instance |
| .getMinZoomLevel(cameraController.cameraId) |
| .then((double value) => _minAvailableZoom = value), |
| ]); |
| } on CameraException catch (e) { |
| switch (e.code) { |
| case 'CameraAccessDenied': |
| showInSnackBar('You have denied camera access.'); |
| break; |
| case 'CameraAccessDeniedWithoutPrompt': |
| // iOS only |
| showInSnackBar('Please go to Settings app to enable camera access.'); |
| break; |
| case 'CameraAccessRestricted': |
| // iOS only |
| showInSnackBar('Camera access is restricted.'); |
| break; |
| case 'AudioAccessDenied': |
| showInSnackBar('You have denied audio access.'); |
| break; |
| case 'AudioAccessDeniedWithoutPrompt': |
| // iOS only |
| showInSnackBar('Please go to Settings app to enable audio access.'); |
| break; |
| case 'AudioAccessRestricted': |
| // iOS only |
| showInSnackBar('Audio access is restricted.'); |
| break; |
| case 'cameraPermission': |
| // Android & web only |
| showInSnackBar('Unknown permission error.'); |
| break; |
| default: |
| _showCameraException(e); |
| break; |
| } |
| } |
| |
| if (mounted) { |
| setState(() {}); |
| } |
| } |
| |
| void onTakePictureButtonPressed() { |
| takePicture().then((XFile? file) { |
| if (mounted) { |
| setState(() { |
| imageFile = file; |
| videoController?.dispose(); |
| videoController = null; |
| }); |
| if (file != null) { |
| showInSnackBar('Picture saved to ${file.path}'); |
| } |
| } |
| }); |
| } |
| |
| void onFlashModeButtonPressed() { |
| if (_flashModeControlRowAnimationController.value == 1) { |
| _flashModeControlRowAnimationController.reverse(); |
| } else { |
| _flashModeControlRowAnimationController.forward(); |
| _exposureModeControlRowAnimationController.reverse(); |
| _focusModeControlRowAnimationController.reverse(); |
| } |
| } |
| |
| void onExposureModeButtonPressed() { |
| if (_exposureModeControlRowAnimationController.value == 1) { |
| _exposureModeControlRowAnimationController.reverse(); |
| } else { |
| _exposureModeControlRowAnimationController.forward(); |
| _flashModeControlRowAnimationController.reverse(); |
| _focusModeControlRowAnimationController.reverse(); |
| } |
| } |
| |
| void onFocusModeButtonPressed() { |
| if (_focusModeControlRowAnimationController.value == 1) { |
| _focusModeControlRowAnimationController.reverse(); |
| } else { |
| _focusModeControlRowAnimationController.forward(); |
| _flashModeControlRowAnimationController.reverse(); |
| _exposureModeControlRowAnimationController.reverse(); |
| } |
| } |
| |
| void onAudioModeButtonPressed() { |
| enableAudio = !enableAudio; |
| if (controller != null) { |
| onNewCameraSelected(controller!.description); |
| } |
| } |
| |
| Future<void> onCaptureOrientationLockButtonPressed() async { |
| try { |
| if (controller != null) { |
| final CameraController cameraController = controller!; |
| if (cameraController.value.isCaptureOrientationLocked) { |
| await cameraController.unlockCaptureOrientation(); |
| showInSnackBar('Capture orientation unlocked'); |
| } else { |
| await cameraController.lockCaptureOrientation(); |
| showInSnackBar( |
| 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); |
| } |
| } |
| } on CameraException catch (e) { |
| _showCameraException(e); |
| } |
| } |
| |
| void onSetFlashModeButtonPressed(FlashMode mode) { |
| setFlashMode(mode).then((_) { |
| if (mounted) { |
| setState(() {}); |
| } |
| showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); |
| }); |
| } |
| |
| void onSetExposureModeButtonPressed(ExposureMode mode) { |
| setExposureMode(mode).then((_) { |
| if (mounted) { |
| setState(() {}); |
| } |
| showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}'); |
| }); |
| } |
| |
| void onSetFocusModeButtonPressed(FocusMode mode) { |
| setFocusMode(mode).then((_) { |
| if (mounted) { |
| setState(() {}); |
| } |
| showInSnackBar('Focus mode set to ${mode.toString().split('.').last}'); |
| }); |
| } |
| |
| void onVideoRecordButtonPressed() { |
| startVideoRecording().then((_) { |
| if (mounted) { |
| setState(() {}); |
| } |
| }); |
| } |
| |
| void onStopButtonPressed() { |
| stopVideoRecording().then((XFile? file) { |
| if (mounted) { |
| setState(() {}); |
| } |
| if (file != null) { |
| showInSnackBar('Video recorded to ${file.path}'); |
| videoFile = file; |
| _startVideoPlayer(); |
| } |
| }); |
| } |
| |
| Future<void> onPausePreviewButtonPressed() async { |
| final CameraController? cameraController = controller; |
| |
| if (cameraController == null || !cameraController.value.isInitialized) { |
| showInSnackBar('Error: select a camera first.'); |
| return; |
| } |
| |
| if (cameraController.value.isPreviewPaused) { |
| await cameraController.resumePreview(); |
| } else { |
| await cameraController.pausePreview(); |
| } |
| |
| if (mounted) { |
| setState(() {}); |
| } |
| } |
| |
| void onPauseButtonPressed() { |
| pauseVideoRecording().then((_) { |
| if (mounted) { |
| setState(() {}); |
| } |
| showInSnackBar('Video recording paused'); |
| }); |
| } |
| |
| void onResumeButtonPressed() { |
| resumeVideoRecording().then((_) { |
| if (mounted) { |
| setState(() {}); |
| } |
| showInSnackBar('Video recording resumed'); |
| }); |
| } |
| |
| Future<void> startVideoRecording() async { |
| final CameraController? cameraController = controller; |
| |
| if (cameraController == null || !cameraController.value.isInitialized) { |
| showInSnackBar('Error: select a camera first.'); |
| return; |
| } |
| |
| if (cameraController.value.isRecordingVideo) { |
| // A recording is already started, do nothing. |
| return; |
| } |
| |
| try { |
| await cameraController.startVideoRecording(); |
| } on CameraException catch (e) { |
| _showCameraException(e); |
| return; |
| } |
| } |
| |
| Future<XFile?> stopVideoRecording() async { |
| final CameraController? cameraController = controller; |
| |
| if (cameraController == null || !cameraController.value.isRecordingVideo) { |
| return null; |
| } |
| |
| try { |
| return cameraController.stopVideoRecording(); |
| } on CameraException catch (e) { |
| _showCameraException(e); |
| return null; |
| } |
| } |
| |
| Future<void> pauseVideoRecording() async { |
| final CameraController? cameraController = controller; |
| |
| if (cameraController == null || !cameraController.value.isRecordingVideo) { |
| return; |
| } |
| |
| try { |
| await cameraController.pauseVideoRecording(); |
| } on CameraException catch (e) { |
| _showCameraException(e); |
| rethrow; |
| } |
| } |
| |
| Future<void> resumeVideoRecording() async { |
| final CameraController? cameraController = controller; |
| |
| if (cameraController == null || !cameraController.value.isRecordingVideo) { |
| return; |
| } |
| |
| try { |
| await cameraController.resumeVideoRecording(); |
| } on CameraException catch (e) { |
| _showCameraException(e); |
| rethrow; |
| } |
| } |
| |
| Future<void> setFlashMode(FlashMode mode) async { |
| if (controller == null) { |
| return; |
| } |
| |
| try { |
| await controller!.setFlashMode(mode); |
| } on CameraException catch (e) { |
| _showCameraException(e); |
| rethrow; |
| } |
| } |
| |
| Future<void> setExposureMode(ExposureMode mode) async { |
| if (controller == null) { |
| return; |
| } |
| |
| try { |
| await controller!.setExposureMode(mode); |
| } on CameraException catch (e) { |
| _showCameraException(e); |
| rethrow; |
| } |
| } |
| |
| Future<void> setExposureOffset(double offset) async { |
| if (controller == null) { |
| return; |
| } |
| |
| setState(() { |
| _currentExposureOffset = offset; |
| }); |
| try { |
| offset = await controller!.setExposureOffset(offset); |
| } on CameraException catch (e) { |
| _showCameraException(e); |
| rethrow; |
| } |
| } |
| |
| Future<void> setFocusMode(FocusMode mode) async { |
| if (controller == null) { |
| return; |
| } |
| |
| try { |
| await controller!.setFocusMode(mode); |
| } on CameraException catch (e) { |
| _showCameraException(e); |
| rethrow; |
| } |
| } |
| |
| Future<void> _startVideoPlayer() async { |
| if (videoFile == null) { |
| return; |
| } |
| |
| final VideoPlayerController vController = kIsWeb |
| ? VideoPlayerController.network(videoFile!.path) |
| : VideoPlayerController.file(File(videoFile!.path)); |
| |
| videoPlayerListener = () { |
| if (videoController != null && videoController!.value.size != null) { |
| // Refreshing the state to update video player with the correct ratio. |
| if (mounted) { |
| setState(() {}); |
| } |
| videoController!.removeListener(videoPlayerListener!); |
| } |
| }; |
| vController.addListener(videoPlayerListener!); |
| await vController.setLooping(true); |
| await vController.initialize(); |
| await videoController?.dispose(); |
| if (mounted) { |
| setState(() { |
| imageFile = null; |
| videoController = vController; |
| }); |
| } |
| await vController.play(); |
| } |
| |
| Future<XFile?> takePicture() async { |
| final CameraController? cameraController = controller; |
| if (cameraController == null || !cameraController.value.isInitialized) { |
| showInSnackBar('Error: select a camera first.'); |
| return null; |
| } |
| |
| if (cameraController.value.isTakingPicture) { |
| // A capture is already pending, do nothing. |
| return null; |
| } |
| |
| try { |
| final XFile file = await cameraController.takePicture(); |
| return file; |
| } on CameraException catch (e) { |
| _showCameraException(e); |
| return null; |
| } |
| } |
| |
| void _showCameraException(CameraException e) { |
| _logError(e.code, e.description); |
| showInSnackBar('Error: ${e.code}\n${e.description}'); |
| } |
| } |
| |
| /// CameraApp is the Main Application. |
| class CameraApp extends StatelessWidget { |
| /// Default Constructor |
| const CameraApp({super.key}); |
| |
| @override |
| Widget build(BuildContext context) { |
| return const MaterialApp( |
| home: CameraExampleHome(), |
| ); |
| } |
| } |
| |
| List<CameraDescription> _cameras = <CameraDescription>[]; |
| |
| Future<void> main() async { |
| // Fetch the available cameras before initializing the app. |
| try { |
| WidgetsFlutterBinding.ensureInitialized(); |
| _cameras = await CameraPlatform.instance.availableCameras(); |
| } on CameraException catch (e) { |
| _logError(e.code, e.description); |
| } |
| runApp(const CameraApp()); |
| } |
| |
| /// This allows a value of type T or T? to be treated as a value of type T?. |
| /// |
| /// We use this so that APIs that have become non-nullable can still be used |
| /// with `!` and `?` on the stable branch. |
| T? _ambiguate<T>(T? value) => value; |