Added haptics

added `HapticsService`

added haptics handling

added `HapticsInteractor`
This commit is contained in:
Vadim 2023-01-21 13:37:49 +03:00
parent 9bedc6e665
commit c7ed4d332e
15 changed files with 190 additions and 33 deletions

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -29,6 +30,7 @@ class Application extends StatelessWidget {
return MultiProvider( return MultiProvider(
providers: [ providers: [
Provider(create: (_) => UserPreferencesService(snapshot.data!)), Provider(create: (_) => UserPreferencesService(snapshot.data!)),
Provider(create: (_) => HapticsService()),
Provider.value(value: evSource), Provider.value(value: evSource),
], ],
child: Provider( child: Provider(

View file

@ -0,0 +1,18 @@
import 'package:vibration/vibration.dart';
class HapticsService {
Future<void> quickVibration() async => _tryVibrate(duration: 25, amplitude: 96);
Future<void> responseVibration() async => _tryVibrate(duration: 50, amplitude: 128);
Future<void> _tryVibrate({required int duration, required int amplitude}) async {
if (await _canVibrate()) {
Vibration.vibrate(
duration: duration,
amplitude: amplitude,
);
}
}
Future<bool> _canVibrate() async => await Vibration.hasVibrator() ?? false;
}

View file

@ -5,10 +5,11 @@ import 'models/photography_values/nd_value.dart';
import 'models/theme_type.dart'; import 'models/theme_type.dart';
class UserPreferencesService { class UserPreferencesService {
static const _isoKey = "ISO"; static const _isoKey = "iso";
static const _ndFilterKey = "ND"; static const _ndFilterKey = "nd";
static const _themeTypeKey = "ThemeType"; static const _hapticsKey = "haptics";
static const _themeTypeKey = "themeType";
final SharedPreferences _sharedPreferences; final SharedPreferences _sharedPreferences;
@ -20,6 +21,9 @@ class UserPreferencesService {
NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0)); NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0));
set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value); set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value);
bool get haptics => _sharedPreferences.getBool(_hapticsKey) ?? false;
set haptics(bool value) => _sharedPreferences.setBool(_hapticsKey, value);
ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0]; ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0];
set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index); set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index);
} }

View file

@ -0,0 +1,26 @@
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
class HapticsInteractor {
final UserPreferencesService _userPreferencesService;
final HapticsService _hapticsService;
const HapticsInteractor(
this._userPreferencesService,
this._hapticsService,
);
bool get isEnabled => _userPreferencesService.haptics;
/// Executes vibration if haptics are enabled in settings
void quickVibration() {
if (_userPreferencesService.haptics) _hapticsService.quickVibration();
}
/// Executes vibration if haptics are enabled in settings
void responseVibration() {
if (_userPreferencesService.haptics) _hapticsService.responseVibration();
}
void enableHaptics(bool enable) => _userPreferencesService.haptics = enable;
}

View file

@ -17,6 +17,7 @@
"showFractionalStops": "Show fractional stops", "showFractionalStops": "Show fractional stops",
"halfStops": "1/2", "halfStops": "1/2",
"thirdStops": "1/3", "thirdStops": "1/3",
"haptics": "Haptics",
"theme": "Theme", "theme": "Theme",
"chooseTheme": "Choose theme", "chooseTheme": "Choose theme",
"themeLight": "Light", "themeLight": "Light",

View file

@ -7,6 +7,7 @@ import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart'; import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/data/models/photography_values/shutter_speed_value.dart'; import 'package:lightmeter/data/models/photography_values/shutter_speed_value.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/haptics_interactor.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events; 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/screens/metering/communication/state_communication_metering.dart' as communication_states;
import 'package:lightmeter/utils/log_2.dart'; import 'package:lightmeter/utils/log_2.dart';
@ -18,6 +19,7 @@ import 'state_metering.dart';
class MeteringBloc extends Bloc<MeteringEvent, MeteringState> { class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
final MeteringCommunicationBloc _communicationBloc; final MeteringCommunicationBloc _communicationBloc;
final UserPreferencesService _userPreferencesService; final UserPreferencesService _userPreferencesService;
final HapticsInteractor _hapticsInteractor;
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription; late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
List<ApertureValue> get _apertureValues => apertureValues.whereStopType(stopType); List<ApertureValue> get _apertureValues => apertureValues.whereStopType(stopType);
@ -28,6 +30,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
MeteringBloc( MeteringBloc(
this._communicationBloc, this._communicationBloc,
this._userPreferencesService, this._userPreferencesService,
this._hapticsInteractor,
this.stopType, this.stopType,
) : super( ) : super(
MeteringState( MeteringState(
@ -46,7 +49,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
on<StopTypeChangedEvent>(_onStopTypeChanged); on<StopTypeChangedEvent>(_onStopTypeChanged);
on<IsoChangedEvent>(_onIsoChanged); on<IsoChangedEvent>(_onIsoChanged);
on<NdChangedEvent>(_onNdChanged); on<NdChangedEvent>(_onNdChanged);
on<MeasureEvent>((_, __) => _communicationBloc.add(const communication_events.MeasureEvent())); on<MeasureEvent>(_onMeasure);
on<MeasuredEvent>(_onMeasured); on<MeasuredEvent>(_onMeasured);
add(const MeasureEvent()); add(const MeasureEvent());
@ -99,7 +102,13 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
)); ));
} }
void _onMeasure(_, __) {
_hapticsInteractor.quickVibration();
_communicationBloc.add(const communication_events.MeasureEvent());
}
void _onMeasured(MeasuredEvent event, Emitter emit) { void _onMeasured(MeasuredEvent event, Emitter emit) {
_hapticsInteractor.responseVibration();
final ev = event.ev100 + log2(state.iso.value / 100); final ev = event.ev100 + log2(state.iso.value / 100);
emit(MeteringState( emit(MeteringState(
iso: state.iso, iso: state.iso,

View file

@ -21,7 +21,7 @@ class CameraExposureSlider extends StatelessWidget {
IconButton( IconButton(
icon: const Icon(Icons.sync), icon: const Icon(Icons.sync),
onPressed: state.currentExposureOffset != 0.0 onPressed: state.currentExposureOffset != 0.0
? () => context.read<CameraBloc>().add(const ExposureOffsetChangedEvent(0)) ? () => context.read<CameraBloc>().add(const ExposureOffsetResetEvent())
: null, : null,
), ),
Expanded( Expanded(

View file

@ -6,6 +6,7 @@ import 'package:camera/camera.dart';
import 'package:exif/exif.dart'; import 'package:exif/exif.dart';
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/interactors/haptics_interactor.dart';
import 'package:lightmeter/screens/metering/ev_source/ev_source_bloc.dart'; import 'package:lightmeter/screens/metering/ev_source/ev_source_bloc.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/communication/event_communication_metering.dart' as communication_event; import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_event;
@ -16,6 +17,7 @@ import 'event_camera.dart';
import 'state_camera.dart'; import 'state_camera.dart';
class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> { class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
final HapticsInteractor _hapticsInteractor;
late final _WidgetsBindingObserver _observer; late final _WidgetsBindingObserver _observer;
CameraController? _cameraController; CameraController? _cameraController;
CameraController? get cameraController => _cameraController; CameraController? get cameraController => _cameraController;
@ -29,8 +31,10 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
double _exposureStep = 0.0; double _exposureStep = 0.0;
double _currentExposureOffset = 0.0; double _currentExposureOffset = 0.0;
CameraBloc(MeteringCommunicationBloc communicationBloc) CameraBloc(
: super( MeteringCommunicationBloc communicationBloc,
this._hapticsInteractor,
) : super(
communicationBloc, communicationBloc,
const CameraInitState(), const CameraInitState(),
) { ) {
@ -40,6 +44,7 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
on<InitializeEvent>(_onInitialize); on<InitializeEvent>(_onInitialize);
on<ZoomChangedEvent>(_onZoomChanged); on<ZoomChangedEvent>(_onZoomChanged);
on<ExposureOffsetChangedEvent>(_onExposureOffsetChanged); on<ExposureOffsetChangedEvent>(_onExposureOffsetChanged);
on<ExposureOffsetResetEvent>(_onExposureOffsetResetEvent);
add(const InitializeEvent()); add(const InitializeEvent());
} }
@ -117,6 +122,11 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
_emitActiveState(emit); _emitActiveState(emit);
} }
Future<void> _onExposureOffsetResetEvent(ExposureOffsetResetEvent event, Emitter emit) async {
_hapticsInteractor.quickVibration();
add(const ExposureOffsetChangedEvent(0));
}
void _emitActiveState(Emitter emit) { void _emitActiveState(Emitter emit) {
emit(CameraActiveState( emit(CameraActiveState(
minZoom: _zoomRange!.start, minZoom: _zoomRange!.start,

View file

@ -17,3 +17,7 @@ class ExposureOffsetChangedEvent extends CameraEvent {
const ExposureOffsetChangedEvent(this.value); const ExposureOffsetChangedEvent(this.value);
} }
class ExposureOffsetResetEvent extends CameraEvent {
const ExposureOffsetResetEvent();
}

View file

@ -1,14 +1,16 @@
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/haptics_service.dart';
import 'package:lightmeter/data/models/ev_source_type.dart'; import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart'; import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/haptics_interactor.dart';
import 'package:provider/provider.dart';
import 'ev_source/camera/bloc_camera.dart'; import 'ev_source/camera/bloc_camera.dart';
import 'ev_source/random_ev/bloc_random_ev.dart'; import 'ev_source/random_ev/bloc_random_ev.dart';
import 'bloc_metering.dart'; import 'bloc_metering.dart';
import 'communication/bloc_communication_metering.dart'; import 'communication/bloc_communication_metering.dart';
import 'screen_metering.dart'; import 'screen_metering.dart';
class MeteringFlow extends StatelessWidget { class MeteringFlow extends StatelessWidget {
@ -16,24 +18,36 @@ class MeteringFlow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return Provider(
providers: [ create: (context) => HapticsInteractor(
BlocProvider(create: (_) => MeteringCommunicationBloc()), context.read<UserPreferencesService>(),
BlocProvider( context.read<HapticsService>(),
create: (context) => MeteringBloc( ),
context.read<MeteringCommunicationBloc>(), child: MultiBlocProvider(
context.read<UserPreferencesService>(), providers: [
context.read<StopType>(), BlocProvider(create: (_) => MeteringCommunicationBloc()),
),
),
BlocProvider(create: (context) => CameraBloc(context.read<MeteringCommunicationBloc>())),
if (context.read<EvSourceType>() == EvSourceType.mock)
BlocProvider( BlocProvider(
lazy: false, create: (context) => MeteringBloc(
create: (context) => RandomEvBloc(context.read<MeteringCommunicationBloc>()), context.read<MeteringCommunicationBloc>(),
context.read<UserPreferencesService>(),
context.read<HapticsInteractor>(),
context.read<StopType>(),
),
), ),
], BlocProvider(
child: const MeteringScreen(), create: (context) => CameraBloc(
context.read<MeteringCommunicationBloc>(),
context.read<HapticsInteractor>(),
),
),
if (context.read<EvSourceType>() == EvSourceType.mock)
BlocProvider(
lazy: false,
create: (context) => RandomEvBloc(context.read<MeteringCommunicationBloc>()),
),
],
child: const MeteringScreen(),
),
); );
} }
} }

View file

@ -0,0 +1,18 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/haptics_interactor.dart';
class HapticsListTileBloc extends Cubit<bool> {
final HapticsInteractor _hapticsInteractor;
HapticsListTileBloc(
this._hapticsInteractor,
) : super(_hapticsInteractor.isEnabled);
void onHapticsChange(bool value) {
_hapticsInteractor.enableHaptics(value);
if (value) {
_hapticsInteractor.quickVibration();
}
emit(value);
}
}

View file

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/haptics_interactor.dart';
import 'package:provider/provider.dart';
import 'bloc_list_tile_haptics.dart';
import 'widget_list_tile_haptics.dart';
class HapticsListTileProvider extends StatelessWidget {
const HapticsListTileProvider({super.key});
@override
Widget build(BuildContext context) {
return Provider(
create: (context) => HapticsInteractor(
context.read<UserPreferencesService>(),
context.read<HapticsService>(),
),
child: BlocProvider(
create: (context) => HapticsListTileBloc(
context.read<HapticsInteractor>()
),
child: const HapticsListTile(),
),
);
}
}

View file

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'bloc_list_tile_haptics.dart';
class HapticsListTile extends StatelessWidget {
const HapticsListTile({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<HapticsListTileBloc, bool>(
builder: (context, state) => SwitchListTile(
secondary: const Icon(Icons.vibration),
title: Text(S.of(context).haptics),
value: state,
onChanged: context.read<HapticsListTileBloc>().onHapticsChange,
),
);
}
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'components/haptics/provider_list_tile_haptics.dart';
import 'components/widget_list_tile_fractional_stops.dart'; import 'components/widget_list_tile_fractional_stops.dart';
import 'components/widget_list_tile_theme_type.dart'; import 'components/widget_list_tile_theme_type.dart';
import 'components/widget_label_version.dart'; import 'components/widget_label_version.dart';
@ -40,8 +41,7 @@ class SettingsScreen extends StatelessWidget {
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
[ [
const StopTypeListTile(), const StopTypeListTile(),
// const CaffeineListTile(), const HapticsListTileProvider(),
// const HapticsListTile(),
const ThemeTypeListTile(), const ThemeTypeListTile(),
], ],
), ),

View file

@ -11,26 +11,27 @@ dependencies:
exif: 3.1.2 exif: 3.1.2
flutter: flutter:
sdk: flutter sdk: flutter
flutter_bloc: ^8.1.1 flutter_bloc: 8.1.1
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
intl: 0.17.0 intl: 0.17.0
intl_utils: 2.8.1 intl_utils: 2.8.1
material_color_utilities: ^0.2.0 material_color_utilities: 0.2.0
package_info_plus: 3.0.2 package_info_plus: 3.0.2
permission_handler: 10.2.0 permission_handler: 10.2.0
provider: ^6.0.4 provider: 6.0.4
shared_preferences: 2.0.15 shared_preferences: 2.0.15
vibration: 1.7.6
dev_dependencies: dev_dependencies:
google_fonts: ^3.0.1 google_fonts: 3.0.1
flutter_launcher_icons: 0.11.0 flutter_launcher_icons: 0.11.0
flutter_lints: ^2.0.0 flutter_lints: 2.0.0
flutter_native_splash: 2.2.16 flutter_native_splash: 2.2.16
test: ^1.21.6 test: 1.21.6
dependency_overrides: dependency_overrides:
material_color_utilities: ^0.2.0 material_color_utilities: 0.2.0
flutter: flutter:
uses-material-design: true uses-material-design: true