implemented MockCameraContainerBloc to stub camera on simulator

This commit is contained in:
Vadim 2023-12-21 14:43:54 +01:00
parent c80bac23b2
commit 174f4612f2
7 changed files with 150 additions and 35 deletions

15
.vscode/launch.json vendored
View file

@ -82,5 +82,20 @@
], ],
"program": "${workspaceFolder}/lib/main_dev.dart", "program": "${workspaceFolder}/lib/main_dev.dart",
}, },
{
"name": "dev-simulator",
"request": "launch",
"type": "dart",
"flutterMode": "debug",
"args": [
"--flavor",
"dev",
"--dart-define",
"cameraPreviewAspectRatio=240/320",
"--dart-define",
"cameraStubImage=assets/camera_stub_image.jpg"
],
"program": "${workspaceFolder}/lib/main_dev.dart",
},
], ],
} }

View file

@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/metering/flow_metering.dart'; import 'package:lightmeter/screens/metering/flow_metering.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart'; import 'package:lightmeter/screens/settings/flow_settings.dart';
@ -17,13 +18,13 @@ class Application extends StatelessWidget {
return AnnotatedRegion( return AnnotatedRegion(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
statusBarBrightness: statusBarBrightness: systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light,
systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light,
statusBarIconBrightness: systemIconsBrightness, statusBarIconBrightness: systemIconsBrightness,
systemNavigationBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness: systemIconsBrightness, systemNavigationBarIconBrightness: systemIconsBrightness,
), ),
child: MaterialApp( child: MaterialApp(
debugShowCheckedModeBanner: !PlatformConfig.isTest,
theme: theme, theme: theme,
locale: Locale(UserPreferencesProvider.localeOf(context).intlName), locale: Locale(UserPreferencesProvider.localeOf(context).intlName),
localizationsDelegates: const [ localizationsDelegates: const [

View file

@ -7,4 +7,6 @@ class PlatformConfig {
} }
static String get cameraStubImage => const String.fromEnvironment('cameraStubImage'); static String get cameraStubImage => const String.fromEnvironment('cameraStubImage');
static bool get isTest => cameraStubImage.isNotEmpty;
} }

View file

@ -4,7 +4,6 @@ import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:exif/exif.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -17,7 +16,9 @@ import 'package:lightmeter/screens/metering/components/camera_container/event_co
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:lightmeter/utils/ev_from_bytes.dart';
part 'mock_bloc_container_camera.dart';
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> { class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
final MeteringInteractor _meteringInteractor; final MeteringInteractor _meteringInteractor;
@ -213,33 +214,15 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
Future<double?> _takePhoto() async { Future<double?> _takePhoto() async {
try { try {
// https://github.com/flutter/flutter/issues/84957#issuecomment-1661155095 // https://github.com/flutter/flutter/issues/84957#issuecomment-1661155095
late final Uint8List bytes;
if (PlatformConfig.cameraStubImage.isNotEmpty) {
bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
} else {
await _cameraController!.setFocusMode(FocusMode.locked); await _cameraController!.setFocusMode(FocusMode.locked);
await _cameraController!.setExposureMode(ExposureMode.locked); await _cameraController!.setExposureMode(ExposureMode.locked);
final file = await _cameraController!.takePicture(); final file = await _cameraController!.takePicture();
await _cameraController!.setFocusMode(FocusMode.auto); await _cameraController!.setFocusMode(FocusMode.auto);
await _cameraController!.setExposureMode(ExposureMode.auto); await _cameraController!.setExposureMode(ExposureMode.auto);
bytes = await file.readAsBytes(); final bytes = await file.readAsBytes();
Directory(file.path).deleteSync(recursive: true); Directory(file.path).deleteSync(recursive: true);
}
final tags = await readExifFromBytes(bytes); return await evFromImage(bytes);
final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}");
final apertureValueRatio = (tags["EXIF FNumber"]?.values as IfdRatios?)?.ratios.first;
final speedValueRatio = (tags["EXIF ExposureTime"]?.values as IfdRatios?)?.ratios.first;
if (iso == null || apertureValueRatio == null || speedValueRatio == null) {
log('Error parsing EXIF: ${tags.keys}');
return null;
}
final aperture = apertureValueRatio.numerator / apertureValueRatio.denominator;
final speed = speedValueRatio.numerator / speedValueRatio.denominator;
return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100);
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
return null; return null;

View file

@ -0,0 +1,80 @@
part of 'bloc_container_camera.dart';
class MockCameraContainerBloc extends CameraContainerBloc {
MockCameraContainerBloc(
super._meteringInteractor,
super.communicationBloc,
);
@override
Future<void> _onRequestPermission(_, Emitter emit) async {
add(const InitializeEvent());
}
@override
Future<void> _onOpenAppSettings(_, Emitter emit) async {
_meteringInteractor.openAppSettings();
}
@override
Future<void> _onInitialize(_, Emitter emit) async {
emit(const CameraLoadingState());
try {
_cameraController = CameraController(
const CameraDescription(name: '0', lensDirection: CameraLensDirection.back, sensorOrientation: 0),
ResolutionPreset.low,
enableAudio: false,
);
_zoomRange = const RangeValues(1, 6);
_currentZoom = _zoomRange!.start;
_exposureOffsetRange = const RangeValues(-4, 4);
_exposureStep = 0.1;
_currentExposureOffset = 0.0;
emit(CameraInitializedState(_cameraController!));
_emitActiveState(emit);
} catch (e) {
emit(const CameraErrorState(CameraErrorType.other));
}
}
@override
Future<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
if (event.value >= _zoomRange!.start && event.value <= _zoomRange!.end) {
_currentZoom = event.value;
_emitActiveState(emit);
}
}
@override
Future<void> _onExposureOffsetChanged(ExposureOffsetChangedEvent event, Emitter emit) async {
_currentExposureOffset = event.value;
_emitActiveState(emit);
}
@override
Future<void> _onExposureOffsetResetEvent(ExposureOffsetResetEvent event, Emitter emit) async {
_meteringInteractor.quickVibration();
add(const ExposureOffsetChangedEvent(0));
}
@override
Future<void> _onExposureSpotChangedEvent(ExposureSpotChangedEvent event, Emitter emit) async {}
@override
bool get _canTakePhoto => PlatformConfig.cameraStubImage.isNotEmpty;
@override
Future<double?> _takePhoto() async {
try {
final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
return await evFromImage(bytes);
} catch (e) {
log(e.toString());
return null;
}
}
}

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
@ -30,12 +31,18 @@ class CameraContainerProvider extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider<CameraContainerBloc>(
lazy: false, lazy: false,
create: (context) => CameraContainerBloc( create: (context) => (PlatformConfig.cameraStubImage.isNotEmpty
? MockCameraContainerBloc(
MeteringInteractorProvider.of(context), MeteringInteractorProvider.of(context),
context.read<MeteringCommunicationBloc>(), context.read<MeteringCommunicationBloc>(),
)..add(const RequestPermissionEvent()), )
: CameraContainerBloc(
MeteringInteractorProvider.of(context),
context.read<MeteringCommunicationBloc>(),
))
..add(const RequestPermissionEvent()),
child: CameraContainer( child: CameraContainer(
fastest: fastest, fastest: fastest,
slowest: slowest, slowest: slowest,

View file

@ -0,0 +1,27 @@
import 'dart:developer';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:exif/exif.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
Future<double?> evFromImage(Uint8List bytes) async {
try {
final tags = await readExifFromBytes(bytes);
final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}");
final apertureValueRatio = (tags["EXIF FNumber"]?.values as IfdRatios?)?.ratios.first;
final speedValueRatio = (tags["EXIF ExposureTime"]?.values as IfdRatios?)?.ratios.first;
if (iso == null || apertureValueRatio == null || speedValueRatio == null) {
log('Error parsing EXIF: ${tags.keys}');
return null;
}
final aperture = apertureValueRatio.numerator / apertureValueRatio.denominator;
final speed = speedValueRatio.numerator / speedValueRatio.denominator;
return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100);
} catch (e) {
log(e.toString());
return null;
}
}