Made TopBarShape responsive

returned `TopBarShape`

fixed `TopBarShape`
This commit is contained in:
Vadim 2023-01-20 22:31:35 +03:00
parent 5eb0869aa0
commit 5bea96669d
5 changed files with 280 additions and 101 deletions

View file

@ -6,4 +6,7 @@ class ExposurePair {
final ShutterSpeedValue shutterSpeed; final ShutterSpeedValue shutterSpeed;
const ExposurePair(this.aperture, this.shutterSpeed); const ExposurePair(this.aperture, this.shutterSpeed);
@override
String toString() => '${aperture.toString()} - ${shutterSpeed.toString()}';
} }

View file

@ -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;
}

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/screens/metering/ev_source/camera/bloc_camera.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/generated/l10n.dart';
import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.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/widget_dialog_picker.dart';
import 'components/container_reading_value.dart'; import 'components/container_reading_value.dart';
class MeteringTopBar extends StatelessWidget { class MeteringTopBar extends StatefulWidget {
final ExposurePair? fastest; final ExposurePair? fastest;
final ExposurePair? slowest; final ExposurePair? slowest;
final double ev; final double ev;
@ -22,6 +23,8 @@ class MeteringTopBar extends StatelessWidget {
final ValueChanged<IsoValue> onIsoChanged; final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged; final ValueChanged<NdValue> onNdChanged;
final ValueChanged<double> onCutoutLayout;
const MeteringTopBar({ const MeteringTopBar({
required this.fastest, required this.fastest,
required this.slowest, required this.slowest,
@ -30,29 +33,40 @@ class MeteringTopBar extends StatelessWidget {
required this.nd, required this.nd,
required this.onIsoChanged, required this.onIsoChanged,
required this.onNdChanged, required this.onNdChanged,
required this.onCutoutLayout,
super.key, super.key,
}); });
@override
State<MeteringTopBar> createState() => _MeteringTopBarState();
}
class _MeteringTopBarState extends State<MeteringTopBar> {
double stepHeight = 0.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return CustomPaint(
borderRadius: const BorderRadius.only( painter: TopBarShape(
bottomLeft: Radius.circular(Dimens.borderRadiusL),
bottomRight: Radius.circular(Dimens.borderRadiusL),
),
child: ColoredBox(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: Padding( appendixWidth: stepHeight > 0
padding: const EdgeInsets.all(Dimens.paddingM), ? MediaQuery.of(context).size.width / 2 - Dimens.grid8 + Dimens.paddingM
child: SafeArea( : MediaQuery.of(context).size.width / 2 + Dimens.grid8 - Dimens.paddingM,
bottom: false, appendixHeight: stepHeight,
child: MediaQuery( ),
data: MediaQuery.of(context), child: Padding(
child: Row( padding: const EdgeInsets.all(Dimens.paddingM),
crossAxisAlignment: CrossAxisAlignment.start, child: SafeArea(
mainAxisSize: MainAxisSize.min, bottom: false,
children: [ child: MediaQuery(
Expanded( data: MediaQuery.of(context),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ReadingsContainer(
onLayout: (size) => _onReadingsLayout(size.height),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@ -60,15 +74,11 @@ class MeteringTopBar extends StatelessWidget {
values: [ values: [
ReadingValue( ReadingValue(
label: S.of(context).fastestExposurePair, label: S.of(context).fastestExposurePair,
value: fastest != null value: widget.fastest != null ? widget.fastest!.toString() : '-',
? '${fastest!.aperture.toString()} - ${fastest!.shutterSpeed.toString()}'
: '-',
), ),
ReadingValue( ReadingValue(
label: S.of(context).slowestExposurePair, label: S.of(context).slowestExposurePair,
value: fastest != null value: widget.fastest != null ? widget.slowest!.toString() : '-',
? '${slowest!.aperture.toString()} - ${slowest!.shutterSpeed.toString()}'
: '-',
), ),
], ],
), ),
@ -85,30 +95,16 @@ class MeteringTopBar extends StatelessWidget {
Row( Row(
children: [ children: [
Expanded( Expanded(
child: _AnimatedDialogPicker( child: _IsoValueTile(
title: S.of(context).iso, value: widget.iso,
subtitle: S.of(context).filmSpeed, onChanged: widget.onIsoChanged,
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,
), ),
), ),
const _InnerPadding(), const _InnerPadding(),
Expanded( Expanded(
child: _AnimatedDialogPicker( child: _NdValueTile(
title: S.of(context).nd, value: widget.nd,
subtitle: S.of(context).ndFilterFactor, onChanged: widget.onNdChanged,
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,
), ),
), ),
], ],
@ -116,33 +112,76 @@ class MeteringTopBar extends StatelessWidget {
], ],
), ),
), ),
const _InnerPadding(), ),
Expanded( const _InnerPadding(),
child: AnimatedDialog( const Expanded(
openedSize: Size( child: ClipRRect(
MediaQuery.of(context).size.width - Dimens.paddingM * 2, borderRadius: BorderRadius.all(Radius.circular(Dimens.borderRadiusM)),
(MediaQuery.of(context).size.width - Dimens.paddingM * 2) / 3 * 4, child: CameraView(),
),
child: BlocProvider.value(
value: context.read<CameraBloc>(),
child: const 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 { class _InnerPadding extends SizedBox {
const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8); const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8);
} }
class _IsoValueTile extends StatelessWidget {
final IsoValue value;
final ValueChanged<IsoValue> onChanged;
const _IsoValueTile({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
return _AnimatedDialogPicker<IsoValue>(
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<NdValue> onChanged;
const _NdValueTile({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
return _AnimatedDialogPicker<NdValue>(
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<T extends PhotographyValue> extends StatelessWidget { class _AnimatedDialogPicker<T extends PhotographyValue> extends StatelessWidget {
final _key = GlobalKey<AnimatedDialogState>(); final _key = GlobalKey<AnimatedDialogState>();
final String title; final String title;

View file

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ReadingsContainer extends SingleChildRenderObjectWidget {
final ValueChanged<Size>? onLayout;
const ReadingsContainer({
super.key,
super.child,
this.onLayout,
});
@override
RenderReadingsContainer createRenderObject(BuildContext context) => RenderReadingsContainer(onLayout: onLayout);
}
class RenderReadingsContainer extends RenderProxyBox {
final ValueChanged<Size>? 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);
}
}

View file

@ -21,6 +21,8 @@ class MeteringScreen extends StatefulWidget {
} }
class _MeteringScreenState extends State<MeteringScreen> { class _MeteringScreenState extends State<MeteringScreen> {
double topBarOverflow = 0.0;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@ -37,31 +39,59 @@ class _MeteringScreenState extends State<MeteringScreen> {
children: [ children: [
Column( Column(
children: [ children: [
_topBar(state), MeteringTopBar(
fastest: state.fastest,
slowest: state.slowest,
ev: state.ev,
iso: state.iso,
nd: state.nd,
onIsoChanged: (value) => context.read<MeteringBloc>().add(IsoChangedEvent(value)),
onNdChanged: (value) => context.read<MeteringBloc>().add(NdChangedEvent(value)),
onCutoutLayout: (value) => topBarOverflow = value,
),
Expanded( Expanded(
child: Padding( child: LayoutBuilder(
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), builder: (context, constraints) => OverflowBox(
child: Row( alignment: Alignment.bottomCenter,
children: [ maxHeight: constraints.maxHeight + topBarOverflow.abs(),
Expanded(child: ExposurePairsList(state.exposurePairs)), maxWidth: constraints.maxWidth,
const SizedBox(width: Dimens.grid8), child: Padding(
Expanded( padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
child: Padding( child: Row(
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), children: [
child: Column( Expanded(
children: const [ child: Padding(
Expanded(child: CameraExposureSlider()), padding: topBarOverflow >= 0 ? EdgeInsets.only(top: topBarOverflow) : EdgeInsets.zero,
SizedBox(height: Dimens.grid24), child: ExposurePairsList(state.exposurePairs),
CameraZoomSlider(), ),
],
), ),
), 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<MeteringBloc>().add(const MeasureEvent()),
onSettings: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen()));
},
),
], ],
), ),
], ],
@ -70,26 +100,4 @@ class _MeteringScreenState extends State<MeteringScreen> {
), ),
); );
} }
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<MeteringBloc>().add(IsoChangedEvent(value)),
onNdChanged: (value) => context.read<MeteringBloc>().add(NdChangedEvent(value)),
);
}
Widget _bottomBar() {
return MeteringBottomControls(
onSourceChanged: () {},
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
onSettings: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen()));
},
);
}
} }