Skip to content

Commit

Permalink
Improved documentation
Browse files Browse the repository at this point in the history
Improved error message for incorrect `AnchoredLayer` usage
  • Loading branch information
JaffaKetchup committed Aug 18, 2023
1 parent dd8befc commit 2a8a321
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 140 deletions.
29 changes: 14 additions & 15 deletions example/lib/pages/wms_tile_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,36 +28,35 @@ class WMSLayerPage extends StatelessWidget {
initialCenter: LatLng(42.58, 12.43),
initialZoom: 6,
),
children: [
TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://{s}.s2maps-tiles.eu/wms/?',
layers: const ['s2cloudless-2021_3857'],
),
subdomains: const ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
),
overlaidAnchoredChildren: [
RichAttributionWidget(
popupInitialDisplayDuration: const Duration(seconds: 5),
attributions: [
TextSourceAttribution(
'Sentinel-2 cloudless - https://s2maps.eu by EOX IT Services GmbH',
onTap: () => launchUrl(
Uri.parse('https://s2maps.eu '),
),
onTap: () => launchUrl(Uri.parse('https://s2maps.eu')),
),
const TextSourceAttribution(
'Modified Copernicus Sentinel data 2021',
),
TextSourceAttribution(
'Rendering: EOX::Maps',
onTap: () => launchUrl(
Uri.parse('https://maps.eox.at/'),
),
onTap: () =>
launchUrl(Uri.parse('https://maps.eox.at/')),
),
],
),
],
children: [
TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://{s}.s2maps-tiles.eu/wms/?',
layers: const ['s2cloudless-2021_3857'],
),
subdomains: const ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
),
],
),
),
],
Expand Down
183 changes: 107 additions & 76 deletions lib/src/layer/general/anchored_layer.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,59 @@
part of '../../map/widget.dart';

/// A layer that is anchored to the map, that does not move with the other layers
///
/// There are multiple ways to add an anchored layer to the map, in order of
/// preference:
///
/// 1. Apply [AnchoredLayerStatelessMixin] to a [StatelessWidget]
/// 2. Apply [AnchoredLayerStatefulMixin] to a [StatefulWidget], and
/// [AnchoredLayerStateMixin] to its corresponding [State]
/// 3. Wrap the widget with an [AnchoredLayerTransformer]
///
/// This layer may be used in both [FlutterMap.children] and
/// [FlutterMap.overlaidAnchoredChildren]. See documentation on those properties
/// for more information.
///
/// {@template anchored_layer_warning}
/// [AnchoredLayer]s must be on the top-level of [FlutterMap.children] or
/// [FlutterMap.overlaidAnchoredChildren]. They must also not be multiplied as an
/// ancestor or child: use only one. Failure to do this will throw an error, as
/// the anchored effect will not be correctly applied. See below for common
/// mistakes and their resolutions:
///
/// * If you have control over the widget, do not use [AnchoredLayerTransformer]
/// in its `build` method. Prefer using the appropriate mixin, or wrap the
/// transformer around every instance of the widget.
/// * If the widget already uses a mixin, do not use [AnchoredLayerTransformer]
/// in addition. These widgets are designed to be used as an anchored layer only,
/// and need no additional setup. These widgets should contain a notice in the
/// documentation.
/// {@endtemplate}
sealed class AnchoredLayer extends Widget {
const AnchoredLayer._();
}

/// Provide an internal detection point for the [AnchoredLayer]s
///
/// Although any other widget could be used as the detection point, this is
/// provided as close as possible to the mixin-ed widgets to allow only one
/// `context.visitAncestorElements` iteration to determine whether usage is
/// correct.
///
/// ---
///
/// Explanation of how [AnchoredLayer]s work internally:
///
/// 1. A [Widget] is converted to an [AnchoredLayer] by means of the mixins, or
/// the [AnchoredLayerTransformer] (which uses the mixins behind the scenes).
/// 2. The mixin means the affected [Widget] must call the mixin's own `build`
/// method, which calls [_detectAncestor].
/// 3. [_detectAncestor] performs a single lookup to the direct ancestor, to
/// check whether it is an [_AnchoredLayerDetectorAncestor], throwing if it
/// isn't, because it will not have the next step applied
/// 4. When laying out the layers, [_LayersStack] avoids applying rotation to
/// any widget that has mixed-in one of the mixins, whilst the layer itself
/// doesn't follow the map movement - resulting in an 'anchored' layer.
class _AnchoredLayerDetectorAncestor extends StatelessWidget {
const _AnchoredLayerDetectorAncestor({required this.child});

Expand All @@ -14,14 +62,25 @@ class _AnchoredLayerDetectorAncestor extends StatelessWidget {
@override
Widget build(BuildContext context) => child;

static Widget _buildDetector(BuildContext context) {
static Widget _detectAncestor(BuildContext context) {
context.visitAncestorElements((e) {
if (e.widget is _AnchoredLayerDetectorAncestor) return false;

throw FlutterError(
'The `AnchoredLayer` was used incorrectly. Read the documenation on '
'`AnchoredLayer` for more information.',
);
throw FlutterError.fromParts([
ErrorSummary(
'`AnchoredLayer` (such as `AnchoredLayerTransformer` or one of the '
'mixins) was used incorrectly.',
),
ErrorDescription(
'Instead of `_AnchoredLayerDetectorAncestor`, the direct ancestor was '
'of type `${e.widget.runtimeType}`.',
),
ErrorHint(
'Ensure that there is only one `AnchoredLayer`, and that it is a '
'top-level widget of `children` or `overlaidAnchoredChildren`. Read '
'the documentation on `AnchoredLayer` for more information.',
)
]);
});

// The user shouldn't build the output of this method
Expand All @@ -34,27 +93,43 @@ class _AnchoredLayerDetectorAncestor extends StatelessWidget {
}
}

/// Apply to a [StatelessWidget] to transform it into an [AnchoredLayer]
/// Transforms the [child] widget into an [AnchoredLayer]
///
/// {@macro anchored_layer_call_super}
/// Uses a [AnchoredLayerStatelessMixin] internally.
///
/// {@macro anchored_layer_more_info}
/// {@template anchored_layer_more_info}
/// See [AnchoredLayer] for more information about other methods to create an
/// anchored layer.
/// {@endtemplate}
///
/// ---
///
/// {@macro anchored_layer_warning}
mixin AnchoredLayerStatelessMixin on StatelessWidget implements AnchoredLayer {
class AnchoredLayerTransformer extends StatelessWidget
with AnchoredLayerStatelessMixin
implements AnchoredLayer {
/// Transforms the [child] widget into an [AnchoredLayer]
///
/// Uses a [AnchoredLayerStatelessMixin] internally.
///
/// ---
///
/// {@macro anchored_layer_warning}
const AnchoredLayerTransformer({
super.key,
required this.child,
});

final Widget child;

@override
@mustCallSuper
Widget build(BuildContext context) =>
_AnchoredLayerDetectorAncestor._buildDetector(context);
Widget build(BuildContext context) {
super.build(context);
return child;
}
}

/// Apply to a [State] to transform its corresponding [StatefulWidget] into an
/// [AnchoredLayer]
///
/// Must be paired with an [AnchoredLayerStatefulMixin] on the [State]. See
/// [RichAttributionWidget] for an example of this.
/// Apply to a [StatelessWidget] to transform it into an [AnchoredLayer]
///
/// {@macro anchored_layer_call_super}
///
Expand All @@ -63,13 +138,11 @@ mixin AnchoredLayerStatelessMixin on StatelessWidget implements AnchoredLayer {
/// ---
///
/// {@macro anchored_layer_warning}
mixin AnchoredLayerStateMixin<
T extends AnchoredLayerStatefulMixin<AnchoredLayerStateMixin<T>>>
on State<T> {
mixin AnchoredLayerStatelessMixin on StatelessWidget implements AnchoredLayer {
@override
@mustCallSuper
Widget build(BuildContext context) =>
_AnchoredLayerDetectorAncestor._buildDetector(context);
_AnchoredLayerDetectorAncestor._detectAncestor(context);
}

/// Apply to a [StatefulWidget] to transform it into an [AnchoredLayer]
Expand All @@ -95,66 +168,24 @@ mixin AnchoredLayerStatefulMixin<T extends State<AnchoredLayerStatefulMixin<T>>>
AnchoredLayerStateMixin createState();
}

/// Transforms the [child] widget into an [AnchoredLayer]
/// Apply to a [State] to transform its corresponding [StatefulWidget] into an
/// [AnchoredLayer]
///
/// Uses a [AnchoredLayerStatelessMixin] internally.
/// Must be paired with an [AnchoredLayerStatefulMixin] on the [State]. See
/// [RichAttributionWidget] for an example of this.
///
/// {@template anchored_layer_more_info}
/// See [AnchoredLayer] for more information about other methods to create an
/// anchored layer.
/// {@endtemplate}
/// {@macro anchored_layer_call_super}
///
/// {@macro anchored_layer_more_info}
///
/// ---
///
/// {@macro anchored_layer_warning}
class AnchoredLayerTransformer extends StatelessWidget
with AnchoredLayerStatelessMixin
implements AnchoredLayer {
/// Transforms the [child] widget into an [AnchoredLayer]
///
/// Uses a [AnchoredLayerStatelessMixin] internally.
///
/// ---
///
/// {@macro anchored_layer_warning}
const AnchoredLayerTransformer({
super.key,
required this.child,
});

final Widget child;

mixin AnchoredLayerStateMixin<
T extends AnchoredLayerStatefulMixin<AnchoredLayerStateMixin<T>>>
on State<T> {
@override
Widget build(BuildContext context) {
super.build(context);
return child;
}
}

/// A layer that is anchored to the map, that does not move with the other layers
///
/// There are multiple ways to add an anchored layer to the map, in order of
/// preference:
///
/// 1. Apply [AnchoredLayerStatelessMixin] to a [StatelessWidget]
/// 2. Apply [AnchoredLayerStatefulMixin] to a [StatefulWidget], and
/// [AnchoredLayerStateMixin] to its corresponding [State]
/// 3. Wrap the normal widget with an [AnchoredLayerTransformer]
///
/// {@template anchored_layer_warning}
/// Anchored layers must be on the top-level of [FlutterMap.children] and
/// [FlutterMap.overlaidAnchoredChildren]. They must also not be multiplied as an
/// ancestor or child. Failure to do this will throw an error, as the anchored
/// effect will not be correctly applied.
///
/// * If you have control over the widget, do not use [AnchoredLayerTransformer]
/// in its `build` method. Prefer using the appropriate mixin, or wrap the
/// transformer around every instance of the widget.
/// * If the widget already uses a mixin, do not use [AnchoredLayerTransformer]
/// in addition. These widgets are designed to be used as an anchored layer only,
/// and need no additional setup. These widgets should contain a notice in the
/// documentation.
/// {@endtemplate}
sealed class AnchoredLayer extends Widget {
const AnchoredLayer({super.key});
@mustCallSuper
Widget build(BuildContext context) =>
_AnchoredLayerDetectorAncestor._detectAncestor(context);
}
51 changes: 7 additions & 44 deletions lib/src/layer/general/translucent_pointer.dart
Original file line number Diff line number Diff line change
@@ -1,44 +1,11 @@
// Migrated from https://github.com/spkersten/flutter_transparent_pointer, with
// some changes
// some API & documentation changes

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

/// This widget is invisible for its parent during hit testing, but still
/// allows its subtree to receive pointer events.
///
///
/// In this example, a drag can be started anywhere in the widget, including on
/// top of the text button, even though the button is visually in front of the
/// background gesture detector. At the same time, the button is tappable.
///
/// ```dart
/// class MyWidget extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Stack(
/// children: [
/// GestureDetector(
/// behavior: HitTestBehavior.opaque,
/// onVerticalDragStart: (_) => print("Background drag started"),
/// ),
/// Positioned(
/// top: 60,
/// left: 60,
/// height: 60,
/// width: 60,
/// child: TransparentPointer(
/// child: TextButton(
/// child: Text("Tap me"),
/// onPressed: () => print("You tapped me"),
/// ),
/// ),
/// ),
/// ],
/// );
/// }
/// }
/// ```
/// A widget that is invisible for its parent during hit testing, but still
/// allows its subtree to receive pointer events
///
/// See also:
///
Expand All @@ -48,7 +15,7 @@ import 'package:flutter/widgets.dart';
/// subtree from receiving pointer event. The opposite of this widget.
class TranslucentPointer extends SingleChildRenderObjectWidget {
/// Creates a widget that is invisible for its parent during hit testing, but
/// still allows its subtree to receive pointer events.
/// still allows its subtree to receive pointer events
const TranslucentPointer({
super.key,
this.translucent = true,
Expand Down Expand Up @@ -95,9 +62,7 @@ class TranslucentPointer extends SingleChildRenderObjectWidget {
/// * [RenderAbsorbPointer], which takes the pointer events but prevents any
/// nodes in the subtree from seeing them.
class RenderTranslucentPointer extends RenderProxyBox {
/// Creates a render object that is invisible to its parent during hit testing.
///
/// The [translucent] argument must not be null.
/// Creates a render object that is invisible to its parent during hit testing
RenderTranslucentPointer({
RenderBox? child,
bool translucent = true,
Expand All @@ -117,10 +82,8 @@ class RenderTranslucentPointer extends RenderProxyBox {
}

@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
final hit = super.hitTest(result, position: position);
return !translucent && hit;
}
bool hitTest(BoxHitTestResult result, {required Offset position}) =>
!translucent && super.hitTest(result, position: position);

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
Expand Down
8 changes: 6 additions & 2 deletions lib/src/map/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart';
import 'package:flutter_map/src/gestures/interactive_flag.dart';
import 'package:flutter_map/src/gestures/map_events.dart';
import 'package:flutter_map/src/gestures/multi_finger_gesture.dart';
import 'package:flutter_map/src/layer/general/translucent_pointer.dart';
import 'package:flutter_map/src/map/camera/camera_constraint.dart';
import 'package:flutter_map/src/map/camera/camera_fit.dart';
import 'package:flutter_map/src/map/inherited_model.dart';
import 'package:flutter_map/src/map/widget.dart';
import 'package:flutter_map/src/misc/fit_bounds_options.dart';
import 'package:flutter_map/src/misc/position.dart';
import 'package:flutter_map/src/misc/private/positioned_tap_detector_2.dart';
Expand Down Expand Up @@ -118,8 +120,10 @@ class MapOptions {
/// Note that layers that are visually obscured behind another layer will
/// recieve events, if this is enabled.
///
/// If this is `false` (defaults to `true`), then `TranslucentPointer` may be
/// used on individual layers.
/// Also note that this applies to (overlaid) [AnchoredLayer]s as well.
///
/// If this is `false` (defaults to `true`), then [TranslucentPointer] may be
/// applied to individual layers.
final bool applyPointerTranslucencyToLayers;

final InteractionOptions? _interactionOptions;
Expand Down
Loading

0 comments on commit 2a8a321

Please sign in to comment.