diff --git a/CHANGELOG.md b/CHANGELOG.md index ffabff6..503743c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Changelog -## 0.0.87 +## 0.0.89 -- Added basic support for face filters +- Adds option to manual focus in the camera +- Adds support to switch between front and back camera during video recording +- Adds basic face filters ## 0.0.86 diff --git a/lib/src/views/camera/camera_preview_components/camera_preview.dart b/lib/src/views/camera/camera_preview_components/camera_preview.dart index 01c4948..cde7474 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview.dart @@ -36,6 +36,7 @@ class MainCameraPreview extends StatelessWidget { height: mainCameraController .cameraController!.value.previewSize!.width, child: CameraPreview( + key: mainCameraController.cameraPreviewKey, mainCameraController.cameraController!, child: Stack( children: [ @@ -47,6 +48,24 @@ class MainCameraPreview extends StatelessWidget { Positioned.fill( child: mainCameraController.facePaint!, ), + if (mainCameraController.focusPointOffset != null) + Positioned( + top: mainCameraController.focusPointOffset!.dy - 40, + left: + mainCameraController.focusPointOffset!.dx - 40, + child: Container( + height: 80, + width: 80, + clipBehavior: Clip.antiAliasWithSaveLayer, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 1, + color: Colors.white.withAlpha(150), + ), + ), + ), + ) ], ), ), diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index ce13173..fe387ee 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -37,55 +37,6 @@ import 'package:url_launcher/url_launcher_string.dart'; int maxVideoRecordingTime = 60; -Future<(SelectedCameraDetails, CameraController)?> initializeCameraController( - SelectedCameraDetails details, - int sCameraId, - bool init, -) async { - var cameraId = sCameraId; - if (cameraId >= gCameras.length) return null; - if (init) { - for (; cameraId < gCameras.length; cameraId++) { - if (gCameras[cameraId].lensDirection == CameraLensDirection.back) { - break; - } - } - } - details.isZoomAble = false; - if (details.cameraId != cameraId) { - // switch between front and back - details.scaleFactor = 1; - } - - final cameraController = CameraController( - gCameras[cameraId], - ResolutionPreset.high, - enableAudio: await Permission.microphone.isGranted, - imageFormatGroup: - Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888, - ); - - await cameraController.initialize().then((_) async { - await cameraController.setZoomLevel(details.scaleFactor); - await cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp); - await cameraController - .setFlashMode(details.isFlashOn ? FlashMode.always : FlashMode.off); - await cameraController - .getMaxZoomLevel() - .then((double value) => details.maxAvailableZoom = value); - await cameraController - .getMinZoomLevel() - .then((double value) => details.minAvailableZoom = value); - details - ..isZoomAble = details.maxAvailableZoom != details.minAvailableZoom - ..cameraLoaded = true - ..cameraId = cameraId; - }).catchError((Object e) { - Log.error('$e'); - }); - return (details, cameraController); -} - class SelectedCameraDetails { double maxAvailableZoom = 1; double minAvailableZoom = 1; diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index 5fb9f8c..f710797 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; import 'package:collection/collection.dart'; @@ -6,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -50,6 +52,7 @@ class MainCameraController { Map scannedNewProfiles = {}; String? scannedUrl; GlobalKey zoomButtonKey = GlobalKey(); + GlobalKey cameraPreviewKey = GlobalKey(); bool isSelectingFaceFilters = false; final BarcodeScanner _barcodeScanner = BarcodeScanner(); @@ -63,6 +66,7 @@ class MainCameraController { bool _isBusyFaces = false; CustomPaint? customPaint; CustomPaint? facePaint; + Offset? focusPointOffset; FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip; FaceFilterType get currentFilterType => _currentFilterType; @@ -83,44 +87,96 @@ class MainCameraController { selectedCameraDetails = SelectedCameraDetails(); } - Future selectCamera(int sCameraId, bool init) async { + Future selectCamera(int sCameraId, bool init) async { initCameraStarted = true; - final opts = await initializeCameraController( - selectedCameraDetails, - sCameraId, - init, - ); - if (opts != null) { - selectedCameraDetails = opts.$1; - cameraController = opts.$2; - } - isSelectingFaceFilters = false; - setFilter(FaceFilterType.none); - await cameraController?.startImageStream(_processCameraImage); - zoomButtonKey = GlobalKey(); - setState(); - return cameraController; - } - Future toggleSelectedCamera() async { - if (cameraController == null) return; - // do not allow switching camera when recording - if (cameraController!.value.isRecordingVideo) { + var cameraId = sCameraId; + if (cameraId >= gCameras.length) { + Log.warn( + 'Trying to select a non existing camera $cameraId >= ${gCameras.length}', + ); return; } - try { - await cameraController!.stopImageStream(); - } catch (e) { - // Log.warn(e); + + if (init) { + for (; cameraId < gCameras.length; cameraId++) { + if (gCameras[cameraId].lensDirection == CameraLensDirection.back) { + break; + } + } } - final tmp = cameraController; - cameraController = null; + + selectedCameraDetails.isZoomAble = false; + if (selectedCameraDetails.cameraId != cameraId) { + // switched camera so reset the scaleFactor + selectedCameraDetails.scaleFactor = 1; + } + + if (cameraController == null) { + cameraController = CameraController( + gCameras[cameraId], + ResolutionPreset.high, + enableAudio: await Permission.microphone.isGranted, + imageFormatGroup: Platform.isAndroid + ? ImageFormatGroup.nv21 + : ImageFormatGroup.bgra8888, + ); + await cameraController?.initialize(); + await cameraController?.startImageStream(_processCameraImage); + } else { + await HapticFeedback.lightImpact(); + await cameraController?.setDescription(gCameras[cameraId]); + } + + await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); + await cameraController + ?.lockCaptureOrientation(DeviceOrientation.portraitUp); + await cameraController?.setFlashMode( + selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off, + ); + selectedCameraDetails.maxAvailableZoom = + await cameraController?.getMaxZoomLevel() ?? 1; + selectedCameraDetails.minAvailableZoom = + await cameraController?.getMinZoomLevel() ?? 1; + selectedCameraDetails + ..isZoomAble = selectedCameraDetails.maxAvailableZoom != + selectedCameraDetails.minAvailableZoom + ..cameraLoaded = true + ..cameraId = cameraId; + facePaint = null; customPaint = null; - await tmp!.dispose(); + isSelectingFaceFilters = false; + setFilter(FaceFilterType.none); + zoomButtonKey = GlobalKey(); + setState(); + } + + Future onDoubleTap() async { await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); } + Future onTapDown(TapDownDetails details) async { + final box = + cameraPreviewKey.currentContext?.findRenderObject() as RenderBox?; + if (box == null) return; + final localPosition = box.globalToLocal(details.globalPosition); + + focusPointOffset = Offset(localPosition.dx, localPosition.dy); + + final dx = localPosition.dx / box.size.width; + final dy = localPosition.dy / box.size.height; + + setState(); + + await HapticFeedback.lightImpact(); + await cameraController?.setFocusPoint(Offset(dx, dy)); + await cameraController?.setFocusMode(FocusMode.auto); + + focusPointOffset = null; + setState(); + } + void setFilter(FaceFilterType type) { _currentFilterType = type; if (_currentFilterType == FaceFilterType.none) { diff --git a/lib/src/views/camera/camera_qr_scanner.view.dart b/lib/src/views/camera/camera_qr_scanner.view.dart index c9fa6ea..32703cc 100644 --- a/lib/src/views/camera/camera_qr_scanner.view.dart +++ b/lib/src/views/camera/camera_qr_scanner.view.dart @@ -32,7 +32,8 @@ class QrCodeScannerState extends State { Widget build(BuildContext context) { return Scaffold( body: GestureDetector( - onDoubleTap: _mainCameraController.toggleSelectedCamera, + onDoubleTap: _mainCameraController.onDoubleTap, + onTapDown: _mainCameraController.onTapDown, child: Stack( children: [ MainCameraPreview( diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to_view.dart index 7dfefbf..cdda133 100644 --- a/lib/src/views/camera/camera_send_to_view.dart +++ b/lib/src/views/camera/camera_send_to_view.dart @@ -34,7 +34,8 @@ class CameraSendToViewState extends State { Widget build(BuildContext context) { return Scaffold( body: GestureDetector( - onDoubleTap: _mainCameraController.toggleSelectedCamera, + onDoubleTap: _mainCameraController.onDoubleTap, + onTapDown: _mainCameraController.onTapDown, child: Stack( children: [ MainCameraPreview( diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index b87ab11..b19210c 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -172,9 +172,9 @@ class HomeViewState extends State { Widget build(BuildContext context) { return Scaffold( body: GestureDetector( - onDoubleTap: offsetRatio == 0 - ? _mainCameraController.toggleSelectedCamera - : null, + onDoubleTap: + offsetRatio == 0 ? _mainCameraController.onDoubleTap : null, + onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null, child: Stack( children: [ MainCameraPreview(mainCameraController: _mainCameraController),