diff --git a/example/lib/main.dart b/example/lib/main.dart index 7addcbea4..0584f187a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -19,6 +19,7 @@ import './pages/plugin_zoombuttons.dart'; import './pages/polyline.dart'; import './pages/sliding_map.dart'; import './pages/tap_to_add.dart'; +import './pages/tile_builder_example.dart'; import './pages/tile_loading_error_handle.dart'; import './pages/widgets.dart'; import './pages/wms_tile_layer.dart'; @@ -58,6 +59,7 @@ class MyApp extends StatelessWidget { CustomCrsPage.route: (context) => CustomCrsPage(), LiveLocationPage.route: (context) => LiveLocationPage(), TileLoadingErrorHandle.route: (context) => TileLoadingErrorHandle(), + TileBuilderPage.route: (context) => TileBuilderPage(), }, ); } diff --git a/example/lib/pages/tile_builder_example.dart b/example/lib/pages/tile_builder_example.dart new file mode 100644 index 000000000..23496d547 --- /dev/null +++ b/example/lib/pages/tile_builder_example.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong/latlong.dart'; + +import '../widgets/drawer.dart'; + +class TileBuilderPage extends StatefulWidget { + static const String route = '/tile_builder_example'; + + @override + _TileBuilderPageState createState() => _TileBuilderPageState(); +} + +class _TileBuilderPageState extends State { + var darkMode = false; + var loadingTime = false; + var showCoords = false; + var grid = false; + + // mix of [coordinateDebugTileBuilder] and [loadingTimeDebugTileBuilder] from tile_builder.dart + Widget tileBuilder(BuildContext context, Widget tileWidget, Tile tile) { + final coords = tile.coords; + + return Container( + decoration: BoxDecoration( + border: grid ? Border.all() : null, + ), + child: Stack( + fit: StackFit.passthrough, + children: [ + tileWidget, + if (loadingTime || showCoords) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (showCoords) + Text( + '${coords.x.floor()} : ${coords.y.floor()} : ${coords.z.floor()}', + style: Theme.of(context).textTheme.headline5, + ), + if (loadingTime) + Text( + tile.loaded == null + ? 'Loading' + // sometimes result is negative which shouldn't happen, abs() corrects it + : '${(tile.loaded.millisecond - tile.loadStarted.millisecond).abs()} ms', + style: Theme.of(context).textTheme.headline5, + ), + ], + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Tile builder')), + drawer: buildDrawer(context, TileBuilderPage.route), + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + FloatingActionButton.extended( + heroTag: 'grid', + label: Text( + grid ? 'Hide grid' : 'Show grid', + textAlign: TextAlign.center, + ), + icon: Icon(grid ? Icons.grid_off : Icons.grid_on), + onPressed: () => setState(() => grid = !grid), + ), + SizedBox(height: 8), + FloatingActionButton.extended( + heroTag: 'coords', + label: Text( + showCoords ? 'Hide coords' : 'Show coords', + textAlign: TextAlign.center, + ), + icon: Icon(showCoords ? Icons.unarchive : Icons.bug_report), + onPressed: () => setState(() => showCoords = !showCoords), + ), + SizedBox(height: 8), + FloatingActionButton.extended( + heroTag: 'ms', + label: Text( + loadingTime ? 'Hide loading time' : 'Show loading time', + textAlign: TextAlign.center, + ), + icon: Icon(loadingTime ? Icons.timer_off : Icons.timer), + onPressed: () => setState(() => loadingTime = !loadingTime), + ), + SizedBox(height: 8), + FloatingActionButton.extended( + heroTag: 'dark-light', + label: Text( + darkMode ? 'Light mode' : 'Dark mode', + textAlign: TextAlign.center, + ), + icon: Icon(darkMode ? Icons.brightness_high : Icons.brightness_2), + onPressed: () => setState(() => darkMode = !darkMode), + ), + ], + ), + body: Padding( + padding: EdgeInsets.all(8.0), + child: FlutterMap( + options: MapOptions( + center: LatLng(51.5, -0.09), + zoom: 5.0, + ), + layers: [ + TileLayerOptions( + urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + tileProvider: NonCachingNetworkTileProvider(), + tileBuilder: tileBuilder, + tilesContainerBuilder: + darkMode ? darkModeTilesContainerBuilder : null, + ), + MarkerLayerOptions( + markers: [ + Marker( + width: 80.0, + height: 80.0, + point: LatLng(51.5, -0.09), + builder: (ctx) => Container( + child: FlutterLogo( + colors: Colors.blue, + key: ObjectKey(Colors.blue), + ), + ), + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/drawer.dart b/example/lib/widgets/drawer.dart index 02766fb63..4707d7d28 100644 --- a/example/lib/widgets/drawer.dart +++ b/example/lib/widgets/drawer.dart @@ -19,6 +19,7 @@ import '../pages/plugin_zoombuttons.dart'; import '../pages/polyline.dart'; import '../pages/sliding_map.dart'; import '../pages/tap_to_add.dart'; +import '../pages/tile_builder_example.dart'; import '../pages/tile_loading_error_handle.dart'; import '../pages/widgets.dart'; import '../pages/wms_tile_layer.dart'; @@ -157,33 +158,35 @@ Drawer buildDrawer(BuildContext context, String currentRoute) { OverlayImagePage.route, currentRoute, ), - ListTile( - title: const Text('Sliding Map'), - selected: currentRoute == SlidingMapPage.route, - onTap: () => - Navigator.pushReplacementNamed(context, SlidingMapPage.route), - ), - ListTile( - title: const Text('Widgets'), - selected: currentRoute == WidgetsPage.route, - onTap: () { - Navigator.pushReplacementNamed(context, WidgetsPage.route); - }, - ), - ListTile( - title: const Text('Live Location Update'), - selected: currentRoute == LiveLocationPage.route, - onTap: () { - Navigator.pushReplacementNamed(context, LiveLocationPage.route); - }, - ), - ListTile( - title: const Text('Tile loading error handle'), - selected: currentRoute == TileLoadingErrorHandle.route, - onTap: () { - Navigator.pushReplacementNamed( - context, TileLoadingErrorHandle.route); - }, + _buildMenuItem( + context, + const Text('Sliding Map'), + SlidingMapPage.route, + currentRoute, + ), + _buildMenuItem( + context, + const Text('Widgets'), + WidgetsPage.route, + currentRoute, + ), + _buildMenuItem( + context, + const Text('Live Location Update'), + LiveLocationPage.route, + currentRoute, + ), + _buildMenuItem( + context, + const Text('Tile loading error handle'), + TileLoadingErrorHandle.route, + currentRoute, + ), + _buildMenuItem( + context, + const Text('Tile builder'), + TileBuilderPage.route, + currentRoute, ), ], ), diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index cc2254036..fbfd8afcd 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -21,6 +21,7 @@ export 'package:flutter_map/src/layer/marker_layer.dart'; export 'package:flutter_map/src/layer/overlay_image_layer.dart'; export 'package:flutter_map/src/layer/polygon_layer.dart'; export 'package:flutter_map/src/layer/polyline_layer.dart'; +export 'package:flutter_map/src/layer/tile_builder/tile_builder.dart'; export 'package:flutter_map/src/layer/tile_layer.dart'; export 'package:flutter_map/src/layer/tile_provider/mbtiles_image_provider.dart'; export 'package:flutter_map/src/layer/tile_provider/tile_provider.dart'; diff --git a/lib/src/layer/tile_builder/tile_builder.dart b/lib/src/layer/tile_builder/tile_builder.dart new file mode 100644 index 000000000..32388d2b4 --- /dev/null +++ b/lib/src/layer/tile_builder/tile_builder.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/src/layer/tile_layer.dart'; + +typedef TileBuilder = Widget Function( + BuildContext context, Widget tileWidget, Tile tile); + +typedef TilesContainerBuilder = Widget Function( + BuildContext context, Widget tilesContainer, List tiles); + +/// Applies inversion color matrix on Tiles container which may simulate Dark mode. +final TilesContainerBuilder darkModeTilesContainerBuilder = + (BuildContext context, Widget tilesContainer, List tiles) { + return ColorFiltered( + colorFilter: const ColorFilter.matrix([ + -1, + 0, + 0, + 0, + 255, + 0, + -1, + 0, + 0, + 255, + 0, + 0, + -1, + 0, + 255, + 0, + 0, + 0, + 1, + 0, + ]), + child: tilesContainer, + ); +}; + +/// Applies inversion color matrix on Tiles which may simulate Dark mode. +/// [darkModeTilesContainerBuilder] is better at performance because it applies color matrix on the container instead of on every Tile +final TileBuilder darkModeTileBuilder = + (BuildContext context, Widget tileWidget, Tile tile) { + return ColorFiltered( + colorFilter: const ColorFilter.matrix([ + -1, + 0, + 0, + 0, + 255, + 0, + -1, + 0, + 0, + 255, + 0, + 0, + -1, + 0, + 255, + 0, + 0, + 0, + 1, + 0, + ]), + child: tileWidget, + ); +}; + +/// Shows coordinates over Tiles +final TileBuilder coordinateDebugTileBuilder = + (BuildContext context, Widget tileWidget, Tile tile) { + final coords = tile.coords; + final readableKey = + '${coords.x.floor()} : ${coords.y.floor()} : ${coords.z.floor()}'; + + return Container( + decoration: BoxDecoration( + border: Border.all(), + ), + child: Stack( + fit: StackFit.passthrough, + children: [ + tileWidget, + Center( + child: Text( + readableKey, + style: Theme.of(context).textTheme.headline5, + ), + ), + ], + ), + ); +}; + +/// Shows the Tile loading time in ms +final TileBuilder loadingTimeDebugTileBuilder = + (BuildContext context, Widget tileWidget, Tile tile) { + var loadStarted = tile.loadStarted; + var loaded = tile.loaded; + + final time = loaded == null + ? 'Loading' + : '${(loaded.millisecond - loadStarted.millisecond).abs()} ms'; + + return Container( + decoration: BoxDecoration( + border: Border.all(), + ), + child: Stack( + fit: StackFit.passthrough, + children: [ + tileWidget, + Center( + child: Text( + time, + style: Theme.of(context).textTheme.headline5, + ), + ), + ], + ), + ); +}; diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 9739a4ec6..6d0cc380d 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -7,6 +7,7 @@ import 'package:flutter_map/src/core/bounds.dart'; import 'package:flutter_map/src/core/point.dart'; import 'package:flutter_map/src/core/util.dart' as util; import 'package:flutter_map/src/geo/crs/crs.dart'; +import 'package:flutter_map/src/layer/tile_builder/tile_builder.dart'; import 'package:flutter_map/src/layer/tile_provider/tile_provider.dart'; import 'package:flutter_map/src/map/map.dart'; import 'package:latlong/latlong.dart'; @@ -189,6 +190,14 @@ class TileLayerOptions extends LayerOptions { /// This callback will be execute if some errors by getting tile final ErrorTileCallBack errorTileCallback; + /// Function which may Wrap Tile with custom Widget + /// There are predefined examples in 'tile_builder.dart' + final TileBuilder tileBuilder; + + /// Function which may wrap Tiles Container with custom Widget + /// There are predefined examples in 'tile_builder.dart' + final TilesContainerBuilder tilesContainerBuilder; + TileLayerOptions({ Key key, this.urlTemplate, @@ -224,6 +233,8 @@ class TileLayerOptions extends LayerOptions { this.overrideTilesWhenUrlChanges = false, this.retinaMode = false, this.errorTileCallback, + this.tileBuilder, + this.tilesContainerBuilder, rebuild, }) : updateInterval = updateInterval <= 0 ? null : Duration(milliseconds: updateInterval), @@ -475,13 +486,21 @@ class _TileLayerState extends State with TickerProviderStateMixin { for (var tile in tilesToRender) _createTileWidget(tile) ]; + var tilesContainer = Stack( + children: tileWidgets, + ); + return Opacity( opacity: options.opacity, child: Container( color: options.backgroundColor, - child: Stack( - children: tileWidgets, - ), + child: options.tilesContainerBuilder == null + ? tilesContainer + : options.tilesContainerBuilder( + context, + tilesContainer, + tilesToRender, + ), ), ); } @@ -497,6 +516,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { final Widget content = AnimatedTile( tile: tile, errorImage: options.errorImage, + tileBuilder: options.tileBuilder, ); return Positioned( @@ -1055,6 +1075,7 @@ class Tile implements Comparable { bool active; bool loadError; DateTime loaded; + DateTime loadStarted; AnimationController animationController; double get opacity => animationController == null @@ -1082,6 +1103,8 @@ class Tile implements Comparable { }); void loadTileImage() { + loadStarted = DateTime.now(); + try { final oldImageStream = _imageStream; _imageStream = imageProvider.resolve(ImageConfiguration()); @@ -1166,9 +1189,14 @@ class Tile implements Comparable { class AnimatedTile extends StatefulWidget { final Tile tile; final ImageProvider errorImage; + final TileBuilder tileBuilder; - AnimatedTile({Key key, @required this.tile, this.errorImage}) - : assert(null != tile), + AnimatedTile({ + Key key, + @required this.tile, + this.errorImage, + @required this.tileBuilder, + }) : assert(null != tile), super(key: key); @override @@ -1180,17 +1208,21 @@ class _AnimatedTileState extends State { @override Widget build(BuildContext context) { + final tileWidget = (widget.tile.loadError && widget.errorImage != null) + ? Image( + image: widget.errorImage, + fit: BoxFit.fill, + ) + : RawImage( + image: widget.tile.imageInfo?.image, + fit: BoxFit.fill, + ); + return Opacity( opacity: widget.tile.opacity, - child: (widget.tile.loadError && widget.errorImage != null) - ? Image( - image: widget.errorImage, - fit: BoxFit.fill, - ) - : RawImage( - image: widget.tile.imageInfo?.image, - fit: BoxFit.fill, - ), + child: widget.tileBuilder == null + ? tileWidget + : widget.tileBuilder(context, tileWidget, widget.tile), ); }