From 7c20bfe80d2242ccc373096618571143cdd1db82 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Tue, 29 Nov 2022 21:46:23 +0300 Subject: [PATCH] Implemented metering dialogs animation added readings dialog animation (wip) added offset animation added size animation added backgroundColor animation added borderRadius animation added opacity animations fixed closing dialog added mock dialog for ND --- lib/main.dart | 2 +- lib/res/dimens.dart | 2 + .../topbar/components/reading_container.dart | 197 ++++++++++++++++++ .../metering/components/topbar/topbar.dart | 24 ++- 4 files changed, 216 insertions(+), 9 deletions(-) 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(), ), ), ],