diff --git a/lib/widgets/common/identity/aves_app_bar.dart b/lib/widgets/common/identity/aves_app_bar.dart index 38c19167d..63f40f20f 100644 --- a/lib/widgets/common/identity/aves_app_bar.dart +++ b/lib/widgets/common/identity/aves_app_bar.dart @@ -142,28 +142,28 @@ class AvesAppBar extends StatelessWidget { static Widget _flightShuttleBuilder( BuildContext flightContext, Animation animation, - HeroFlightDirection direction, - BuildContext fromHero, - BuildContext toHero, + HeroFlightDirection flightDirection, + BuildContext fromHeroContext, + BuildContext toHeroContext, ) { - final pushing = direction == HeroFlightDirection.push; + final pushing = flightDirection == HeroFlightDirection.push; Widget popBuilder(context, child) => Opacity(opacity: 1 - animation.value, child: child); Widget pushBuilder(context, child) => Opacity(opacity: animation.value, child: child); return Material( type: MaterialType.transparency, child: DefaultTextStyle( - style: DefaultTextStyle.of(toHero).style, + style: DefaultTextStyle.of(toHeroContext).style, child: Stack( children: [ AnimatedBuilder( animation: animation, builder: pushing ? popBuilder : pushBuilder, - child: fromHero.widget, + child: fromHeroContext.widget, ), AnimatedBuilder( animation: animation, builder: pushing ? pushBuilder : popBuilder, - child: toHero.widget, + child: toHeroContext.widget, ), ], ), diff --git a/lib/widgets/common/map/attribution.dart b/lib/widgets/common/map/attribution.dart index 067623085..bfe8c7aee 100644 --- a/lib/widgets/common/map/attribution.dart +++ b/lib/widgets/common/map/attribution.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/text.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -5,6 +6,7 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:provider/provider.dart'; class Attribution extends StatelessWidget { final EntryMapStyle? style; @@ -33,17 +35,40 @@ class Attribution extends StatelessWidget { Widget _buildOsmAttributionMarkdown(BuildContext context, String data) { final theme = Theme.of(context); + Widget child = MarkdownBody( + data: '${context.l10n.mapAttributionOsmData}${AText.separator}$data', + selectable: true, + styleSheet: MarkdownStyleSheet( + a: TextStyle(color: theme.colorScheme.primary), + p: theme.textTheme.bodySmall!.merge(const TextStyle(fontSize: InfoRowGroup.fontSize)), + ), + onTapLink: (text, href, title) => AvesApp.launchUrl(href), + ); + + final animate = context.select((v) => v.animate); + if (animate) { + child = Hero( + tag: 'map-attribution', + flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) { + return DefaultTextStyle( + style: DefaultTextStyle.of(toHeroContext).style, + child: MediaQuery.removeViewPadding( + context: context, + removeLeft: true, + removeTop: true, + removeRight: true, + removeBottom: true, + child: toHeroContext.widget, + ), + ); + }, + child: child, + ); + } + return Padding( padding: const EdgeInsets.only(top: 4), - child: MarkdownBody( - data: '${context.l10n.mapAttributionOsmData}${AText.separator}$data', - selectable: true, - styleSheet: MarkdownStyleSheet( - a: TextStyle(color: theme.colorScheme.primary), - p: theme.textTheme.bodySmall!.merge(const TextStyle(fontSize: InfoRowGroup.fontSize)), - ), - onTapLink: (text, href, title) => AvesApp.launchUrl(href), - ), + child: child, ); } } diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart index 52ded17e4..c05419415 100644 --- a/lib/widgets/common/map/buttons/panel.dart +++ b/lib/widgets/common/map/buttons/panel.dart @@ -9,6 +9,7 @@ import 'package:aves/widgets/common/map/buttons/button.dart'; import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart'; import 'package:aves/widgets/common/map/compass.dart'; import 'package:aves/widgets/common/map/map_action_delegate.dart'; +import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves_map/aves_map.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; @@ -52,46 +53,12 @@ class _MapButtonPanelState extends State { @override Widget build(BuildContext context) { - final iconTheme = IconTheme.of(context); - final iconSize = Size.square(iconTheme.size!); - - Widget? navigationButton; - switch (context.select((v) => v.navigationButton)) { - case MapNavigationButton.back: - if (!settings.useTvLayout) { - navigationButton = MapOverlayButton.icon( - icon: const BackButtonIcon(), - onPressed: () => Navigator.maybeOf(context)?.pop(), - tooltip: MaterialLocalizations.of(context).backButtonTooltip, - ); - } - case MapNavigationButton.close: - navigationButton = MapOverlayButton.icon( - icon: const CloseButtonIcon(), - onPressed: SystemNavigator.pop, - tooltip: MaterialLocalizations.of(context).closeButtonTooltip, - ); - case MapNavigationButton.map: - final _openMapPage = widget.openMapPage; - if (_openMapPage != null) { - navigationButton = MapOverlayButton.icon( - icon: const Icon(AIcons.showFullscreenCorners), - onPressed: () => _openMapPage.call(context), - tooltip: context.l10n.openMapPageTooltip, - ); - } - case MapNavigationButton.none: - break; - } - final showCoordinateFilter = context.select((v) => v.showCoordinateFilter); final visualDensity = context.select((v) => v.visualDensity); final double padding = 8 + visualDensity.horizontal * 2; - final actions = [ - MapAction.openMapApp, - MapAction.addShortcut, - ].where((action) => _actionDelegate.isVisible(context, action)).toList(); + Widget? topLeftButton = _buildNavigationButton(context); + Widget? topRightButton = _buildTopRightButton(context); return Positioned.fill( child: TooltipTheme( @@ -113,38 +80,11 @@ class _MapButtonPanelState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (navigationButton != null) ...[ - navigationButton, + if (topLeftButton != null) ...[ + topLeftButton, SizedBox(height: padding), ], - ValueListenableBuilder( - valueListenable: widget.boundsNotifier, - builder: (context, bounds, child) { - final degrees = bounds.rotation; - final opacity = degrees == 0 ? .0 : 1.0; - return IgnorePointer( - ignoring: opacity == 0, - child: AnimatedOpacity( - opacity: opacity, - duration: context.select((v) => v.viewerOverlayAnimation), - child: MapOverlayButton.icon( - icon: Transform( - origin: iconSize.center(Offset.zero), - transform: Matrix4.rotationZ(degToRadian(degrees)), - child: CustomPaint( - painter: CompassPainter( - color: iconTheme.color!, - ), - size: iconSize, - ), - ), - onPressed: widget.controller.resetRotation, - tooltip: context.l10n.mapPointNorthUpTooltip, - ), - ), - ); - }, - ), + _buildCompass(context), ], ), ), @@ -161,30 +101,10 @@ class _MapButtonPanelState extends State { // key is expected by test driver child: Column( children: [ - if (actions.length == 1) _buildActionButton(context, actions.first), - if (actions.length > 1) - MapOverlayButton(builder: (context, visualDensity, child) { - final animations = context.read().accessibilityAnimations; - return PopupMenuButton( - itemBuilder: (context) => actions - .map((action) => PopupMenuItem( - value: action, - child: MenuRow( - text: action.getText(context), - icon: action.getIcon(), - ), - )) - .toList(), - onSelected: (action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(animations.popUpAnimationDelay * timeDilation); - _actionDelegate.onActionSelected(context, action); - }, - iconSize: MapOverlayButton.iconSize(visualDensity), - popUpAnimationStyle: animations.popUpAnimationStyle, - ); - }), - SizedBox(height: padding), + if (topRightButton != null) ...[ + topRightButton, + SizedBox(height: padding), + ], // key is expected by test driver _buildActionButton(context, MapAction.selectStyle, buttonKey: const Key('map-menu-layers')), ], @@ -212,10 +132,133 @@ class _MapButtonPanelState extends State { ); } - Widget _buildActionButton(BuildContext context, MapAction action, {Key? buttonKey}) => MapOverlayButton.icon( - buttonKey: buttonKey, - icon: action.getIcon(), - onPressed: () => _actionDelegate.onActionSelected(context, action), - tooltip: action.getText(context), - ); + Widget? _buildNavigationButton(BuildContext context) { + Widget? child; + switch (context.select((v) => v.navigationButton)) { + case MapNavigationButton.back: + if (!settings.useTvLayout) { + child = MapOverlayButton.icon( + icon: const BackButtonIcon(), + onPressed: () => Navigator.maybeOf(context)?.pop(), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + ); + } + case MapNavigationButton.close: + child = MapOverlayButton.icon( + icon: const CloseButtonIcon(), + onPressed: SystemNavigator.pop, + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, + ); + case MapNavigationButton.map: + final _openMapPage = widget.openMapPage; + if (_openMapPage != null) { + child = MapOverlayButton.icon( + icon: const Icon(AIcons.showFullscreenCorners), + onPressed: () => _openMapPage.call(context), + tooltip: context.l10n.openMapPageTooltip, + ); + } + case MapNavigationButton.none: + break; + } + if (child != null) { + child = _heroify(context, 'top-left', child); + } + return child; + } + + Widget? _buildTopRightButton(BuildContext context) { + const heroTag = 'top-right'; + + final actions = [ + MapAction.openMapApp, + MapAction.addShortcut, + ].where((action) => _actionDelegate.isVisible(context, action)).toList(); + + Widget? child; + if (actions.length == 1) { + child = _buildActionButton(context, actions.first, heroTag: heroTag); + } else if (actions.length > 1) { + child = MapOverlayButton(builder: (context, visualDensity, child) { + final animations = context.read().accessibilityAnimations; + return PopupMenuButton( + itemBuilder: (context) => actions + .map((action) => PopupMenuItem( + value: action, + child: MenuRow( + text: action.getText(context), + icon: action.getIcon(), + ), + )) + .toList(), + onSelected: (action) async { + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(animations.popUpAnimationDelay * timeDilation); + _actionDelegate.onActionSelected(context, action); + }, + iconSize: MapOverlayButton.iconSize(visualDensity), + popUpAnimationStyle: animations.popUpAnimationStyle, + ); + }); + child = _heroify(context, heroTag, child); + } + return child; + } + + Widget _buildCompass(BuildContext context) { + final iconTheme = IconTheme.of(context); + final iconSize = Size.square(iconTheme.size!); + return ValueListenableBuilder( + valueListenable: widget.boundsNotifier, + builder: (context, bounds, child) { + final degrees = bounds.rotation; + final opacity = degrees == 0 ? .0 : 1.0; + return IgnorePointer( + ignoring: opacity == 0, + child: AnimatedOpacity( + opacity: opacity, + duration: context.select((v) => v.viewerOverlayAnimation), + child: MapOverlayButton.icon( + icon: Transform( + origin: iconSize.center(Offset.zero), + transform: Matrix4.rotationZ(degToRadian(degrees)), + child: CustomPaint( + painter: CompassPainter( + color: iconTheme.color!, + ), + size: iconSize, + ), + ), + onPressed: widget.controller.resetRotation, + tooltip: context.l10n.mapPointNorthUpTooltip, + ), + ), + ); + }, + ); + } + + Widget _buildActionButton(BuildContext context, MapAction action, {Key? buttonKey, String? heroTag}) { + final child = MapOverlayButton.icon( + buttonKey: buttonKey, + icon: action.getIcon(), + onPressed: () => _actionDelegate.onActionSelected(context, action), + tooltip: action.getText(context), + ); + return _heroify(context, heroTag ?? action.name, child); + } + + Widget _heroify(BuildContext context, String? tag, Widget child) { + if (tag != null) { + final animate = context.select((v) => v.animate); + if (animate) { + return Hero( + tag: 'map-button-$tag', + flightShuttleBuilder: MapTheme.heroFlightShuttleBuilder, + child: child, + ); + } + } + return child; + } } diff --git a/lib/widgets/common/map/decorator.dart b/lib/widgets/common/map/decorator.dart index 7969420ae..3de7a72d6 100644 --- a/lib/widgets/common/map/decorator.dart +++ b/lib/widgets/common/map/decorator.dart @@ -1,10 +1,12 @@ +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class MapDecorator extends StatelessWidget { - final Widget? child; + final Widget child; static const mapBorderRadius = BorderRadius.all(Radius.circular(24)); // to match button circles static const mapBackground = Color(0xFFDBD5D3); @@ -12,11 +14,45 @@ class MapDecorator extends StatelessWidget { const MapDecorator({ super.key, - this.child, + required this.child, }); @override Widget build(BuildContext context) { + Widget _child = ClipRRect( + borderRadius: mapBorderRadius, + child: Container( + color: mapBackground, + foregroundDecoration: BoxDecoration( + border: AvesBorder.border(context), + borderRadius: mapBorderRadius, + ), + child: Stack( + children: [ + const GridPaper( + color: mapLoadingGrid, + interval: 10, + divisions: 1, + subdivisions: 1, + child: CustomPaint( + size: Size.infinite, + ), + ), + child, + ], + ), + ), + ); + + final animate = context.select((v) => v.animate); + if (animate) { + _child = Hero( + tag: 'map-canvas', + flightShuttleBuilder: MapTheme.heroFlightShuttleBuilder, + child: _child, + ); + } + final interactive = context.select((v) => v.interactive); return GestureDetector( onScaleStart: interactive @@ -25,30 +61,7 @@ class MapDecorator extends StatelessWidget { // absorb scale gesture here to prevent scrolling // and triggering by mistake a move to the image page above }, - child: ClipRRect( - borderRadius: mapBorderRadius, - child: Container( - color: mapBackground, - foregroundDecoration: BoxDecoration( - border: AvesBorder.border(context), - borderRadius: mapBorderRadius, - ), - child: Stack( - children: [ - const GridPaper( - color: mapLoadingGrid, - interval: 10, - divisions: 1, - subdivisions: 1, - child: CustomPaint( - size: Size.infinite, - ), - ), - if (child != null) child!, - ], - ), - ), - ), + child: _child, ); } } diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index 7e61d2963..4a628359c 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -20,7 +20,6 @@ import 'package:aves/widgets/common/map/attribution.dart'; import 'package:aves/widgets/common/map/buttons/panel.dart'; import 'package:aves/widgets/common/map/decorator.dart'; import 'package:aves/widgets/common/map/leaflet/map.dart'; -import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/dialogs/selection_dialogs/common.dart'; import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart'; @@ -242,50 +241,6 @@ class _GeoMapState extends State { child = _decorateMap(context, overlay); } - child = Hero( - tag: 'map', - flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) { - final pushing = flightDirection == HeroFlightDirection.push; - final fromMediaQuery = MediaQuery.of(fromHeroContext); - final toMediaQuery = MediaQuery.of(toHeroContext); - final fromRenderBox = fromHeroContext.findRenderObject()! as RenderBox; - final toRenderBox = toHeroContext.findRenderObject()! as RenderBox; - final fromTheme = fromHeroContext.read(); - final toTheme = toHeroContext.read(); - - return DefaultTextStyle( - style: DefaultTextStyle.of(toHeroContext).style, - child: AnimatedBuilder( - animation: animation, - builder: (context, child) { - final t = pushing ? animation.value : 1 - animation.value; - return MapTheme( - interactive: false, - showCoordinateFilter: false, - navigationButton: toTheme.navigationButton, - visualDensity: VisualDensity.lerp(fromTheme.visualDensity, toTheme.visualDensity, t), - child: MediaQuery( - data: toMediaQuery.copyWith( - padding: EdgeInsets.lerp(fromMediaQuery.padding, toMediaQuery.padding, t), - viewPadding: EdgeInsets.lerp(fromMediaQuery.viewPadding, toMediaQuery.viewPadding, t), - ), - child: Align( - alignment: Alignment.topCenter, - child: SizedBox.fromSize( - size: Size.lerp(fromRenderBox.size, toRenderBox.size, t), - child: child, - ), - ), - ), - ); - }, - child: toHeroContext.widget, - ), - ); - }, - child: child, - ); - final mapHeight = context.select((v) => v.mapHeight); child = Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -319,7 +274,9 @@ class _GeoMapState extends State { } Widget replacement = Stack( children: [ - const MapDecorator(), + const MapDecorator( + child: SizedBox(), + ), _buildButtonPanel(context), ], ); @@ -550,7 +507,7 @@ class _GeoMapState extends State { ); } - Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child); + Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child!); Widget _buildButtonPanel(BuildContext context) { if (settings.useTvLayout) return const SizedBox(); diff --git a/lib/widgets/common/providers/map_theme_provider.dart b/lib/widgets/common/providers/map_theme_provider.dart index 065e423f7..919837d66 100644 --- a/lib/widgets/common/providers/map_theme_provider.dart +++ b/lib/widgets/common/providers/map_theme_provider.dart @@ -41,4 +41,50 @@ class MapTheme extends StatelessWidget { child: child, ); } + + static Widget heroFlightShuttleBuilder( + BuildContext flightContext, + Animation animation, + HeroFlightDirection flightDirection, + BuildContext fromHeroContext, + BuildContext toHeroContext, + ) { + final pushing = flightDirection == HeroFlightDirection.push; + final fromMediaQuery = MediaQuery.of(fromHeroContext); + final toMediaQuery = MediaQuery.of(toHeroContext); + final fromRenderBox = fromHeroContext.findRenderObject()! as RenderBox; + final toRenderBox = toHeroContext.findRenderObject()! as RenderBox; + final fromTheme = fromHeroContext.read(); + final toTheme = toHeroContext.read(); + + return DefaultTextStyle( + style: DefaultTextStyle.of(toHeroContext).style, + child: AnimatedBuilder( + animation: animation, + builder: (context, child) { + final t = pushing ? animation.value : 1 - animation.value; + return MapTheme( + interactive: false, + showCoordinateFilter: false, + navigationButton: toTheme.navigationButton, + visualDensity: VisualDensity.lerp(fromTheme.visualDensity, toTheme.visualDensity, t), + child: MediaQuery( + data: toMediaQuery.copyWith( + padding: EdgeInsets.lerp(fromMediaQuery.padding, toMediaQuery.padding, t), + viewPadding: EdgeInsets.lerp(fromMediaQuery.viewPadding, toMediaQuery.viewPadding, t), + ), + child: Align( + alignment: Alignment.topCenter, + child: SizedBox.fromSize( + size: Size.lerp(fromRenderBox.size, toRenderBox.size, t), + child: child, + ), + ), + ), + ); + }, + child: toHeroContext.widget, + ), + ); + } } diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index 87154a755..e152a2c1d 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -269,7 +269,7 @@ class _ThumbnailImageState extends State { final backgroundColor = background.isColor ? background.color : null; image = Hero( tag: heroTag, - flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { + flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) { Widget child = TransitionImage( image: entry.bestCachedThumbnail, animation: animation, @@ -304,11 +304,11 @@ class _ThumbnailImageState extends State { if (animate && heroTag != null) { child = Hero( tag: heroTag, - flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { + flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) { return MediaQueryDataProvider( child: DefaultTextStyle( - style: DefaultTextStyle.of(toHero).style, - child: toHero.widget, + style: DefaultTextStyle.of(toHeroContext).style, + child: toHeroContext.widget, ), ); }, diff --git a/lib/widgets/navigation/nav_bar/nav_bar.dart b/lib/widgets/navigation/nav_bar/nav_bar.dart index 0b7891c72..7e42b2fbe 100644 --- a/lib/widgets/navigation/nav_bar/nav_bar.dart +++ b/lib/widgets/navigation/nav_bar/nav_bar.dart @@ -104,11 +104,11 @@ class _AppBottomNavBarState extends State { if (animate) { child = Hero( tag: 'nav-bar', - flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { + flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) { return MediaQuery.removeViewInsets( context: context, removeBottom: true, - child: toHero.widget, + child: toHeroContext.widget, ); }, child: child,