Flutter地图动画插件flutter_map_animations的使用

发布于 1周前 作者 h691938207 来自 Flutter

Flutter地图动画插件flutter_map_animations的使用

简介

flutter_map_animations 是为 flutter_map 包提供的动画工具,可以实现流畅的地图动画效果。你可以通过它来创建平滑的缩放、旋转和移动动画。

安装

pubspec.yaml 文件中添加依赖:

dependencies:
  flutter_map: ^6.0.0
  flutter_map_animations: ^0.5.0

然后运行 flutter pub get 来安装包。

使用示例

示例代码

以下是一个完整的示例,展示了如何使用 flutter_map_animations 实现各种动画效果:

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_animations/flutter_map_animations.dart';
import 'package:flutter_map_cancellable_tile_provider/flutter_map_cancellable_tile_provider.dart';
import 'package:latlong2/latlong.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  static const _useTransformerId = 'useTransformerId';

  final markers = ValueNotifier<List<AnimatedMarker>>([]);
  final center = const LatLng(51.509364, -0.128928);

  bool _useTransformer = true;
  int _lastMovedToMarkerIndex = -1;

  late final _animatedMapController = AnimatedMapController(vsync: this);

  @override
  void dispose() {
    markers.dispose();
    _animatedMapController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flutter Map Animations')),
      body: ValueListenableBuilder<List<AnimatedMarker>>(
        valueListenable: markers,
        builder: (context, markers, _) {
          return FlutterMap(
            mapController: _animatedMapController.mapController,
            options: MapOptions(
              initialCenter: center,
              onTap: (_, point) => _addMarker(point),
            ),
            children: [
              TileLayer(
                urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
                userAgentPackageName: 'com.example.app',
                tileUpdateTransformer: _animatedMoveTileUpdateTransformer,
                tileProvider: CancellableNetworkTileProvider(),
              ),
              AnimatedMarkerLayer(markers: markers),
            ],
          );
        },
      ),
      floatingActionButton: SeparatedColumn(
        separator: const SizedBox(height: 8),
        children: [
          FloatingActionButton(
            onPressed: () => _animatedMapController.animatedRotateFrom(
              90,
              customId: _useTransformer ? _useTransformerId : null,
            ),
            tooltip: 'Rotate 90°',
            child: const Icon(Icons.rotate_right),
          ),
          FloatingActionButton(
            onPressed: () => _animatedMapController.animatedRotateFrom(
              -90,
              customId: _useTransformer ? _useTransformerId : null,
            ),
            tooltip: 'Rotate -90°',
            child: const Icon(Icons.rotate_left),
          ),
          FloatingActionButton(
            onPressed: () {
              markers.value = [];
              _animatedMapController.animateTo(
                dest: center,
                rotation: 0,
                customId: _useTransformer ? _useTransformerId : null,
              );
            },
            tooltip: 'Clear modifications',
            child: const Icon(Icons.clear_all),
          ),
          FloatingActionButton(
            onPressed: () => _animatedMapController.animatedZoomIn(
              customId: _useTransformer ? _useTransformerId : null,
            ),
            tooltip: 'Zoom in',
            child: const Icon(Icons.zoom_in),
          ),
          FloatingActionButton(
            onPressed: () => _animatedMapController.animatedZoomOut(
              customId: _useTransformer ? _useTransformerId : null,
            ),
            tooltip: 'Zoom out',
            child: const Icon(Icons.zoom_out),
          ),
          FloatingActionButton(
            tooltip: 'Center on markers',
            onPressed: () {
              if (markers.value.length < 2) return;

              final points = markers.value.map((m) => m.point).toList();
              _animatedMapController.animatedFitCamera(
                cameraFit: CameraFit.coordinates(
                  coordinates: points,
                  padding: const EdgeInsets.all(12),
                ),
                rotation: 0,
                customId: _useTransformer ? _useTransformerId : null,
              );
            },
            child: const Icon(Icons.center_focus_strong),
          ),
          Row(
            children: [
              FloatingActionButton(
                tooltip: 'Move to next marker with offset',
                onPressed: () {
                  if (markers.value.isEmpty) return;

                  final points = markers.value.map((m) => m.point);
                  setState(
                    () => _lastMovedToMarkerIndex =
                        (_lastMovedToMarkerIndex + 1) % points.length,
                  );

                  _animatedMapController.animateTo(
                    dest: points.elementAt(_lastMovedToMarkerIndex),
                    customId: _useTransformer ? _useTransformerId : null,
                    offset: const Offset(100, 100),
                  );
                },
                child: const Icon(Icons.multiple_stop),
              ),
              const SizedBox(width: 8),
              FloatingActionButton(
                tooltip: 'Move to next marker',
                onPressed: () {
                  if (markers.value.isEmpty) return;

                  final points = markers.value.map((m) => m.point);
                  setState(
                    () => _lastMovedToMarkerIndex =
                        (_lastMovedToMarkerIndex + 1) % points.length,
                  );

                  _animatedMapController.animateTo(
                    dest: points.elementAt(_lastMovedToMarkerIndex),
                    customId: _useTransformer ? _useTransformerId : null,
                  );
                },
                child: const Icon(Icons.polyline_rounded),
              ),
            ],
          ),
          FloatingActionButton.extended(
            label: Row(
              children: [
                const Text('Transformer'),
                Switch(
                  activeColor: Colors.blue.shade200,
                  activeTrackColor: Colors.black38,
                  value: _useTransformer,
                  onChanged: (newValue) {
                    setState(() => _useTransformer = newValue);
                  },
                ),
              ],
            ),
            onPressed: () {
              setState(() => _useTransformer = !_useTransformer);
            },
          ),
        ],
      ),
    );
  }

  void _addMarker(LatLng point) {
    markers.value = List.from(markers.value)
      ..add(
        MyMarker(
          point: point,
          onTap: () => _animatedMapController.animateTo(
            dest: point,
            customId: _useTransformer ? _useTransformerId : null,
          ),
        ),
      );
  }
}

class MyMarker extends AnimatedMarker {
  MyMarker({
    required super.point,
    VoidCallback? onTap,
  }) : super(
          width: markerSize,
          height: markerSize,
          builder: (context, animation) {
            final size = markerSize * animation.value;

            return GestureDetector(
              onTap: onTap,
              child: Opacity(
                opacity: animation.value,
                child: Icon(
                  Icons.room,
                  size: size,
                ),
              ),
            );
          },
        );

  static const markerSize = 50.0;
}

class SeparatedColumn extends StatelessWidget {
  const SeparatedColumn({
    super.key,
    required this.separator,
    this.children = const [],
    this.mainAxisSize = MainAxisSize.max,
    this.crossAxisAlignment = CrossAxisAlignment.start,
  });

  final Widget separator;
  final List<Widget> children;
  final MainAxisSize mainAxisSize;
  final CrossAxisAlignment crossAxisAlignment;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
      children: [
        ..._buildChildren(),
      ],
    );
  }

  Iterable<Widget> _buildChildren() sync* {
    for (var i = 0; i < children.length; i++) {
      yield children[i];
      if (i < children.length - 1) yield separator;
    }
  }
}

final _animatedMoveTileUpdateTransformer = TileUpdateTransformer.fromHandlers(
  handleData: (updateEvent, sink) {
    final id = AnimationId.fromMapEvent(updateEvent.mapEvent);

    if (id == null) return sink.add(updateEvent);
    if (id.customId != _MyHomePageState._useTransformerId) {
      if (id.moveId == AnimatedMoveId.started) {
        debugPrint('TileUpdateTransformer disabled, using default behaviour.');
      }
      return sink.add(updateEvent);
    }

    switch (id.moveId) {
      case AnimatedMoveId.started:
        debugPrint('Loading tiles at animation destination.');
        sink.add(
          updateEvent.loadOnly(
            loadCenterOverride: id.destLocation,
            loadZoomOverride: id.destZoom,
          ),
        );
        break;
      case AnimatedMoveId.inProgress:
        // Do not prune or load during movement.
        break;
      case AnimatedMoveId.finished:
        debugPrint('Pruning tiles after animated movement.');
        sink.add(updateEvent.pruneOnly());
        break;
    }
  },
);

主要功能

AnimatedMapController

  • 初始化:创建 AnimatedMapController 并设置 vsync 参数。
  • 全局配置:可以通过 durationcurvecancelPreviousAnimations 来配置动画的行为。
  • 方法
    • animateTo:移动到指定位置。
    • animatedZoomInanimatedZoomOut:缩放。
    • animatedRotateFrom:旋转。
    • animatedFitCamera:适应多个标记点。

AnimatedMarkerLayer & AnimatedMarker

  • 动画标记层:用于在地图上显示带有动画效果的标记。
  • 示例:通过 AnimatedMarker 创建一个标记,并在点击时触发动画。

迁移指南

  • v0.5.0:移除了一些参数,如 AnimatedMarker.rotateOriginAnimatedMarker.anchorPos,并重命名了部分参数。
  • v0.4.0:不再允许直接扩展 MapControllerImpl,而是通过内部创建或传递 MapController 实例。

贡献者

感谢以下贡献者的努力:

  • Guillaume Roux
  • Luka S
  • Rory Stephenson
  • Reinis Sprogis

希望这个示例能帮助你更好地理解和使用 flutter_map_animations 插件。如果有任何问题或需要进一步的帮助,请随时提问!


更多关于Flutter地图动画插件flutter_map_animations的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter地图动画插件flutter_map_animations的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用flutter_map_animations插件来实现地图动画效果的代码案例。flutter_map_animations插件允许你在Flutter应用中为flutter_map添加动画效果。

首先,确保你已经在pubspec.yaml文件中添加了必要的依赖项:

dependencies:
  flutter:
    sdk: flutter
  flutter_map: ^10.2.0  # 确保版本与flutter_map_animations兼容
  flutter_map_animations: ^0.4.0  # 请检查最新版本

然后运行flutter pub get来安装这些依赖项。

接下来,在你的Flutter应用中,你可以按照以下步骤来添加地图动画效果:

  1. 导入必要的包
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_animations/flutter_map_animations.dart';
import 'package:latlong2/latlong.dart';
  1. 定义地图的中心点和缩放级别
final LatLng center = LatLng(51.5, -0.09);
final double zoom = 13.0;
  1. 创建地图和动画
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Map Animations Demo'),
        ),
        body: MapWithAnimations(),
      ),
    );
  }
}

class MapWithAnimations extends StatefulWidget {
  @override
  _MapWithAnimationsState createState() => _MapWithAnimationsState();
}

class _MapWithAnimationsState extends State<MapWithAnimations> {
  MapController? mapController;

  @override
  Widget build(BuildContext context) {
    return FlutterMap(
      mapController: mapController,
      options: MapOptions(
        center: center,
        zoom: zoom,
      ),
      layers: [
        TileLayerOptions(
          urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
          subdomains: ['a', 'b', 'c'],
        ),
        MarkerLayerOptions(
          markers: [
            Marker(
              point: LatLng(51.5, -0.09),
              builder: (ctx) => Container(
                child: Icon(Icons.location_on, color: Colors.red,),
              ),
            ),
            // 你可以添加更多的Marker
          ],
        ),
        AnimatedMarkerLayerOptions(
          animatedMarkers: [
            AnimatedMarker(
              id: 'animatedMarker',
              initialPosition: LatLng(51.5, -0.09),
              finalPosition: LatLng(51.51, -0.11),
              duration: Duration(seconds: 5),
              curve: Curves.easeInOutQuad,
              builder: (ctx, position) => Container(
                child: Icon(Icons.directions_car, color: Colors.blue,),
                alignment: Alignment.center,
              ),
            ),
          ],
        ),
      ],
      onMapCreated: (controller) {
        setState(() {
          mapController = controller;
        });
      },
    );
  }
}

在这个示例中,我们:

  • 使用FlutterMap来创建地图。
  • 添加了一个静态的Marker
  • 使用AnimatedMarkerLayerOptions添加了一个动画标记(AnimatedMarker),这个标记会从初始位置移动到最终位置,并在移动过程中应用动画效果。

你可以根据需要调整动画的idinitialPositionfinalPositiondurationcurve等参数,以实现不同的动画效果。

希望这能帮助你开始在Flutter项目中使用flutter_map_animations插件!

回到顶部