Skip to content

Commit

Permalink
Reworked retina mode behaviour (#1673)
Browse files Browse the repository at this point in the history
Co-authored-by: JaffaKetchup <github@jaffaketchup.dev>
  • Loading branch information
bramp and JaffaKetchup authored Oct 2, 2023
1 parent 0bd301b commit caa0787
Show file tree
Hide file tree
Showing 8 changed files with 363 additions and 50 deletions.
Binary file added example/assets/mapbox-logo-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'package:flutter_map_example/pages/point_to_latlng.dart';
import 'package:flutter_map_example/pages/polygon.dart';
import 'package:flutter_map_example/pages/polyline.dart';
import 'package:flutter_map_example/pages/reset_tile_layer.dart';
import 'package:flutter_map_example/pages/retina.dart';
import 'package:flutter_map_example/pages/secondary_tap.dart';
import 'package:flutter_map_example/pages/sliding_map.dart';
import 'package:flutter_map_example/pages/stateful_markers.dart';
Expand Down Expand Up @@ -81,6 +82,7 @@ class MyApp extends StatelessWidget {
FallbackUrlNetworkPage.route: (context) =>
const FallbackUrlNetworkPage(),
SecondaryTapPage.route: (context) => const SecondaryTapPage(),
RetinaPage.route: (context) => const RetinaPage(),
},
);
}
Expand Down
223 changes: 223 additions & 0 deletions example/lib/pages/retina.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:flutter_map_example/widgets/drawer.dart';
import 'package:latlong2/latlong.dart';
import 'package:url_launcher/url_launcher.dart';

class RetinaPage extends StatefulWidget {
static const String route = '/retina';

static const String _defaultUrlTemplate =
'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/{z}/{x}/{y}{r}?access_token={accessToken}';

const RetinaPage({Key? key}) : super(key: key);

@override
State<RetinaPage> createState() => _RetinaPageState();
}

class _RetinaPageState extends State<RetinaPage> {
String urlTemplate = RetinaPage._defaultUrlTemplate;
final urlTemplateInputController = InputFieldColorizer(
{
'{r}': const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
'{accessToken}': const TextStyle(fontStyle: FontStyle.italic),
},
initialValue: RetinaPage._defaultUrlTemplate,
);
String? accessToken;

bool? retinaMode;

@override
Widget build(BuildContext context) {
final tileLayer = TileLayer(
urlTemplate: urlTemplate,
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
additionalOptions: {'accessToken': accessToken ?? ''},
retinaMode: switch (retinaMode) {
null => RetinaMode.isHighDensity(context),
_ => retinaMode!,
},
tileBuilder: (context, tileWidget, _) => DecoratedBox(
decoration: BoxDecoration(
border: Border.all(width: 2, color: Colors.white),
),
position: DecorationPosition.foreground,
child: tileWidget,
),
);

return Scaffold(
appBar: AppBar(title: const Text('Retina Tiles')),
drawer: buildDrawer(context, RetinaPage.route),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Column(
children: [
const Text(
'Retina Mode',
style: TextStyle(fontWeight: FontWeight.bold),
),
Row(
children: [
Checkbox.adaptive(
tristate: true,
value: retinaMode,
onChanged: (v) => setState(() => retinaMode = v),
),
Text(switch (retinaMode) {
null => '(auto)',
true => '(force)',
false => '(disabled)',
}),
],
),
const SizedBox.square(dimension: 4),
Builder(
builder: (context) {
final dpr = MediaQuery.of(context).devicePixelRatio;
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
const TextSpan(
text: 'Screen Density: ',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: '@${dpr.toStringAsFixed(2)}x\n'),
const TextSpan(
text: 'Resulting Method: ',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(
text: tileLayer.resolvedRetinaMode.friendlyName,
),
],
),
);
},
),
],
),
const SizedBox.square(dimension: 12),
Expanded(
child: Column(
children: [
TextFormField(
onChanged: (v) => setState(() => urlTemplate = v),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link),
border: const UnderlineInputBorder(),
isDense: true,
labelText: 'URL Template',
helperText: urlTemplate.contains('{r}')
? "Remove the '{r}' placeholder to simulate retina mode when enabled"
: "Add an '{r}' placeholder to request retina tiles when enabled",
),
controller: urlTemplateInputController,
),
TextFormField(
onChanged: (v) => setState(() => accessToken = v),
autofocus: true,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.password),
border: const UnderlineInputBorder(),
isDense: true,
labelText: 'Access Token',
errorText: accessToken?.isEmpty ?? true
? 'Insert your own access token'
: null,
),
),
],
),
),
],
),
),
Expanded(
child: FlutterMap(
options: const MapOptions(
initialCenter: LatLng(51.5, -0.09),
initialZoom: 5,
maxZoom: 19,
),
nonRotatedChildren: [
RichAttributionWidget(
attributions: [
LogoSourceAttribution(
Image.asset(
"assets/mapbox-logo-white.png",
color: Colors.black,
),
height: 16,
),
TextSourceAttribution(
'Mapbox',
onTap: () => launchUrl(
Uri.parse('https://www.mapbox.com/about/maps/')),
),
TextSourceAttribution(
'OpenStreetMap',
onTap: () => launchUrl(
Uri.parse('https://www.openstreetmap.org/copyright')),
),
TextSourceAttribution(
'Improve this map',
prependCopyright: false,
onTap: () => launchUrl(
Uri.parse('https://www.mapbox.com/map-feedback')),
),
],
),
],
children: [if (accessToken?.isNotEmpty ?? false) tileLayer],
),
),
],
),
);
}
}

// Inspired by https://stackoverflow.com/a/59773962/11846040
class InputFieldColorizer extends TextEditingController {
final Map<String, TextStyle> mapping;
final Pattern pattern;

InputFieldColorizer(this.mapping, {String? initialValue})
: pattern =
RegExp(mapping.keys.map((key) => RegExp.escape(key)).join('|')),
super(text: initialValue);

@override
TextSpan buildTextSpan({
required BuildContext context,
TextStyle? style,
required bool withComposing,
}) {
final List<InlineSpan> children = [];

text.splitMapJoin(
pattern,
onMatch: (Match match) {
children.add(
TextSpan(text: match[0], style: style!.merge(mapping[match[0]])),
);
return '';
},
onNonMatch: (String text) {
children.add(TextSpan(text: text, style: style));
return '';
},
);

return TextSpan(style: style, children: children);
}
}
7 changes: 7 additions & 0 deletions example/lib/widgets/drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import 'package:flutter_map_example/pages/point_to_latlng.dart';
import 'package:flutter_map_example/pages/polygon.dart';
import 'package:flutter_map_example/pages/polyline.dart';
import 'package:flutter_map_example/pages/reset_tile_layer.dart';
import 'package:flutter_map_example/pages/retina.dart';
import 'package:flutter_map_example/pages/secondary_tap.dart';
import 'package:flutter_map_example/pages/sliding_map.dart';
import 'package:flutter_map_example/pages/stateful_markers.dart';
Expand Down Expand Up @@ -245,6 +246,12 @@ Drawer buildDrawer(BuildContext context, String currentRoute) {
TileBuilderPage.route,
currentRoute,
),
_buildMenuItem(
context,
const Text('Retina Tile Layer'),
RetinaPage.route,
currentRoute,
),
_buildMenuItem(
context,
const Text('Reset Tile Layer'),
Expand Down
1 change: 1 addition & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,5 @@ flutter:
- assets/map/anholt_osmbright/14/8726/
- assets/map/anholt_osmbright/14/8727/
- assets/map/epsg3413/amsr2.png
- assets/mapbox-logo-white.png
- assets/
68 changes: 68 additions & 0 deletions lib/src/layer/tile_layer/retina_mode.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
part of 'tile_layer.dart';

/// Retina mode improves the resolution of map tiles, particularly on high
/// density displays
///
/// Map tiles can look pixelated on high density displays, so some servers
/// support "@2x" tiles, which are tiles at twice the resolution of normal.
/// However, not all tile servers support this, so flutter_map can attempt to
/// simulate retina behaviour.
///
/// ---
///
/// Enabling or disabling of retina mode functionality is done through
/// [TileLayer]'s constructor, with the `retinaMode` argument.
///
/// If this is `true`, the '{r}' placeholder inside [TileLayer.urlTemplate] will
/// be filled with "@2x" to request high resolution tiles from the server, if it
/// is present. If not present, flutter_map will simulate retina behaviour by
/// requesting four tiles at a larger zoom level and combining them together
/// in place of one.
///
/// Note that simulating retina mode will increase tile requests, decrease the
/// effective maximum zoom by 1, and may result in map labels/text/POIs appearing
/// smaller than normal.
///
/// It is recommended to enable retina mode on high density retina displays
/// automatically, using [RetinaMode.isHighDensity].
///
/// If this is `false` (default), then retina mode is disabled.
///
/// ---
///
/// Caution is advised when mixing retina mode with different `tileSize`s,
/// especially when simulating retina mode.
///
/// It is expected that [TileLayer.fallbackUrl] follows the same retina support
/// behaviour as [TileLayer.urlTemplate].
enum RetinaMode {
/// Resolved to disable retina mode
///
/// This should not be referred to by users, but is open for internal and
/// plugin use by [TileLayer.resolvedRetinaMode].
disabled('Disabled'),

/// Resolved to use the '{r}' placeholder to request native retina tiles from
/// the server
///
/// This should not be referred to by users, but is open for internal and
/// plugin use by [TileLayer.resolvedRetinaMode].
server('Server'),

/// Resolved to simulate retina mode
///
/// This should not be referred to by users, but is open for internal and
/// plugin use by [TileLayer.resolvedRetinaMode].
simulation('Simulation');

final String friendlyName;

const RetinaMode(this.friendlyName);

/// Recommended switching method to assign to [TileLayer]`.retinaMode`
///
/// Returns `true` when the [MediaQuery] of [context] returns an indication
/// of a high density display.
static bool isHighDensity(BuildContext context) =>
MediaQuery.of(context).devicePixelRatio > 1.0;
}
Loading

0 comments on commit caa0787

Please sign in to comment.