diff --git a/lib/widgets/hero_viewer.dart b/lib/widgets/hero_viewer.dart index e50a0b8d..4ff53014 100644 --- a/lib/widgets/hero_viewer.dart +++ b/lib/widgets/hero_viewer.dart @@ -144,7 +144,7 @@ class HeroViewerRoute extends PageRoute { required this.transitionBuilder, this.transitionDuration = const Duration(milliseconds: 300), this.maintainState = true, - this.opaque = true, + this.opaque = false, this.barrierColor, this.barrierDismissible = true, this.barrierLabel diff --git a/lib/widgets/map_overlay/loading_indicator.dart b/lib/widgets/map_overlay/loading_indicator.dart deleted file mode 100644 index 1753fd82..00000000 --- a/lib/widgets/map_overlay/loading_indicator.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; - -class LoadingIndicator extends StatelessWidget { - final bool active; - - final Duration duration; - - const LoadingIndicator({ - this.active = false, - this.duration = const Duration(milliseconds: 500), - Key? key - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return AnimatedSwitcher( - switchInCurve: Curves.elasticOut, - switchOutCurve: Curves.elasticOut, - transitionBuilder: _transition, - duration: duration, - child: !active ? null : Material( - type: MaterialType.circle, - elevation: Theme.of(context).floatingActionButtonTheme.elevation ?? 4.0, - color: Theme.of(context).colorScheme.primaryContainer, - shadowColor: Theme.of(context).colorScheme.shadow, - // enforce no box constraints - // see: https://flutter.dev/docs/development/ui/layout/constraints - child: UnconstrainedBox( - child: Container( - height: 40.0, - width: 40.0, - padding: const EdgeInsets.all(10), - child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.primary, - ) - ) - ) - ) - ); - } - - - Widget _transition(Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - } -} diff --git a/lib/widgets/map_overlay/map_overlay.dart b/lib/widgets/map_overlay/map_overlay.dart index 505ba0cb..8ee738ad 100644 --- a/lib/widgets/map_overlay/map_overlay.dart +++ b/lib/widgets/map_overlay/map_overlay.dart @@ -8,8 +8,8 @@ import '/commons/osm_config.dart' as osm_config; import 'location_button.dart'; import 'compass_button.dart'; import 'zoom_button.dart'; -import 'loading_indicator.dart'; import 'credit_text.dart'; +import 'shimmer.dart'; /// Builds the action/control buttons and attribution text which overlay the map. @@ -24,87 +24,96 @@ class MapOverlay extends ViewFragment { @override Widget build(BuildContext context, viewModel) { - return Padding( - padding: MediaQuery.of(context).padding + EdgeInsets.all(buttonSpacing), - child: Stack( - children: [ - Align( - alignment: Alignment.topCenter, - child: LoadingIndicator( - active: viewModel.isLoadingStopAreas, + return Stack( + children: [ + Positioned.fill( + child: Shimmer( + active: viewModel.isLoadingStopAreas, + color: Theme.of(context).colorScheme.primary.withOpacity(0.7), + child: Container( + color: Colors.transparent, + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, ), ), - Align( - alignment: Alignment.topLeft, - child: FloatingActionButton.small( - heroTag: null, - onPressed: Scaffold.of(context).openDrawer, - child: const Icon( - Icons.menu, - ), - ), - ), - Align( - alignment: Alignment.topRight, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: viewModel.mapRotation % 360 != 0 - ? CompassButton( - rotation: viewModel.mapRotation, - isDegree: true, - onPressed: viewModel.resetRotation, - ) - : null - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: CreditText( - alignment: TextAlign.left, - padding: const EdgeInsets.symmetric( - horizontal: 10, - ), - children: [ - CreditTextPart( - AppLocalizations.of(context)!.osmCreditsText, - url: osm_config.osmCreditsURL, - ), - CreditTextPart( - kTileLayerPublicTransport.creditsText, - url: kTileLayerPublicTransport.creditsUrl, - ), - ], + ), + Padding( + padding: MediaQuery.of(context).padding + EdgeInsets.all(buttonSpacing), + child: Stack( + children: [ + Align( + alignment: Alignment.topLeft, + child: FloatingActionButton.small( + heroTag: null, + onPressed: Scaffold.of(context).openDrawer, + child: const Icon( + Icons.menu, ), ), - Column( - mainAxisAlignment: MainAxisAlignment.end, + ), + Align( + alignment: Alignment.topRight, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: viewModel.mapRotation % 360 != 0 + ? CompassButton( + rotation: viewModel.mapRotation, + isDegree: true, + onPressed: viewModel.resetRotation, + ) + : null + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, children: [ - LocationButton( - activeColor: Theme.of(context).colorScheme.primary, - activeIconColor: Theme.of(context).colorScheme.onPrimary, - color: Theme.of(context).colorScheme.primaryContainer, - iconColor: Theme.of(context).colorScheme.onPrimaryContainer, - active: viewModel.cameraIsFollowingLocation, - onPressed: viewModel.toggleLocationFollowing, - ), - SizedBox ( - height: buttonSpacing, + Expanded( + child: CreditText( + alignment: TextAlign.left, + padding: const EdgeInsets.symmetric( + horizontal: 10, + ), + children: [ + CreditTextPart( + AppLocalizations.of(context)!.osmCreditsText, + url: osm_config.osmCreditsURL, + ), + CreditTextPart( + kTileLayerPublicTransport.creditsText, + url: kTileLayerPublicTransport.creditsUrl, + ), + ], + ), ), - ZoomButton( - onZoomInPressed: viewModel.zoomIn, - onZoomOutPressed: viewModel.zoomOut, + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + LocationButton( + activeColor: Theme.of(context).colorScheme.primary, + activeIconColor: Theme.of(context).colorScheme.onPrimary, + color: Theme.of(context).colorScheme.primaryContainer, + iconColor: Theme.of(context).colorScheme.onPrimaryContainer, + active: viewModel.cameraIsFollowingLocation, + onPressed: viewModel.toggleLocationFollowing, + ), + SizedBox ( + height: buttonSpacing, + ), + ZoomButton( + onZoomInPressed: viewModel.zoomIn, + onZoomOutPressed: viewModel.zoomOut, + ), + ], ), ], ), - ], - ), + ), + ], ), - ], - ), - ); + ), + ] + ); } } diff --git a/lib/widgets/map_overlay/shimmer.dart b/lib/widgets/map_overlay/shimmer.dart new file mode 100644 index 00000000..bc9c2f7e --- /dev/null +++ b/lib/widgets/map_overlay/shimmer.dart @@ -0,0 +1,111 @@ + +import 'package:flutter/material.dart'; + +class Shimmer extends StatefulWidget { + final bool active; + final Color color; + final Widget? child; + final Duration animationDuration; + final Duration animationDelay; + final double min; + final double max; + final BlendMode blendMode; + + const Shimmer({ + this.child, + this.active = true, + this.color = const Color(0xFFF4F4F4), + this.animationDuration = const Duration(milliseconds: 1500), + this.animationDelay = const Duration(milliseconds: 50), + this.min = -1.5, + this.max = 1.5, + this.blendMode = BlendMode.color, + super.key, + }); + + @override + State createState() => _ShimmerState(); +} + +class _ShimmerState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation ratio; + late final Duration totalDuration; + + @override + void initState() { + super.initState(); + totalDuration = widget.animationDuration + widget.animationDelay; + final begin = widget.min - ((widget.animationDelay.inMilliseconds * (widget.max - widget.min))/ totalDuration.inMilliseconds); + _controller = AnimationController( + vsync: this, + duration: totalDuration, + ); + ratio = _controller.drive(Tween(begin: begin, end: widget.max)); + } + + @override + void didUpdateWidget(covariant Shimmer oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.active != widget.active) { + if (oldWidget.active == true) { + _controller.forward(from: _controller.value).whenComplete(() + { + _controller.stop(); + setState(() {}); + } + ); + } + else { _controller.repeat();} + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + LinearGradient get gradient => LinearGradient( + colors: [ widget.color.withOpacity(0.0), widget.color, widget.color.withOpacity(0.0),], + stops: const [ 0.1, 0.3, 0.4,], + begin: const Alignment(-1.0, -0.3), + end: const Alignment(1.0, 0.3), + transform:_SlidingGradientTransform(ratio: ratio.value), + ); + + @override + Widget build(BuildContext context) { + if (_controller.isAnimating) { + return IgnorePointer( + ignoring: true, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return ShaderMask( + blendMode: widget.blendMode, + shaderCallback: gradient.createShader, + child: widget.child, + ); + }, + ), + ); + } + else { + return const SizedBox(); + } + } +} + +class _SlidingGradientTransform extends GradientTransform { + const _SlidingGradientTransform({ + required this.ratio, + }); + + final double ratio; + + @override + Matrix4? transform(Rect bounds, {TextDirection? textDirection}) { + return Matrix4.translationValues(bounds.width * ratio, 0.0, 0.0); + } +}