diff --git a/android/app/build.gradle b/android/app/build.gradle
index c9ca0aa..5f0c906 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -43,7 +43,7 @@ android {
}
defaultConfig {
- minSdkVersion flutter.minSdkVersion
+ minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
index 1d526a1..21a3cc1 100644
--- a/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -4,4 +4,7 @@
+
+
diff --git a/lib/data/ev_source/camera/bloc_camera.dart b/lib/data/ev_source/camera/bloc_camera.dart
new file mode 100644
index 0000000..382e855
--- /dev/null
+++ b/lib/data/ev_source/camera/bloc_camera.dart
@@ -0,0 +1,126 @@
+import 'dart:async';
+import 'dart:io';
+import 'dart:math';
+import 'dart:typed_data';
+import 'package:camera/camera.dart';
+import 'package:exif/exif.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
+import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_event;
+import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states;
+import 'package:lightmeter/utils/log_2.dart';
+
+import 'event_camera.dart';
+import 'state_camera.dart';
+
+class CameraBloc extends Bloc {
+ final MeteringCommunicationBloc _communicationBloc;
+ late final StreamSubscription _communicationSubscription;
+
+ late final _WidgetsBindingObserver _observer;
+ CameraController? _cameraController;
+ CameraController? get cameraController => _cameraController;
+
+ CameraBloc(this._communicationBloc) : super(const CameraInitState()) {
+ _communicationSubscription = _communicationBloc.stream.listen(_onCommunicationState);
+
+ _observer = _WidgetsBindingObserver(_appLifecycleStateObserver);
+ WidgetsBinding.instance.addObserver(_observer);
+
+ on(_onInitialized);
+
+ add(const InitializeEvent());
+ }
+
+ @override
+ Future close() async {
+ WidgetsBinding.instance.removeObserver(_observer);
+ _cameraController?.dispose();
+ await _communicationSubscription.cancel();
+ super.close();
+ }
+
+ void _onCommunicationState(communication_states.MeteringCommunicationState communicationState) {
+ if (communicationState is communication_states.MeasureState) {
+ _takePhoto().then((ev100) {
+ if (ev100 != null) {
+ _communicationBloc.add(communication_event.MeasuredEvent(ev100));
+ }
+ });
+ }
+ }
+
+ Future _onInitialized(_, Emitter emit) async {
+ emit(const CameraLoadingState());
+ try {
+ final cameras = await availableCameras();
+ _cameraController = CameraController(
+ cameras.firstWhere(
+ (camera) => camera.lensDirection == CameraLensDirection.back,
+ orElse: () => cameras.last,
+ ),
+ ResolutionPreset.medium,
+ enableAudio: false,
+ );
+
+ await _cameraController!.initialize();
+ await _cameraController!.setFlashMode(FlashMode.off);
+ emit(CameraReadyState(_cameraController!));
+ } catch (e) {
+ emit(const CameraErrorState());
+ }
+ }
+
+ Future _takePhoto() async {
+ if (_cameraController == null ||
+ !_cameraController!.value.isInitialized ||
+ _cameraController!.value.isTakingPicture) {
+ return null;
+ }
+
+ try {
+ final file = await _cameraController!.takePicture();
+ final Uint8List bytes = await file.readAsBytes();
+ Directory(file.path).deleteSync(recursive: true);
+
+ final tags = await readExifFromBytes(bytes);
+ final iso = double.parse("${tags["EXIF ISOSpeedRatings"]}");
+ final apertureValueRatio = (tags["EXIF FNumber"]!.values as IfdRatios).ratios.first;
+ final aperture = apertureValueRatio.numerator / apertureValueRatio.denominator;
+ final speedValueRatio = (tags["EXIF ExposureTime"]!.values as IfdRatios).ratios.first;
+ final speed = speedValueRatio.numerator / speedValueRatio.denominator;
+
+ return log2(pow(aperture, 2)) - log2(speed) - log2(iso / 100);
+ } on CameraException catch (e) {
+ debugPrint('Error: ${e.code}\nError Message: ${e.description}');
+ return null;
+ }
+ }
+
+ Future _appLifecycleStateObserver(AppLifecycleState state) async {
+ switch (state) {
+ case AppLifecycleState.resumed:
+ add(const InitializeEvent());
+ break;
+ case AppLifecycleState.paused:
+ case AppLifecycleState.inactive:
+ _cameraController?.dispose();
+ _cameraController = null;
+ break;
+ default:
+ }
+ }
+}
+
+/// This is needed only because we cannot use `with` with mixins
+class _WidgetsBindingObserver with WidgetsBindingObserver {
+ final ValueChanged onLifecycleStateChanged;
+
+ _WidgetsBindingObserver(this.onLifecycleStateChanged);
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ onLifecycleStateChanged(state);
+ }
+}
diff --git a/lib/data/ev_source/camera/event_camera.dart b/lib/data/ev_source/camera/event_camera.dart
new file mode 100644
index 0000000..099f150
--- /dev/null
+++ b/lib/data/ev_source/camera/event_camera.dart
@@ -0,0 +1,7 @@
+abstract class CameraEvent {
+ const CameraEvent();
+}
+
+class InitializeEvent extends CameraEvent {
+ const InitializeEvent();
+}
\ No newline at end of file
diff --git a/lib/data/ev_source/camera/state_camera.dart b/lib/data/ev_source/camera/state_camera.dart
new file mode 100644
index 0000000..1364310
--- /dev/null
+++ b/lib/data/ev_source/camera/state_camera.dart
@@ -0,0 +1,23 @@
+import 'package:camera/camera.dart';
+
+abstract class CameraState {
+ const CameraState();
+}
+
+class CameraInitState extends CameraState {
+ const CameraInitState();
+}
+
+class CameraLoadingState extends CameraState {
+ const CameraLoadingState();
+}
+
+class CameraReadyState extends CameraState {
+ final CameraController controller;
+
+ const CameraReadyState(this.controller);
+}
+
+class CameraErrorState extends CameraState {
+ const CameraErrorState();
+}
diff --git a/lib/main.dart b/lib/main.dart
index a3b598f..5b740d5 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,16 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/screens/settings/settings_screen.dart';
import 'package:provider/provider.dart';
import 'generated/l10n.dart';
-import 'models/photography_value.dart';
import 'res/theme.dart';
-import 'screens/metering/metering_bloc.dart';
-import 'screens/metering/metering_screen.dart';
+import 'screens/metering/flow_metering.dart';
import 'utils/stop_type_provider.dart';
void main() {
@@ -45,30 +42,27 @@ class _ApplicationState extends State {
return Provider(
create: (context) => PermissionsService(),
child: StopTypeProvider(
- child: BlocProvider(
- create: (context) => MeteringBloc(context.read()),
- child: MaterialApp(
- theme: ThemeData(
- useMaterial3: true,
- colorScheme: lightColorScheme,
- ),
- localizationsDelegates: const [
- S.delegate,
- GlobalMaterialLocalizations.delegate,
- GlobalWidgetsLocalizations.delegate,
- GlobalCupertinoLocalizations.delegate,
- ],
- supportedLocales: S.delegate.supportedLocales,
- builder: (context, child) => MediaQuery(
- data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
- child: child!,
- ),
- home: const MeteringScreen(),
- routes: {
- "metering": (context) => const MeteringScreen(),
- "settings": (context) => const SettingsScreen(),
- },
+ child: MaterialApp(
+ theme: ThemeData(
+ useMaterial3: true,
+ colorScheme: lightColorScheme,
),
+ localizationsDelegates: const [
+ S.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: S.delegate.supportedLocales,
+ builder: (context, child) => MediaQuery(
+ data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
+ child: child!,
+ ),
+ home: const MeteringFlow(),
+ routes: {
+ "metering": (context) => const MeteringFlow(),
+ "settings": (context) => const SettingsScreen(),
+ },
),
),
);
diff --git a/lib/screens/metering/metering_bloc.dart b/lib/screens/metering/bloc_metering.dart
similarity index 72%
rename from lib/screens/metering/metering_bloc.dart
rename to lib/screens/metering/bloc_metering.dart
index 74a6fad..18358b3 100644
--- a/lib/screens/metering/metering_bloc.dart
+++ b/lib/screens/metering/bloc_metering.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
import 'dart:math';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -7,19 +8,25 @@ import 'package:lightmeter/models/iso_value.dart';
import 'package:lightmeter/models/nd_value.dart';
import 'package:lightmeter/models/photography_value.dart';
import 'package:lightmeter/models/shutter_speed_value.dart';
+import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events;
+import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states;
import 'package:lightmeter/utils/log_2.dart';
-import 'metering_event.dart';
-import 'metering_state.dart';
+import 'communication/bloc_communication_metering.dart';
+import 'event_metering.dart';
+import 'state_metering.dart';
class MeteringBloc extends Bloc {
+ final MeteringCommunicationBloc _communicationBloc;
+ late final StreamSubscription _communicationSubscription;
+
List get _apertureValues => apertureValues.whereStopType(stopType);
List get _shutterSpeedValues => shutterSpeedValues.whereStopType(stopType);
final _random = Random();
StopType stopType;
- MeteringBloc(this.stopType)
+ MeteringBloc(this._communicationBloc, this.stopType)
: super(
MeteringState(
iso: isoValues.where((element) => element.value == 100).first,
@@ -29,14 +36,42 @@ class MeteringBloc extends Bloc {
exposurePairs: [],
),
) {
+ _communicationSubscription = _communicationBloc.stream
+ .where((state) => state is communication_states.ScreenState)
+ .map((state) => state as communication_states.ScreenState)
+ .listen(_onCommunicationState);
+
on(_onStopTypeChanged);
on(_onIsoChanged);
on(_onNdChanged);
- on(_onMeasure);
+ on((_, __) => _communicationBloc.add(const communication_events.MeasureEvent()));
+ on(_onMeasured);
add(const MeasureEvent());
}
+ @override
+ Future close() async {
+ await _communicationSubscription.cancel();
+ return super.close();
+ }
+
+ void _onCommunicationState(communication_states.ScreenState communicationState) {
+ if (communicationState is communication_states.MeasuredState) {
+ add(MeasuredEvent(communicationState.ev100));
+ }
+ }
+
+ /// https://www.scantips.com/lights/exposurecalc.html
+ Future measureEv() async {
+ final aperture = _apertureValues[_random.nextInt(_apertureValues.length)];
+ final shutterSpeed = _shutterSpeedValues[_random.nextInt(_shutterSpeedValues.thirdStops().length)];
+ final iso = isoValues[_random.nextInt(isoValues.thirdStops().length)];
+
+ final evAtSystemIso = log2(pow(aperture.value, 2).toDouble() / shutterSpeed.value);
+ return evAtSystemIso - log2(iso.value / state.iso.value);
+ }
+
void _onStopTypeChanged(StopTypeChangedEvent event, Emitter emit) {
stopType = event.stopType;
emit(MeteringState(
@@ -70,15 +105,8 @@ class MeteringBloc extends Bloc {
));
}
- /// https://www.scantips.com/lights/exposurecalc.html
- void _onMeasure(_, Emitter emit) {
- final aperture = _apertureValues[_random.nextInt(_apertureValues.length)];
- final shutterSpeed = _shutterSpeedValues[_random.nextInt(_shutterSpeedValues.thirdStops().length)];
- final iso = isoValues[_random.nextInt(isoValues.thirdStops().length)];
-
- final evAtSystemIso = log2(pow(aperture.value, 2).toDouble() / shutterSpeed.value);
- final ev = evAtSystemIso - log2(iso.value / state.iso.value);
-
+ void _onMeasured(MeasuredEvent event, Emitter emit) {
+ final ev = event.ev100 + log2(state.iso.value / 100);
emit(MeteringState(
iso: state.iso,
ev: ev,
diff --git a/lib/screens/metering/communication/bloc_communication_metering.dart b/lib/screens/metering/communication/bloc_communication_metering.dart
new file mode 100644
index 0000000..b6fa04c
--- /dev/null
+++ b/lib/screens/metering/communication/bloc_communication_metering.dart
@@ -0,0 +1,11 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'event_communication_metering.dart';
+import 'state_communication_metering.dart';
+
+class MeteringCommunicationBloc extends Bloc {
+ MeteringCommunicationBloc() : super(const InitState()) {
+ on((_, emit) => emit(const MeasureState()));
+ on((event, emit) => emit(MeasuredState(event.ev100)));
+ }
+}
diff --git a/lib/screens/metering/communication/event_communication_metering.dart b/lib/screens/metering/communication/event_communication_metering.dart
new file mode 100644
index 0000000..b8666e9
--- /dev/null
+++ b/lib/screens/metering/communication/event_communication_metering.dart
@@ -0,0 +1,21 @@
+abstract class MeteringCommunicationEvent {
+ const MeteringCommunicationEvent();
+}
+
+abstract class SourceEvent extends MeteringCommunicationEvent {
+ const SourceEvent();
+}
+
+abstract class ScreenEvent extends MeteringCommunicationEvent {
+ const ScreenEvent();
+}
+
+class MeasureEvent extends ScreenEvent {
+ const MeasureEvent();
+}
+
+class MeasuredEvent extends SourceEvent {
+ final double ev100;
+
+ const MeasuredEvent(this.ev100);
+}
diff --git a/lib/screens/metering/communication/state_communication_metering.dart b/lib/screens/metering/communication/state_communication_metering.dart
new file mode 100644
index 0000000..a41bef8
--- /dev/null
+++ b/lib/screens/metering/communication/state_communication_metering.dart
@@ -0,0 +1,25 @@
+abstract class MeteringCommunicationState {
+ const MeteringCommunicationState();
+}
+
+class InitState extends MeteringCommunicationState {
+ const InitState();
+}
+
+abstract class SourceState extends MeteringCommunicationState {
+ const SourceState();
+}
+
+abstract class ScreenState extends MeteringCommunicationState {
+ const ScreenState();
+}
+
+class MeasureState extends SourceState {
+ const MeasureState();
+}
+
+class MeasuredState extends ScreenState {
+ final double ev100;
+
+ const MeasuredState(this.ev100);
+}
\ No newline at end of file
diff --git a/lib/screens/metering/components/bottom_controls/components/zoom_slider.dart b/lib/screens/metering/components/bottom_controls/components/zoom_slider.dart
new file mode 100644
index 0000000..9de55c7
--- /dev/null
+++ b/lib/screens/metering/components/bottom_controls/components/zoom_slider.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+import 'package:lightmeter/res/dimens.dart';
+
+class ZoomSlider extends StatelessWidget {
+ const ZoomSlider({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
+ child: Row(
+ children: [
+ const Icon(Icons.zoom_out),
+ Expanded(
+ child: Slider(
+ value: 0,
+ onChanged: (value) {},
+ ),
+ ),
+ const Icon(Icons.zoom_in),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/screens/metering/components/topbar/components/camera_preview.dart b/lib/screens/metering/components/topbar/components/camera_preview.dart
index e69de29..c2a88e0 100644
--- a/lib/screens/metering/components/topbar/components/camera_preview.dart
+++ b/lib/screens/metering/components/topbar/components/camera_preview.dart
@@ -0,0 +1,57 @@
+import 'package:camera/camera.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:lightmeter/data/ev_source/camera/bloc_camera.dart';
+import 'package:lightmeter/data/ev_source/camera/state_camera.dart';
+
+class CameraView extends StatelessWidget {
+ const CameraView({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return AspectRatio(
+ aspectRatio: 3 / 4,
+ child: BlocBuilder(
+ builder: (context, state) {
+ if (state is CameraReadyState) {
+ final value = state.controller.value;
+ return ValueListenableBuilder(
+ valueListenable: state.controller,
+ builder: (_, __, ___) => AspectRatio(
+ aspectRatio:
+ _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio),
+ child: RotatedBox(
+ quarterTurns: _getQuarterTurns(value),
+ child: state.controller.buildPreview(),
+ ),
+ ),
+ );
+ }
+ return const ColoredBox(color: Colors.black);
+ },
+ ),
+ );
+ }
+
+ bool _isLandscape(CameraValue value) {
+ return [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]
+ .contains(_getApplicableOrientation(value));
+ }
+
+ int _getQuarterTurns(CameraValue value) {
+ final Map turns = {
+ DeviceOrientation.portraitUp: 0,
+ DeviceOrientation.landscapeRight: 1,
+ DeviceOrientation.portraitDown: 2,
+ DeviceOrientation.landscapeLeft: 3,
+ };
+ return turns[_getApplicableOrientation(value)]!;
+ }
+
+ DeviceOrientation _getApplicableOrientation(CameraValue value) {
+ return value.isRecordingVideo
+ ? value.recordingOrientation!
+ : (value.previewPauseOrientation ?? value.lockedCaptureOrientation ?? value.deviceOrientation);
+ }
+}
diff --git a/lib/screens/metering/components/topbar/topbar.dart b/lib/screens/metering/components/topbar/topbar.dart
index afffc67..903414d 100644
--- a/lib/screens/metering/components/topbar/topbar.dart
+++ b/lib/screens/metering/components/topbar/topbar.dart
@@ -6,6 +6,7 @@ import 'package:lightmeter/models/nd_value.dart';
import 'package:lightmeter/models/photography_value.dart';
import 'package:lightmeter/res/dimens.dart';
+import 'components/camera_preview.dart';
import 'components/shared/animated_dialog.dart';
import 'components/dialog_picker.dart';
import 'components/reading_container.dart';
@@ -74,6 +75,13 @@ class MeteringTopBar extends StatelessWidget {
),
),
const _InnerPadding(),
+ ReadingContainer.singleValue(
+ value: ReadingValue(
+ label: 'EV',
+ value: ev.toString(),
+ ),
+ ),
+ const _InnerPadding(),
Row(
children: [
Expanded(
@@ -111,18 +119,8 @@ class MeteringTopBar extends StatelessWidget {
const _InnerPadding(),
Expanded(
child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- AnimatedDialog(
- openedSize: Size(
- MediaQuery.of(context).size.width - Dimens.paddingM * 2,
- (MediaQuery.of(context).size.width - Dimens.paddingM * 2) / 3 * 4,
- ),
- child: const AspectRatio(
- aspectRatio: 3 / 4,
- child: ColoredBox(color: Colors.black),
- ),
- ),
+ const CameraView(),
],
),
),
diff --git a/lib/screens/metering/metering_event.dart b/lib/screens/metering/event_metering.dart
similarity index 86%
rename from lib/screens/metering/metering_event.dart
rename to lib/screens/metering/event_metering.dart
index 40e4ae7..275f03d 100644
--- a/lib/screens/metering/metering_event.dart
+++ b/lib/screens/metering/event_metering.dart
@@ -27,3 +27,9 @@ class NdChangedEvent extends MeteringEvent {
class MeasureEvent extends MeteringEvent {
const MeasureEvent();
}
+
+class MeasuredEvent extends MeteringEvent {
+ final double ev100;
+
+ const MeasuredEvent(this.ev100);
+}
diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart
new file mode 100644
index 0000000..419e3a9
--- /dev/null
+++ b/lib/screens/metering/flow_metering.dart
@@ -0,0 +1,29 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:lightmeter/data/ev_source/camera/bloc_camera.dart';
+import 'package:lightmeter/models/photography_value.dart';
+import 'package:lightmeter/screens/metering/bloc_metering.dart';
+import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
+
+import 'screen_metering.dart';
+
+class MeteringFlow extends StatelessWidget {
+ const MeteringFlow({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MultiBlocProvider(
+ providers: [
+ BlocProvider(create: (_) => MeteringCommunicationBloc()),
+ BlocProvider(
+ create: (context) => MeteringBloc(
+ context.read(),
+ context.read(),
+ ),
+ ),
+ BlocProvider(create: (context) => CameraBloc(context.read())),
+ ],
+ child: const MeteringScreen(),
+ );
+ }
+}
diff --git a/lib/screens/metering/metering_screen.dart b/lib/screens/metering/screen_metering.dart
similarity index 96%
rename from lib/screens/metering/metering_screen.dart
rename to lib/screens/metering/screen_metering.dart
index 21e82bf..480dba0 100644
--- a/lib/screens/metering/metering_screen.dart
+++ b/lib/screens/metering/screen_metering.dart
@@ -7,9 +7,9 @@ import 'package:lightmeter/screens/settings/settings_screen.dart';
import 'components/bottom_controls/bottom_controls.dart';
import 'components/exposure_pairs_list/exposure_pairs_list.dart';
import 'components/topbar/topbar.dart';
-import 'metering_bloc.dart';
-import 'metering_event.dart';
-import 'metering_state.dart';
+import 'bloc_metering.dart';
+import 'event_metering.dart';
+import 'state_metering.dart';
class MeteringScreen extends StatefulWidget {
const MeteringScreen({super.key});
diff --git a/lib/screens/metering/metering_state.dart b/lib/screens/metering/state_metering.dart
similarity index 100%
rename from lib/screens/metering/metering_state.dart
rename to lib/screens/metering/state_metering.dart
diff --git a/pubspec.yaml b/pubspec.yaml
index ae49fe4..c9d5f79 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,12 +1,14 @@
name: lightmeter
description: A new Flutter project.
-publish_to: 'none'
+publish_to: "none"
version: 1.0.0+1
environment:
- sdk: '>=2.18.0 <3.0.0'
+ sdk: ">=2.18.0 <3.0.0"
dependencies:
+ camera: 0.10.0+4
+ exif: 3.1.2
flutter:
sdk: flutter
flutter_bloc: ^8.1.1
@@ -61,4 +63,4 @@ flutter:
# see https://flutter.dev/custom-fonts/#from-packages
flutter_intl:
- enabled: true
\ No newline at end of file
+ enabled: true