mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-03-03 15:26:47 +00:00
improving camera
This commit is contained in:
parent
d83e9a26c4
commit
cd5deca6b6
7 changed files with 114 additions and 84 deletions
|
|
@ -1,8 +1,10 @@
|
||||||
# Changelog
|
# 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
|
## 0.0.86
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ class MainCameraPreview extends StatelessWidget {
|
||||||
height: mainCameraController
|
height: mainCameraController
|
||||||
.cameraController!.value.previewSize!.width,
|
.cameraController!.value.previewSize!.width,
|
||||||
child: CameraPreview(
|
child: CameraPreview(
|
||||||
|
key: mainCameraController.cameraPreviewKey,
|
||||||
mainCameraController.cameraController!,
|
mainCameraController.cameraController!,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -47,6 +48,24 @@ class MainCameraPreview extends StatelessWidget {
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: mainCameraController.facePaint!,
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -37,55 +37,6 @@ import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
int maxVideoRecordingTime = 60;
|
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 {
|
class SelectedCameraDetails {
|
||||||
double maxAvailableZoom = 1;
|
double maxAvailableZoom = 1;
|
||||||
double minAvailableZoom = 1;
|
double minAvailableZoom = 1;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
@ -6,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.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:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
|
@ -50,6 +52,7 @@ class MainCameraController {
|
||||||
Map<int, ScannedNewProfile> scannedNewProfiles = {};
|
Map<int, ScannedNewProfile> scannedNewProfiles = {};
|
||||||
String? scannedUrl;
|
String? scannedUrl;
|
||||||
GlobalKey zoomButtonKey = GlobalKey();
|
GlobalKey zoomButtonKey = GlobalKey();
|
||||||
|
GlobalKey cameraPreviewKey = GlobalKey();
|
||||||
bool isSelectingFaceFilters = false;
|
bool isSelectingFaceFilters = false;
|
||||||
|
|
||||||
final BarcodeScanner _barcodeScanner = BarcodeScanner();
|
final BarcodeScanner _barcodeScanner = BarcodeScanner();
|
||||||
|
|
@ -63,6 +66,7 @@ class MainCameraController {
|
||||||
bool _isBusyFaces = false;
|
bool _isBusyFaces = false;
|
||||||
CustomPaint? customPaint;
|
CustomPaint? customPaint;
|
||||||
CustomPaint? facePaint;
|
CustomPaint? facePaint;
|
||||||
|
Offset? focusPointOffset;
|
||||||
|
|
||||||
FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip;
|
FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip;
|
||||||
FaceFilterType get currentFilterType => _currentFilterType;
|
FaceFilterType get currentFilterType => _currentFilterType;
|
||||||
|
|
@ -83,44 +87,96 @@ class MainCameraController {
|
||||||
selectedCameraDetails = SelectedCameraDetails();
|
selectedCameraDetails = SelectedCameraDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<CameraController?> selectCamera(int sCameraId, bool init) async {
|
Future<void> selectCamera(int sCameraId, bool init) async {
|
||||||
initCameraStarted = true;
|
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<void> toggleSelectedCamera() async {
|
var cameraId = sCameraId;
|
||||||
if (cameraController == null) return;
|
if (cameraId >= gCameras.length) {
|
||||||
// do not allow switching camera when recording
|
Log.warn(
|
||||||
if (cameraController!.value.isRecordingVideo) {
|
'Trying to select a non existing camera $cameraId >= ${gCameras.length}',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await cameraController!.stopImageStream();
|
if (init) {
|
||||||
} catch (e) {
|
for (; cameraId < gCameras.length; cameraId++) {
|
||||||
// Log.warn(e);
|
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;
|
facePaint = null;
|
||||||
customPaint = null;
|
customPaint = null;
|
||||||
await tmp!.dispose();
|
isSelectingFaceFilters = false;
|
||||||
|
setFilter(FaceFilterType.none);
|
||||||
|
zoomButtonKey = GlobalKey();
|
||||||
|
setState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onDoubleTap() async {
|
||||||
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
|
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> 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) {
|
void setFilter(FaceFilterType type) {
|
||||||
_currentFilterType = type;
|
_currentFilterType = type;
|
||||||
if (_currentFilterType == FaceFilterType.none) {
|
if (_currentFilterType == FaceFilterType.none) {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ class QrCodeScannerState extends State<QrCodeScanner> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
onDoubleTap: _mainCameraController.toggleSelectedCamera,
|
onDoubleTap: _mainCameraController.onDoubleTap,
|
||||||
|
onTapDown: _mainCameraController.onTapDown,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
MainCameraPreview(
|
MainCameraPreview(
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ class CameraSendToViewState extends State<CameraSendToView> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
onDoubleTap: _mainCameraController.toggleSelectedCamera,
|
onDoubleTap: _mainCameraController.onDoubleTap,
|
||||||
|
onTapDown: _mainCameraController.onTapDown,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
MainCameraPreview(
|
MainCameraPreview(
|
||||||
|
|
|
||||||
|
|
@ -172,9 +172,9 @@ class HomeViewState extends State<HomeView> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
onDoubleTap: offsetRatio == 0
|
onDoubleTap:
|
||||||
? _mainCameraController.toggleSelectedCamera
|
offsetRatio == 0 ? _mainCameraController.onDoubleTap : null,
|
||||||
: null,
|
onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
MainCameraPreview(mainCameraController: _mainCameraController),
|
MainCameraPreview(mainCameraController: _mainCameraController),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue