-
-
Notifications
You must be signed in to change notification settings - Fork 862
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Reworked retina mode behaviour (#1673)
Co-authored-by: JaffaKetchup <github@jaffaketchup.dev>
- Loading branch information
1 parent
0bd301b
commit caa0787
Showing
8 changed files
with
363 additions
and
50 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.