diff --git a/lib/main.dart b/lib/main.dart index 3a2ad6e..589c262 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -79,7 +79,7 @@ class _ApplicationState extends State with TickerProviderStateMixin GlobalCupertinoLocalizations.delegate, ], supportedLocales: S.delegate.supportedLocales, - home: const PermissionsCheckFlow(), + home: MeteringScreen(animationController: _animationController), routes: { "metering": (context) => MeteringScreen(animationController: _animationController), "settings": (context) => const SettingsScreen(), diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index 34adfc3..3607a86 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -3,6 +3,7 @@ class Dimens { static const double borderRadiusM = 16; static const double borderRadiusL = 24; + static const double borderRadiusXL = 32; static const double grid4 = 4; static const double grid8 = 8; @@ -16,5 +17,6 @@ class Dimens { static const Duration durationS = Duration(milliseconds: 100); static const Duration durationSM = Duration(milliseconds: 150); static const Duration durationM = Duration(milliseconds: 200); + static const Duration durationML = Duration(milliseconds: 250); static const Duration durationL = Duration(milliseconds: 300); } diff --git a/lib/screens/metering/components/topbar/components/reading_container.dart b/lib/screens/metering/components/topbar/components/reading_container.dart index 695b9d2..1701be0 100644 --- a/lib/screens/metering/components/topbar/components/reading_container.dart +++ b/lib/screens/metering/components/topbar/components/reading_container.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:lightmeter/res/dimens.dart'; class ReadingValue { @@ -11,6 +12,202 @@ class ReadingValue { }); } +class ReadingContainerWithDialog extends StatefulWidget { + final ReadingValue value; + final Widget Function(BuildContext context) dialogBuilder; + + const ReadingContainerWithDialog({ + required this.value, + required this.dialogBuilder, + super.key, + }); + + @override + State createState() => _ReadingContainerWithDialogState(); +} + +class _ReadingContainerWithDialogState extends State with SingleTickerProviderStateMixin { + final GlobalKey _key = GlobalKey(); + final LayerLink _layerLink = LayerLink(); + OverlayEntry? _overlayEntry; + + late final _animationController = AnimationController( + duration: Dimens.durationL, + reverseDuration: Dimens.durationML, + vsync: this, + ); + late final _defaultCurve = CurvedAnimation(parent: _animationController, curve: Curves.linear); + + late final _colorAnimation = ColorTween( + begin: Colors.transparent, + end: Colors.black54, + ).animate(_defaultCurve); + late final _borderRadiusAnimation = Tween( + begin: Dimens.borderRadiusM, + end: Dimens.borderRadiusXL, + ).animate(_defaultCurve); + late final _itemOpacityAnimation = Tween( + begin: 1, + end: 0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0, 0.5, curve: Curves.linear), + )); + late final _dialogOpacityAnimation = Tween( + begin: 0, + end: 1, + ).animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.5, 1.0, curve: Curves.linear), + )); + + late final SizeTween _sizeTween; + late final Animation _sizeAnimation; + late final Animation _offsetAnimation; + + @override + void initState() { + super.initState(); + //timeDilation = 5.0; + WidgetsBinding.instance.addPostFrameCallback((_) { + final mediaQuery = MediaQuery.of(context); + final itemWidth = _key.currentContext!.size!.width; + final itemHeight = _key.currentContext!.size!.height; + + _sizeTween = SizeTween( + begin: Size( + itemWidth, + itemHeight, + ), + end: Size( + mediaQuery.size.width - mediaQuery.padding.horizontal - Dimens.paddingL * 2, + mediaQuery.size.height - mediaQuery.padding.vertical - Dimens.paddingL * 2, + ), + ); + _sizeAnimation = _sizeTween.animate(_defaultCurve); + + final renderBox = _key.currentContext!.findRenderObject() as RenderBox; + final offset = renderBox.localToGlobal(Offset.zero); + _offsetAnimation = SizeTween( + begin: Size( + offset.dx + itemWidth / 2, + offset.dy + itemHeight / 2, + ), + end: Size( + mediaQuery.size.width / 2, + mediaQuery.size.height / 2 + mediaQuery.padding.top / 2 - mediaQuery.padding.bottom / 2, + ), + ).animate(_defaultCurve); + }); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return InkWell( + key: _key, + onTap: _openDialog, + child: CompositedTransformTarget( + link: _layerLink, + child: ClipRRect( + borderRadius: BorderRadius.circular(Dimens.borderRadiusM), + child: ColoredBox( + color: Theme.of(context).colorScheme.primaryContainer, + child: Padding( + padding: const EdgeInsets.all(Dimens.paddingM), + child: _ReadingValueBuilder(widget.value), + ), + ), + ), + ), + ); + } + + void _openDialog() { + final RenderBox renderBox = _key.currentContext!.findRenderObject() as RenderBox; + final offset = renderBox.localToGlobal(Offset.zero); + _overlayEntry = OverlayEntry( + builder: (context) => CompositedTransformFollower( + offset: Offset(-offset.dx, -offset.dy), + link: _layerLink, + showWhenUnlinked: false, + child: SizedBox.fromSize( + size: MediaQuery.of(context).size, + child: AnimatedBuilder( + animation: _animationController, + builder: (context, _) => Stack( + children: [ + Positioned.fill( + child: GestureDetector( + onTap: _closeDialog, + child: ColoredBox(color: _colorAnimation.value!), + ), + ), + Positioned.fromRect( + rect: Rect.fromCenter( + center: Offset( + _offsetAnimation.value!.width, + _offsetAnimation.value!.height, + ), + width: _sizeAnimation.value!.width, + height: _sizeAnimation.value!.height, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(_borderRadiusAnimation.value), + child: ColoredBox( + color: Theme.of(context).colorScheme.primaryContainer, + child: Center( + child: Stack( + children: [ + Opacity( + opacity: _itemOpacityAnimation.value, + child: Transform.scale( + scale: _sizeAnimation.value!.width / _sizeTween.begin!.width, + child: SizedBox( + width: _sizeTween.begin!.width, + child: Padding( + padding: const EdgeInsets.all(Dimens.paddingM), + child: _ReadingValueBuilder(widget.value), + ), + ), + ), + ), + Opacity( + opacity: _dialogOpacityAnimation.value, + child: Transform.scale( + scale: _sizeAnimation.value!.width / _sizeTween.end!.width, + child: widget.dialogBuilder(context), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + Overlay.of(context)?.insert(_overlayEntry!); + _animationController.forward(); + } + + void _closeDialog() { + _animationController.reverse(); + Future.delayed(_animationController.reverseDuration! * timeDilation).then((_) { + _overlayEntry?.remove(); + }); + } +} + class ReadingContainer extends StatelessWidget { final List<_ReadingValueBuilder> _items; diff --git a/lib/screens/metering/components/topbar/topbar.dart b/lib/screens/metering/components/topbar/topbar.dart index 54c037e..37d0152 100644 --- a/lib/screens/metering/components/topbar/topbar.dart +++ b/lib/screens/metering/components/topbar/topbar.dart @@ -79,10 +79,14 @@ class MeteringTopBar extends StatelessWidget { const _InnerPadding(), SizedBox( width: columnWidth, - child: ReadingContainer.singleValue( - value: ReadingValue( - label: 'ISO', - value: iso.toString(), + child: MediaQuery( + data: MediaQuery.of(context), + child: ReadingContainerWithDialog( + value: ReadingValue( + label: 'ISO', + value: iso.toString(), + ), + dialogBuilder: (context) => SizedBox(), ), ), ), @@ -105,10 +109,14 @@ class MeteringTopBar extends StatelessWidget { ), ), const _InnerPadding(), - ReadingContainer.singleValue( - value: ReadingValue( - label: 'ND', - value: nd.toString(), + MediaQuery( + data: MediaQuery.of(context), + child: ReadingContainerWithDialog( + value: ReadingValue( + label: 'ND', + value: nd.toString(), + ), + dialogBuilder: (context) => SizedBox(), ), ), ],