mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-22 07:20:39 +00:00
Added zoom and exposure sliders
added `ZoomSlider` (wip) wip implemented horizontal slider implemented vertical slider integrated 2 sliders to metering screen (wip) removed filled track placed camera slider in the proper folder temp fix for exposure list fonts update exposure event added ruler to `CameraExposureSlider` moved slider sizes to dimens moved `CameraZoomSlider` to the separate folder
This commit is contained in:
parent
14bac950cf
commit
abd07764fd
12 changed files with 420 additions and 12 deletions
|
@ -20,4 +20,10 @@ class Dimens {
|
|||
static const Duration durationM = Duration(milliseconds: 200);
|
||||
static const Duration durationML = Duration(milliseconds: 250);
|
||||
static const Duration durationL = Duration(milliseconds: 300);
|
||||
|
||||
// `CameraSlider`
|
||||
static const double cameraSliderTrackHeight = grid4;
|
||||
static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2;
|
||||
static const double cameraSliderHandleSize = 32;
|
||||
static const double cameraSliderHandleIconSize = cameraSliderHandleSize * 2 / 3;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
|
||||
class CameraSlider extends StatefulWidget {
|
||||
final Icon icon;
|
||||
final double value;
|
||||
final double min;
|
||||
final double max;
|
||||
final ValueChanged<double> onChanged;
|
||||
final bool isVertical;
|
||||
|
||||
const CameraSlider({
|
||||
required this.icon,
|
||||
required this.value,
|
||||
required this.min,
|
||||
required this.max,
|
||||
required this.onChanged,
|
||||
this.isVertical = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CameraSlider> createState() => _CameraSliderState();
|
||||
}
|
||||
|
||||
class _CameraSliderState extends State<CameraSlider> {
|
||||
double relativeValue = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
relativeValue = (widget.value - widget.min) / (widget.max - widget.min);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CameraSlider oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
relativeValue = (widget.value - widget.min) / (widget.max - widget.min);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final biggestSize = widget.isVertical ? constraints.maxHeight : constraints.maxWidth;
|
||||
final handleDistance = biggestSize - Dimens.cameraSliderHandleSize;
|
||||
return RotatedBox(
|
||||
quarterTurns: widget.isVertical ? -1 : 0,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapUp: (details) => _updateHandlePosition(details.localPosition.dx, handleDistance),
|
||||
onHorizontalDragUpdate: (details) => _updateHandlePosition(details.localPosition.dx, handleDistance),
|
||||
child: SizedBox(
|
||||
height: Dimens.cameraSliderHandleSize,
|
||||
width: biggestSize,
|
||||
child: _Slider(
|
||||
handleDistance: handleDistance,
|
||||
handleSize: Dimens.cameraSliderHandleSize,
|
||||
trackThickness: Dimens.cameraSliderTrackHeight,
|
||||
value: relativeValue,
|
||||
icon: RotatedBox(
|
||||
quarterTurns: widget.isVertical ? 1 : 0,
|
||||
child: widget.icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateHandlePosition(double offset, double handleDistance) {
|
||||
if (offset <= Dimens.cameraSliderHandleSize / 2) {
|
||||
relativeValue = 0;
|
||||
} else if (offset >= handleDistance + Dimens.cameraSliderHandleSize / 2) {
|
||||
relativeValue = 1;
|
||||
} else {
|
||||
relativeValue = (offset - Dimens.cameraSliderHandleSize / 2) / handleDistance;
|
||||
}
|
||||
setState(() {});
|
||||
widget.onChanged(_clampToRange(relativeValue));
|
||||
}
|
||||
|
||||
double _clampToRange(double relativeValue) => relativeValue * (widget.max - widget.min) + widget.min;
|
||||
}
|
||||
|
||||
class _Slider extends StatelessWidget {
|
||||
final double handleSize;
|
||||
final double trackThickness;
|
||||
final double handleDistance;
|
||||
final double value;
|
||||
final Widget icon;
|
||||
|
||||
const _Slider({
|
||||
required this.handleSize,
|
||||
required this.trackThickness,
|
||||
required this.handleDistance,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
height: trackThickness,
|
||||
width: handleDistance + trackThickness, // add thickness to maintain radius overlap with handle
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(trackThickness / 2),
|
||||
child: ColoredBox(color: Theme.of(context).colorScheme.surfaceVariant),
|
||||
),
|
||||
),
|
||||
AnimatedPositioned.fromRect(
|
||||
duration: Dimens.durationM,
|
||||
rect: Rect.fromCenter(
|
||||
center: Offset(
|
||||
handleSize / 2 + handleDistance * value,
|
||||
handleSize / 2,
|
||||
),
|
||||
width: handleSize,
|
||||
height: handleSize,
|
||||
),
|
||||
child: _Handle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: handleSize,
|
||||
child: IconTheme(
|
||||
data: Theme.of(context).iconTheme.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
size: Dimens.cameraSliderHandleIconSize,
|
||||
),
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Handle extends StatelessWidget {
|
||||
final Color color;
|
||||
final double size;
|
||||
final Widget? child;
|
||||
|
||||
const _Handle({
|
||||
required this.color,
|
||||
required this.size,
|
||||
this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(size / 2),
|
||||
child: SizedBox(
|
||||
height: size,
|
||||
width: size,
|
||||
child: ColoredBox(
|
||||
color: color,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/metering/ev_source/camera/bloc_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/ev_source/camera/event_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/ev_source/camera/state_camera.dart';
|
||||
import 'package:lightmeter/utils/to_string_signed.dart';
|
||||
|
||||
import 'shared/widget_slider_camera.dart';
|
||||
|
||||
class CameraExposureSlider extends StatelessWidget {
|
||||
const CameraExposureSlider({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CameraBloc, CameraState>(
|
||||
builder: (context, state) {
|
||||
if (state is CameraActiveState) {
|
||||
return Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sync),
|
||||
onPressed: state.currentExposureOffset != 0.0
|
||||
? () => context.read<CameraBloc>().add(const ExposureOffsetChangedEvent(0))
|
||||
: null,
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: Dimens.grid8),
|
||||
child: _Ruler(state.minExposureOffset, state.maxExposureOffset),
|
||||
),
|
||||
CameraSlider(
|
||||
isVertical: true,
|
||||
icon: const Icon(Icons.light_mode),
|
||||
value: state.currentExposureOffset,
|
||||
min: state.minExposureOffset,
|
||||
max: state.maxExposureOffset,
|
||||
onChanged: (value) {
|
||||
context.read<CameraBloc>().add(ExposureOffsetChangedEvent(value));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Ruler extends StatelessWidget {
|
||||
final double min;
|
||||
final double max;
|
||||
|
||||
const _Ruler(this.min, this.max);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(
|
||||
(max - min + 1).toInt(),
|
||||
(index) {
|
||||
final bool showValue = index % 2 == 0.0 || index == 0.0;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (showValue)
|
||||
Text(
|
||||
(index + min).toStringSigned(),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(width: Dimens.grid8),
|
||||
ColoredBox(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
child: SizedBox(
|
||||
height: 1,
|
||||
width: showValue ? Dimens.grid16 : Dimens.grid8,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Dimens.grid8),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/screens/metering/ev_source/camera/bloc_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/ev_source/camera/event_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/ev_source/camera/state_camera.dart';
|
||||
|
||||
import 'shared/widget_slider_camera.dart';
|
||||
|
||||
class CameraZoomSlider extends StatelessWidget {
|
||||
const CameraZoomSlider({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CameraBloc, CameraState>(
|
||||
builder: (context, state) {
|
||||
if (state is CameraActiveState) {
|
||||
return CameraSlider(
|
||||
icon: const Icon(Icons.search),
|
||||
value: state.currentZoom,
|
||||
min: state.minZoom,
|
||||
max: state.maxZoom,
|
||||
onChanged: (value) {
|
||||
context.read<CameraBloc>().add(ZoomChangedEvent(value));
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ class ExposurePairsListItem<T extends PhotographyStopValue> extends StatelessWid
|
|||
width: tickLength(),
|
||||
),
|
||||
),
|
||||
if (value.stopType != StopType.full) const SizedBox(width: Dimens.grid8),
|
||||
if (value.stopType != StopType.full) const SizedBox(width: Dimens.grid4),
|
||||
];
|
||||
return Row(
|
||||
mainAxisAlignment: tickOnTheLeft ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||
|
@ -45,9 +45,9 @@ class ExposurePairsListItem<T extends PhotographyStopValue> extends StatelessWid
|
|||
double tickLength() {
|
||||
switch (value.stopType) {
|
||||
case StopType.full:
|
||||
return Dimens.grid24;
|
||||
case StopType.half:
|
||||
return Dimens.grid16;
|
||||
case StopType.half:
|
||||
return Dimens.grid8;
|
||||
case StopType.third:
|
||||
return Dimens.grid8;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ class ExposurePairsList extends StatelessWidget {
|
|||
Positioned.fill(
|
||||
child: ListView.builder(
|
||||
key: ValueKey(exposurePairs.hashCode),
|
||||
padding: const EdgeInsets.all(Dimens.paddingL),
|
||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL),
|
||||
itemCount: exposurePairs.length,
|
||||
itemBuilder: (_, index) => Stack(
|
||||
alignment: Alignment.center,
|
||||
|
|
|
@ -13,14 +13,14 @@ class CameraView extends StatelessWidget {
|
|||
return AspectRatio(
|
||||
aspectRatio: 3 / 4,
|
||||
child: BlocBuilder<CameraBloc, CameraState>(
|
||||
buildWhen: (previous, current) => current is CameraInitializedState,
|
||||
builder: (context, state) {
|
||||
if (state is CameraReadyState) {
|
||||
if (state is CameraInitializedState) {
|
||||
final value = state.controller.value;
|
||||
return ValueListenableBuilder<CameraValue>(
|
||||
valueListenable: state.controller,
|
||||
builder: (_, __, ___) => AspectRatio(
|
||||
aspectRatio:
|
||||
_isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio),
|
||||
aspectRatio: _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio),
|
||||
child: RotatedBox(
|
||||
quarterTurns: _getQuarterTurns(value),
|
||||
child: state.controller.buildPreview(),
|
||||
|
|
|
@ -4,7 +4,7 @@ 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/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/screens/metering/ev_source/ev_source_bloc.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
|
@ -20,6 +20,15 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
|
|||
CameraController? _cameraController;
|
||||
CameraController? get cameraController => _cameraController;
|
||||
|
||||
static const _maxZoom = 7.0;
|
||||
RangeValues? _zoomRange;
|
||||
double _currentZoom = 0.0;
|
||||
|
||||
static const _exposureMaxRange = RangeValues(-4, 4);
|
||||
RangeValues? _exposureOffsetRange;
|
||||
double _exposureStep = 0.0;
|
||||
double _currentExposureOffset = 0.0;
|
||||
|
||||
CameraBloc(MeteringCommunicationBloc communicationBloc)
|
||||
: super(
|
||||
communicationBloc,
|
||||
|
@ -29,6 +38,8 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
|
|||
WidgetsBinding.instance.addObserver(_observer);
|
||||
|
||||
on<InitializeEvent>(_onInitialize);
|
||||
on<ZoomChangedEvent>(_onZoomChanged);
|
||||
on<ExposureOffsetChangedEvent>(_onExposureOffsetChanged);
|
||||
|
||||
add(const InitializeEvent());
|
||||
}
|
||||
|
@ -66,7 +77,24 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
|
|||
|
||||
await _cameraController!.initialize();
|
||||
await _cameraController!.setFlashMode(FlashMode.off);
|
||||
emit(CameraReadyState(_cameraController!));
|
||||
|
||||
_zoomRange = await Future.wait<double>([
|
||||
_cameraController!.getMinZoomLevel(),
|
||||
_cameraController!.getMaxZoomLevel(),
|
||||
]).then((levels) => RangeValues(levels[0], min(_maxZoom, levels[1])));
|
||||
_currentZoom = _zoomRange!.start;
|
||||
|
||||
_exposureOffsetRange = await Future.wait<double>([
|
||||
_cameraController!.getMinExposureOffset(),
|
||||
_cameraController!.getMaxExposureOffset(),
|
||||
]).then((levels) => RangeValues(max(_exposureMaxRange.start, levels[0]), min(_exposureMaxRange.end, levels[1])));
|
||||
await _cameraController!.getExposureOffsetStepSize().then((value) {
|
||||
_exposureStep = value == 0 ? 0.1 : value;
|
||||
});
|
||||
|
||||
emit(CameraInitializedState(_cameraController!));
|
||||
|
||||
_emitActiveState(emit);
|
||||
_takePhoto().then((ev100) {
|
||||
if (ev100 != null) {
|
||||
communicationBloc.add(communication_event.MeasuredEvent(ev100));
|
||||
|
@ -77,6 +105,30 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
|
||||
_cameraController!.setZoomLevel(event.value);
|
||||
_currentZoom = event.value;
|
||||
_emitActiveState(emit);
|
||||
}
|
||||
|
||||
Future<void> _onExposureOffsetChanged(ExposureOffsetChangedEvent event, Emitter emit) async {
|
||||
_cameraController!.setExposureOffset(event.value);
|
||||
_currentExposureOffset = event.value;
|
||||
_emitActiveState(emit);
|
||||
}
|
||||
|
||||
void _emitActiveState(Emitter emit) {
|
||||
emit(CameraActiveState(
|
||||
minZoom: _zoomRange!.start,
|
||||
maxZoom: _zoomRange!.end,
|
||||
currentZoom: _currentZoom,
|
||||
minExposureOffset: _exposureOffsetRange!.start,
|
||||
maxExposureOffset: _exposureOffsetRange!.end,
|
||||
exposureOffsetStep: _exposureStep,
|
||||
currentExposureOffset: _currentExposureOffset,
|
||||
));
|
||||
}
|
||||
|
||||
Future<double?> _takePhoto() async {
|
||||
if (_cameraController == null ||
|
||||
!_cameraController!.value.isInitialized ||
|
||||
|
|
|
@ -4,4 +4,16 @@ abstract class CameraEvent {
|
|||
|
||||
class InitializeEvent extends CameraEvent {
|
||||
const InitializeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
class ZoomChangedEvent extends CameraEvent {
|
||||
final double value;
|
||||
|
||||
const ZoomChangedEvent(this.value);
|
||||
}
|
||||
|
||||
class ExposureOffsetChangedEvent extends CameraEvent {
|
||||
final double value;
|
||||
|
||||
const ExposureOffsetChangedEvent(this.value);
|
||||
}
|
||||
|
|
|
@ -12,10 +12,30 @@ class CameraLoadingState extends CameraState {
|
|||
const CameraLoadingState();
|
||||
}
|
||||
|
||||
class CameraReadyState extends CameraState {
|
||||
class CameraInitializedState extends CameraState {
|
||||
final CameraController controller;
|
||||
|
||||
const CameraReadyState(this.controller);
|
||||
const CameraInitializedState(this.controller);
|
||||
}
|
||||
|
||||
class CameraActiveState extends CameraState {
|
||||
final double minZoom;
|
||||
final double maxZoom;
|
||||
final double currentZoom;
|
||||
final double minExposureOffset;
|
||||
final double maxExposureOffset;
|
||||
final double? exposureOffsetStep;
|
||||
final double currentExposureOffset;
|
||||
|
||||
const CameraActiveState({
|
||||
required this.minZoom,
|
||||
required this.maxZoom,
|
||||
required this.currentZoom,
|
||||
required this.minExposureOffset,
|
||||
required this.maxExposureOffset,
|
||||
required this.exposureOffsetStep,
|
||||
required this.currentExposureOffset,
|
||||
});
|
||||
}
|
||||
|
||||
class CameraErrorState extends CameraState {
|
||||
|
|
|
@ -5,6 +5,8 @@ import 'package:lightmeter/res/dimens.dart';
|
|||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||
|
||||
import 'components/bottom_controls/widget_bottom_controls.dart';
|
||||
import 'components/camera/widget_exposure_slider.dart';
|
||||
import 'components/camera/widget_zoom_camera.dart';
|
||||
import 'components/exposure_pairs_list/widget_list_exposure_pairs.dart';
|
||||
import 'components/topbar/widget_topbar.dart';
|
||||
import 'bloc_metering.dart';
|
||||
|
@ -42,6 +44,19 @@ class _MeteringScreenState extends State<MeteringScreen> {
|
|||
child: Row(
|
||||
children: [
|
||||
Expanded(child: ExposurePairsList(state.exposurePairs)),
|
||||
const SizedBox(width: Dimens.grid8),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
|
||||
child: Column(
|
||||
children: const [
|
||||
Expanded(child: CameraExposureSlider()),
|
||||
SizedBox(height: Dimens.grid24),
|
||||
CameraZoomSlider(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
9
lib/utils/to_string_signed.dart
Normal file
9
lib/utils/to_string_signed.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
extension SignedString on num {
|
||||
String toStringSigned() {
|
||||
if (this > 0) {
|
||||
return "+${toString()}";
|
||||
} else {
|
||||
return toString();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue