mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-22 07:20:39 +00:00
Made TopBarShape
responsive
returned `TopBarShape` fixed `TopBarShape`
This commit is contained in:
parent
5eb0869aa0
commit
5bea96669d
5 changed files with 280 additions and 101 deletions
|
@ -6,4 +6,7 @@ class ExposurePair {
|
|||
final ShutterSpeedValue shutterSpeed;
|
||||
|
||||
const ExposurePair(this.aperture, this.shutterSpeed);
|
||||
|
||||
@override
|
||||
String toString() => '${aperture.toString()} - ${shutterSpeed.toString()}';
|
||||
}
|
||||
|
|
97
lib/screens/metering/components/topbar/shape_topbar.dart
Normal file
97
lib/screens/metering/components/topbar/shape_topbar.dart
Normal 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;
|
||||
}
|
|
@ -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<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
|
||||
final ValueChanged<double> 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<MeteringTopBar> createState() => _MeteringTopBarState();
|
||||
}
|
||||
|
||||
class _MeteringTopBarState extends State<MeteringTopBar> {
|
||||
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<CameraBloc>(),
|
||||
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<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 {
|
||||
final _key = GlobalKey<AnimatedDialogState>();
|
||||
final String title;
|
||||
|
|
32
lib/screens/metering/components/widget_size_render.dart
Normal file
32
lib/screens/metering/components/widget_size_render.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ class MeteringScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _MeteringScreenState extends State<MeteringScreen> {
|
||||
double topBarOverflow = 0.0;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
@ -37,31 +39,59 @@ class _MeteringScreenState extends State<MeteringScreen> {
|
|||
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<MeteringBloc>().add(IsoChangedEvent(value)),
|
||||
onNdChanged: (value) => context.read<MeteringBloc>().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<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()));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue