diff --git a/lib/data/models/exposure_pair.dart b/lib/data/models/exposure_pair.dart index cea5a69..bff3112 100644 --- a/lib/data/models/exposure_pair.dart +++ b/lib/data/models/exposure_pair.dart @@ -6,4 +6,7 @@ class ExposurePair { final ShutterSpeedValue shutterSpeed; const ExposurePair(this.aperture, this.shutterSpeed); + + @override + String toString() => '${aperture.toString()} - ${shutterSpeed.toString()}'; } diff --git a/lib/screens/metering/components/topbar/shape_topbar.dart b/lib/screens/metering/components/topbar/shape_topbar.dart new file mode 100644 index 0000000..aa79d73 --- /dev/null +++ b/lib/screens/metering/components/topbar/shape_topbar.dart @@ -0,0 +1,97 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; + +class TopBarShape extends CustomPainter { + final Color color; + + /// The appendix is on the left side + /// but if appendix height is negative, then we have to make a cutout + /// + /// negative positive + /// | | /// | | + /// | | /// | | + /// | | /// | | + /// | | /// | | + /// \________ | /// | ________/ + /// \ | /// | / ↑ + /// | | /// | | | appendix height + /// \__________/ /// \__________/ ↓ + /// + final double appendixHeight; + final double appendixWidth; + + TopBarShape({ + required this.color, + required this.appendixHeight, + required this.appendixWidth, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + final path = Path(); + const circularRadius = Radius.circular(Dimens.borderRadiusL); + if (appendixHeight == 0 || appendixWidth == 0) { + path.addRRect( + RRect.fromLTRBAndCorners( + 0, + 0, + 0, + 0, + bottomLeft: circularRadius, + bottomRight: circularRadius, + ), + ); + } else { + // Left side with bottom corner + path.lineTo(0, size.height + appendixHeight - Dimens.borderRadiusL); + path.arcToPoint( + Offset(Dimens.borderRadiusL, size.height + appendixHeight), + radius: circularRadius, + clockwise: false, + ); + + // Bottom side with step + final double allowedRadius = min(appendixHeight.abs() / 2, Dimens.borderRadiusL); + path.lineTo(appendixWidth - allowedRadius, size.height + appendixHeight); + + final bool isCutout = appendixHeight < 0; + if (isCutout) { + path.arcToPoint( + Offset(appendixWidth, size.height + appendixHeight + allowedRadius), + radius: circularRadius, + clockwise: true, + ); + path.lineTo(appendixWidth, size.height - allowedRadius); + } else { + path.arcToPoint( + Offset(appendixWidth, size.height + appendixHeight - allowedRadius), + radius: circularRadius, + clockwise: false, + ); + path.lineTo(appendixWidth, size.height + allowedRadius); + } + path.arcToPoint( + Offset(appendixWidth + allowedRadius, size.height), + radius: circularRadius, + clockwise: !isCutout, + ); + + // Right side with bottom corner + path.lineTo(size.width - Dimens.borderRadiusL, size.height); + path.arcToPoint( + Offset(size.width, size.height - Dimens.borderRadiusL), + radius: circularRadius, + clockwise: false, + ); + path.lineTo(size.width, 0); + path.close(); + } + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/screens/metering/components/topbar/widget_topbar.dart b/lib/screens/metering/components/topbar/widget_topbar.dart index 2a8ca34..e524751 100644 --- a/lib/screens/metering/components/topbar/widget_topbar.dart +++ b/lib/screens/metering/components/topbar/widget_topbar.dart @@ -1,6 +1,7 @@ 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/platform_config.dart'; +import 'package:lightmeter/screens/metering/components/topbar/shape_topbar.dart'; +import 'package:lightmeter/screens/metering/components/widget_size_render.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/photography_values/iso_value.dart'; @@ -13,7 +14,7 @@ import 'components/shared/widget_dialog_animated.dart'; import 'components/widget_dialog_picker.dart'; import 'components/container_reading_value.dart'; -class MeteringTopBar extends StatelessWidget { +class MeteringTopBar extends StatefulWidget { final ExposurePair? fastest; final ExposurePair? slowest; final double ev; @@ -22,6 +23,8 @@ class MeteringTopBar extends StatelessWidget { final ValueChanged onIsoChanged; final ValueChanged onNdChanged; + final ValueChanged onCutoutLayout; + const MeteringTopBar({ required this.fastest, required this.slowest, @@ -30,29 +33,40 @@ class MeteringTopBar extends StatelessWidget { required this.nd, required this.onIsoChanged, required this.onNdChanged, + required this.onCutoutLayout, super.key, }); + @override + State createState() => _MeteringTopBarState(); +} + +class _MeteringTopBarState extends State { + double stepHeight = 0.0; + @override Widget build(BuildContext context) { - return ClipRRect( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(Dimens.borderRadiusL), - bottomRight: Radius.circular(Dimens.borderRadiusL), - ), - child: ColoredBox( + return CustomPaint( + painter: TopBarShape( color: Theme.of(context).colorScheme.surface, - child: Padding( - padding: const EdgeInsets.all(Dimens.paddingM), - child: SafeArea( - bottom: false, - child: MediaQuery( - data: MediaQuery.of(context), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( + appendixWidth: stepHeight > 0 + ? MediaQuery.of(context).size.width / 2 - Dimens.grid8 + Dimens.paddingM + : MediaQuery.of(context).size.width / 2 + Dimens.grid8 - Dimens.paddingM, + appendixHeight: stepHeight, + ), + child: Padding( + padding: const EdgeInsets.all(Dimens.paddingM), + child: SafeArea( + bottom: false, + child: MediaQuery( + data: MediaQuery.of(context), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: ReadingsContainer( + onLayout: (size) => _onReadingsLayout(size.height), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -60,15 +74,11 @@ class MeteringTopBar extends StatelessWidget { values: [ ReadingValue( label: S.of(context).fastestExposurePair, - value: fastest != null - ? '${fastest!.aperture.toString()} - ${fastest!.shutterSpeed.toString()}' - : '-', + value: widget.fastest != null ? widget.fastest!.toString() : '-', ), ReadingValue( label: S.of(context).slowestExposurePair, - value: fastest != null - ? '${slowest!.aperture.toString()} - ${slowest!.shutterSpeed.toString()}' - : '-', + value: widget.fastest != null ? widget.slowest!.toString() : '-', ), ], ), @@ -85,30 +95,16 @@ class MeteringTopBar extends StatelessWidget { Row( children: [ Expanded( - child: _AnimatedDialogPicker( - title: S.of(context).iso, - subtitle: S.of(context).filmSpeed, - selectedValue: iso, - values: isoValues, - itemTitleBuilder: (_, value) => Text(value.value.toString()), - // using ascending order, because increase in film speed rises EV - evDifferenceBuilder: (selected, other) => selected.toStringDifference(other), - onChanged: onIsoChanged, + child: _IsoValueTile( + value: widget.iso, + onChanged: widget.onIsoChanged, ), ), const _InnerPadding(), Expanded( - child: _AnimatedDialogPicker( - title: S.of(context).nd, - subtitle: S.of(context).ndFilterFactor, - selectedValue: nd, - values: ndValues, - itemTitleBuilder: (_, value) => Text( - value.value == 0 ? S.of(context).none : value.value.toString(), - ), - // using descending order, because ND filter darkens image & lowers EV - evDifferenceBuilder: (selected, other) => other.toStringDifference(selected), - onChanged: onNdChanged, + child: _NdValueTile( + value: widget.nd, + onChanged: widget.onNdChanged, ), ), ], @@ -116,33 +112,76 @@ class MeteringTopBar extends StatelessWidget { ], ), ), - const _InnerPadding(), - Expanded( - child: AnimatedDialog( - openedSize: Size( - MediaQuery.of(context).size.width - Dimens.paddingM * 2, - (MediaQuery.of(context).size.width - Dimens.paddingM * 2) / 3 * 4, - ), - child: BlocProvider.value( - value: context.read(), - child: const CameraView(), - ), - ), + ), + const _InnerPadding(), + const Expanded( + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(Dimens.borderRadiusM)), + child: CameraView(), ), - ], - ), + ), + ], ), ), ), ), ); } + + void _onReadingsLayout(double readingsSectionHeight) { + stepHeight = readingsSectionHeight - + ((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) / + PlatformConfig.cameraPreviewAspectRatio; + widget.onCutoutLayout(stepHeight); + } } class _InnerPadding extends SizedBox { const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8); } +class _IsoValueTile extends StatelessWidget { + final IsoValue value; + final ValueChanged onChanged; + + const _IsoValueTile({required this.value, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return _AnimatedDialogPicker( + title: S.of(context).iso, + subtitle: S.of(context).filmSpeed, + selectedValue: value, + values: isoValues, + itemTitleBuilder: (_, value) => Text(value.value.toString()), + // using ascending order, because increase in film speed rises EV + evDifferenceBuilder: (selected, other) => selected.toStringDifference(other), + onChanged: onChanged, + ); + } +} + +class _NdValueTile extends StatelessWidget { + final NdValue value; + final ValueChanged onChanged; + + const _NdValueTile({required this.value, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return _AnimatedDialogPicker( + title: S.of(context).nd, + subtitle: S.of(context).ndFilterFactor, + selectedValue: value, + values: ndValues, + itemTitleBuilder: (_, value) => Text(value.value == 0 ? S.of(context).none : value.value.toString()), + // using descending order, because ND filter darkens image & lowers EV + evDifferenceBuilder: (selected, other) => other.toStringDifference(selected), + onChanged: onChanged, + ); + } +} + class _AnimatedDialogPicker extends StatelessWidget { final _key = GlobalKey(); final String title; diff --git a/lib/screens/metering/components/widget_size_render.dart b/lib/screens/metering/components/widget_size_render.dart new file mode 100644 index 0000000..f0e60ee --- /dev/null +++ b/lib/screens/metering/components/widget_size_render.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class ReadingsContainer extends SingleChildRenderObjectWidget { + final ValueChanged? onLayout; + + const ReadingsContainer({ + super.key, + super.child, + this.onLayout, + }); + + @override + RenderReadingsContainer createRenderObject(BuildContext context) => RenderReadingsContainer(onLayout: onLayout); +} + +class RenderReadingsContainer extends RenderProxyBox { + final ValueChanged? onLayout; + + RenderReadingsContainer({this.onLayout}); + + @override + void performLayout() { + if (child != null) { + child!.layout(constraints, parentUsesSize: true); + size = child!.size; + } else { + size = computeSizeForNoChild(constraints); + } + onLayout?.call(size); + } +} diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index 8aab0da..477878a 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -21,6 +21,8 @@ class MeteringScreen extends StatefulWidget { } class _MeteringScreenState extends State { + double topBarOverflow = 0.0; + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -37,31 +39,59 @@ class _MeteringScreenState extends State { children: [ Column( children: [ - _topBar(state), + MeteringTopBar( + fastest: state.fastest, + slowest: state.slowest, + ev: state.ev, + iso: state.iso, + nd: state.nd, + onIsoChanged: (value) => context.read().add(IsoChangedEvent(value)), + onNdChanged: (value) => context.read().add(NdChangedEvent(value)), + onCutoutLayout: (value) => topBarOverflow = value, + ), Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), - 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(), - ], + child: LayoutBuilder( + builder: (context, constraints) => OverflowBox( + alignment: Alignment.bottomCenter, + maxHeight: constraints.maxHeight + topBarOverflow.abs(), + maxWidth: constraints.maxWidth, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), + child: Row( + children: [ + Expanded( + child: Padding( + padding: topBarOverflow >= 0 ? EdgeInsets.only(top: topBarOverflow) : EdgeInsets.zero, + child: ExposurePairsList(state.exposurePairs), + ), ), - ), + const SizedBox(width: Dimens.grid8), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM).add( + topBarOverflow <= 0 ? EdgeInsets.only(top: -topBarOverflow) : EdgeInsets.zero), + child: Column( + children: const [ + Expanded(child: CameraExposureSlider()), + SizedBox(height: Dimens.grid24), + CameraZoomSlider(), + ], + ), + ), + ), + ], ), - ], + ), ), ), ), - _bottomBar(), + MeteringBottomControls( + onSourceChanged: () {}, + onMeasure: () => context.read().add(const MeasureEvent()), + onSettings: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen())); + }, + ), ], ), ], @@ -70,26 +100,4 @@ class _MeteringScreenState extends State { ), ); } - - Widget _topBar(MeteringState state) { - return MeteringTopBar( - fastest: state.fastest, - slowest: state.slowest, - ev: state.ev, - iso: state.iso, - nd: state.nd, - onIsoChanged: (value) => context.read().add(IsoChangedEvent(value)), - onNdChanged: (value) => context.read().add(NdChangedEvent(value)), - ); - } - - Widget _bottomBar() { - return MeteringBottomControls( - onSourceChanged: () {}, - onMeasure: () => context.read().add(const MeasureEvent()), - onSettings: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen())); - }, - ); - } }