You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter的Google Map中为Marker添加跳跃动画效果?

在Flutter Google Map中实现Marker脉冲缩放动画

要实现动图里的Marker弹性缩放(脉冲)效果,核心思路是自定义带动画的Widget,再将其转换为Google Map Marker支持的BitmapDescriptor,具体步骤和代码如下:

1. 依赖准备

首先确保pubspec.yaml中引入最新版的Google Map包:

dependencies:
  flutter:
    sdk: flutter
  google_maps_flutter: ^2.5.0

2. 实现带动画的Marker Widget

创建一个支持缩放/脉冲动画的自定义Widget,这里用AnimationController结合ScaleTransition实现弹性缩放效果:

class AnimatedMarker extends StatefulWidget {
  final Color color;
  final double size;

  const AnimatedMarker({super.key, required this.color, this.size = 40});

  @override
  State<AnimatedMarker> createState() => _AnimatedMarkerState();
}

class _AnimatedMarkerState extends State<AnimatedMarker> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    // 初始化动画控制器,800ms一个周期,循环反向播放实现回弹
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 800),
    )..repeat(reverse: true);

    // 缩放动画:从1倍放大到1.4倍
    _scaleAnimation = Tween<double>(begin: 1.0, end: 1.4).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _scaleAnimation,
      child: Container(
        width: widget.size,
        height: widget.size,
        decoration: BoxDecoration(
          color: widget.color,
          shape: BoxShape.circle,
          boxShadow: [
            BoxShadow(
              color: widget.color.withOpacity(0.5),
              blurRadius: 8,
              spreadRadius: 2,
            )
          ],
        ),
        child: const Center(
          child: Icon(
            Icons.location_on,
            color: Colors.white,
            size: 20,
          ),
        ),
      ),
    );
  }
}

如果需要更贴近动图的外围扩散脉冲效果,可以修改build方法,叠加一个渐隐的缩放背景圈:

@override
Widget build(BuildContext context) {
  return Stack(
    alignment: Alignment.center,
    children: [
      // 外围扩散渐隐圈
      ScaleTransition(
        scale: Tween<double>(begin: 1.0, end: 2.0).animate(
          CurvedAnimation(parent: _controller, curve: Curves.easeOut),
        ),
        child: Opacity(
          opacity: Tween<double>(begin: 0.5, end: 0.0).animate(_controller).value,
          child: Container(
            width: widget.size,
            height: widget.size,
            decoration: BoxDecoration(
              color: widget.color,
              shape: BoxShape.circle,
            ),
          ),
        ),
      ),
      // 中心缩放Marker
      ScaleTransition(
        scale: _scaleAnimation,
        child: Container(
          width: widget.size,
          height: widget.size,
          decoration: BoxDecoration(
            color: widget.color,
            shape: BoxShape.circle,
            boxShadow: [
              BoxShadow(
                color: widget.color.withOpacity(0.5),
                blurRadius: 8,
                spreadRadius: 2,
              )
            ],
          ),
          child: const Center(
            child: Icon(
              Icons.location_on,
              color: Colors.white,
              size: 20,
            ),
          ),
        ),
      ),
    ],
  );
}

3. 在Google Map中使用动画Marker

将自定义的动画Widget转换为BitmapDescriptor,再赋值给Marker的icon属性:

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

  @override
  State<MapScreen> createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  late GoogleMapController _mapController;
  BitmapDescriptor? _animatedMarkerIcon;

  // 初始地图位置(示例:旧金山)
  final CameraPosition _initialCamera = const CameraPosition(
    target: LatLng(37.7749, -122.4194),
    zoom: 14,
  );

  @override
  void initState() {
    super.initState();
    _createAnimatedMarker();
  }

  // 异步生成动画Marker的BitmapDescriptor
  Future<void> _createAnimatedMarker() async {
    _animatedMarkerIcon = await BitmapDescriptor.fromWidget(
      const AnimatedMarker(color: Colors.blue),
    );
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GoogleMap(
        initialCameraPosition: _initialCamera,
        onMapCreated: (controller) => _mapController = controller,
        markers: _animatedMarkerIcon != null
            ? {
                Marker(
                  markerId: const MarkerId('animated_marker'),
                  position: const LatLng(37.7749, -122.4194),
                  icon: _animatedMarkerIcon!,
                )
              }
            : {},
      ),
    );
  }
}

关键注意事项

  • BitmapDescriptor.fromWidget是异步方法,必须等待初始化完成后再渲染Marker。
  • 若不需要循环动画,可去掉_controller.repeat(reverse: true),改用_controller.forward()在需要时触发单次动画。
  • 大量动画Marker可能影响性能,建议复用AnimationController或根据可视范围动态创建/销毁动画。

内容的提问来源于stack exchange,提问作者abdul

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 08:25:28