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/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -29,6 +30,7 @@ class Application extends StatelessWidget {
return MultiProvider(
providers: [
Provider(create: (_) => UserPreferencesService(snapshot.data!)),
Provider(create: (_) => HapticsService()),
Provider.value(value: evSource),
],
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';
class UserPreferencesService {
static const _isoKey = "ISO";
static const _ndFilterKey = "ND";
static const _isoKey = "iso";
static const _ndFilterKey = "nd";
static const _themeTypeKey = "ThemeType";
static const _hapticsKey = "haptics";
static const _themeTypeKey = "themeType";
final SharedPreferences _sharedPreferences;
@ -20,6 +21,9 @@ class UserPreferencesService {
NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0));
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];
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",
"halfStops": "1/2",
"thirdStops": "1/3",
"haptics": "Haptics",
"theme": "Theme",
"chooseTheme": "Choose theme",
"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/shutter_speed_value.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/state_communication_metering.dart' as communication_states;
import 'package:lightmeter/utils/log_2.dart';
@ -18,6 +19,7 @@ import 'state_metering.dart';
class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
final MeteringCommunicationBloc _communicationBloc;
final UserPreferencesService _userPreferencesService;
final HapticsInteractor _hapticsInteractor;
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
List<ApertureValue> get _apertureValues => apertureValues.whereStopType(stopType);
@ -28,6 +30,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
MeteringBloc(
this._communicationBloc,
this._userPreferencesService,
this._hapticsInteractor,
this.stopType,
) : super(
MeteringState(
@ -46,7 +49,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
on<StopTypeChangedEvent>(_onStopTypeChanged);
on<IsoChangedEvent>(_onIsoChanged);
on<NdChangedEvent>(_onNdChanged);
on<MeasureEvent>((_, __) => _communicationBloc.add(const communication_events.MeasureEvent()));
on<MeasureEvent>(_onMeasure);
on<MeasuredEvent>(_onMeasured);
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) {
_hapticsInteractor.responseVibration();
final ev = event.ev100 + log2(state.iso.value / 100);
emit(MeteringState(
iso: state.iso,

View file

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

View file

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

View file

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

View file

@ -1,14 +1,16 @@
import 'package:flutter/material.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/photography_values/photography_value.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/random_ev/bloc_random_ev.dart';
import 'bloc_metering.dart';
import 'communication/bloc_communication_metering.dart';
import 'screen_metering.dart';
class MeteringFlow extends StatelessWidget {
@ -16,17 +18,28 @@ class MeteringFlow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
return Provider(
create: (context) => HapticsInteractor(
context.read<UserPreferencesService>(),
context.read<HapticsService>(),
),
child: MultiBlocProvider(
providers: [
BlocProvider(create: (_) => MeteringCommunicationBloc()),
BlocProvider(
create: (context) => MeteringBloc(
context.read<MeteringCommunicationBloc>(),
context.read<UserPreferencesService>(),
context.read<HapticsInteractor>(),
context.read<StopType>(),
),
),
BlocProvider(create: (context) => CameraBloc(context.read<MeteringCommunicationBloc>())),
BlocProvider(
create: (context) => CameraBloc(
context.read<MeteringCommunicationBloc>(),
context.read<HapticsInteractor>(),
),
),
if (context.read<EvSourceType>() == EvSourceType.mock)
BlocProvider(
lazy: false,
@ -34,6 +47,7 @@ class MeteringFlow extends StatelessWidget {
),
],
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/res/dimens.dart';
import 'components/haptics/provider_list_tile_haptics.dart';
import 'components/widget_list_tile_fractional_stops.dart';
import 'components/widget_list_tile_theme_type.dart';
import 'components/widget_label_version.dart';
@ -40,8 +41,7 @@ class SettingsScreen extends StatelessWidget {
delegate: SliverChildListDelegate(
[
const StopTypeListTile(),
// const CaffeineListTile(),
// const HapticsListTile(),
const HapticsListTileProvider(),
const ThemeTypeListTile(),
],
),

View file

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