Flutter自定义Google Map标记弹窗卡片,替代默认MarkerInfoWindow
解决Google Maps自定义标记弹窗居中显示的问题
嘿,我看你已经搞定了带图片和按钮的自定义卡片,现在就差把它从屏幕底部移到中部,像默认的MarkerInfoWindow那样显示对吧?咱们来一步步调整代码:
1. 调整卡片定位,从底部移到屏幕中部
原来的卡片用Positioned(bottom: 0)固定在底部,咱们换成Align组件实现居中定位,视觉上更贴近默认InfoWindow的位置:
把原来底部的Positioned代码替换成:
Align( alignment: Alignment.center, child: Container( height: 200.0, width: MediaQuery.of(context).size.width * 0.9, // 缩小宽度更贴合InfoWindow的紧凑感 child: PageView.builder( controller: _pageController, itemCount: camDetails.length, itemBuilder: (BuildContext context, int index) { return _coffeeShopList(index); }, ), ), )
2. 给Marker绑定点击事件,点击时切换对应卡片
原来的Marker没有关联卡片切换逻辑,咱们在创建Marker时添加onTap回调,点击标记直接跳转到对应的卡片页面:
修改initState里的Marker创建代码:
@override void initState() { super.initState(); // 用asMap获取索引,方便绑定点击事件 camDetails.asMap().forEach((index, element) { allMarkers.add(Marker( markerId: MarkerId(element.camTitle), draggable: false, infoWindow: InfoWindow.noTitle(), // 隐藏默认InfoWindow,用自定义卡片替代 position: element.locationCoords, onTap: () { _pageController?.animateToPage( index, duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, ); })); }); _pageController = PageController(initialPage: 1, viewportFraction: 0.9) // 调整viewportFraction让卡片间距更合理 ..addListener(_onScroll); }
3. 添加InfoWindow风格的小三角(可选)
如果想要更还原默认InfoWindow的样式,可以给卡片加一个指向Marker的小三角,在_coffeeShopList的Stack里添加如下代码:
// 放在Center(child: Container(...))的后面 Positioned( bottom: -10, // 让三角刚好卡在卡片下方 left: 0, right: 0, child: Center( child: Transform.rotate( angle: 45 * pi / 180, // 旋转45度形成三角形 child: Container( width: 20, height: 20, color: Colors.white, boxShadow: const [ BoxShadow( color: Colors.black54, offset: Offset(0.0, 4.0), blurRadius: 10.0, ), ], ), ), ), )
完整修改后的代码
import 'dart:math'; import 'package:earth_cam/model/cams.dart'; import 'package:earth_cam/splash/splash_screen.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class MapView extends StatefulWidget { @override _MapViewState createState() => _MapViewState(); } class _MapViewState extends State<MapView> { GoogleMapController? _controller; List<Marker> allMarkers = []; PageController? _pageController; int? prevPage; bool isClicked = false; @override void initState() { super.initState(); // 绑定Marker点击切换卡片事件 camDetails.asMap().forEach((index, element) { allMarkers.add(Marker( markerId: MarkerId(element.camTitle), draggable: false, infoWindow: InfoWindow.noTitle(), // 隐藏默认InfoWindow position: element.locationCoords, onTap: () { _pageController?.animateToPage( index, duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, ); })); }); _pageController = PageController(initialPage: 1, viewportFraction: 0.9) ..addListener(_onScroll); } void _onScroll() { if (_pageController?.page?.toInt() != prevPage) { prevPage = _pageController?.page?.toInt(); moveCamera(); } } _coffeeShopList(index) { return AnimatedBuilder( animation: _pageController!, builder: (BuildContext context, Widget? widget) { double value = 1; if (_pageController!.position.haveDimensions) { value = _pageController!.page! - index; value = (1 - (value.abs() * 0.3) + 0.06).clamp(0.0, 1.0); } return Center( child: SizedBox( height: Curves.easeInOut.transform(value) * 125.0, width: Curves.easeInOut.transform(value) * 350.0, child: widget, ), ); }, child: InkWell( onTap: () { moveCamera(); }, child: Stack(children: [ Center( child: Container( margin: const EdgeInsets.symmetric( horizontal: 10.0, vertical: 20.0, ), height: 200.0, width: 275.0, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.0), boxShadow: const [ BoxShadow( color: Colors.black54, offset: Offset(0.0, 4.0), blurRadius: 10.0, ), ]), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.0), color: Colors.white), child: Row(children: [ Container( height: 90.0, width: 90.0, decoration: BoxDecoration( borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(10.0), topLeft: Radius.circular(10.0)), image: DecorationImage( image: NetworkImage( camDetails[index].thumbNail), fit: BoxFit.cover))), const SizedBox(width: 5.0), Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( camDetails[index].camTitle, style: const TextStyle( fontSize: 12.5, fontWeight: FontWeight.bold), ), Text( camDetails[index].address, style: const TextStyle( fontSize: 12.0, fontWeight: FontWeight.w600), ), Container( width: 150, child: RaisedButton.icon( elevation: 12, shape: RoundedRectangleBorder( borderRadius: new BorderRadius.circular( 15), ), onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (context)=>const SplashScreen())); }, icon: const Icon( Icons.play_arrow, color: Colors.white, ), label: const Text( 'Play', style: TextStyle(color: Colors.white), ), color: Colors.redAccent, ), ) ]) ]))), ), // 添加InfoWindow风格的底部三角 Positioned( bottom: -10, left: 0, right: 0, child: Center( child: Transform.rotate( angle: 45 * pi / 180, child: Container( width: 20, height: 20, color: Colors.white, boxShadow: const [ BoxShadow( color: Colors.black54, offset: Offset(0.0, 4.0), blurRadius: 10.0, ), ], ), ), ), ) ])), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF1B2D45), appBar: AppBar( centerTitle: true, title: const Text('All Live Cams'), leading: IconButton( icon: const Icon( Icons.arrow_back, color: Colors.white, ), onPressed: null), actions: [ IconButton( icon: const Icon( Icons.do_not_disturb_off, color: Colors.white, size: 25, ), onPressed: null), ], ), body: Stack( children: <Widget>[ Container( height: MediaQuery.of(context).size.height - 50.0, width: MediaQuery.of(context).size.width, child: GoogleMap( compassEnabled: true, mapToolbarEnabled: false, zoomControlsEnabled: false, myLocationEnabled: true, initialCameraPosition: const CameraPosition( target: LatLng(40.7128, -74.0060), zoom: 12.0), markers: Set.from(allMarkers), onMapCreated: mapCreated, ), ), // 自定义卡片居中显示 Align( alignment: Alignment.center, child: Container( height: 200.0, width: MediaQuery.of(context).size.width * 0.9, child: PageView.builder( controller: _pageController!, itemCount: camDetails.length, itemBuilder: (BuildContext context, int index) { return _coffeeShopList(index); }, ), ), ), isClicked ? Container( padding: const EdgeInsets.only(top: 30), alignment: Alignment.topRight, child: RaisedButton.icon( elevation: 20, shape: RoundedRectangleBorder( borderRadius: new BorderRadius.only( bottomLeft: Radius.circular(20), topLeft: Radius.circular(20)), ), onPressed: () { print('YT cam Clicked'); setState(() { isClicked = false; }); }, icon: const Icon( Icons.play_arrow, color: Colors.white, ), label: const Text( 'YT Cam', style: TextStyle(color: Colors.white), ), color: Colors.redAccent, ), ) : Container( padding: const EdgeInsets.only(top: 30), alignment: Alignment.topRight, child: RaisedButton.icon( elevation: 20, shape: RoundedRectangleBorder( borderRadius: new BorderRadius.only( bottomLeft: Radius.circular(20), topLeft: Radius.circular(20)), ), onPressed: () { print('IP cam Clicked'); setState(() { isClicked = true; }); }, icon: const Icon( Icons.linked_camera, color: Colors.white, ), label: const Text( 'IP Cam', style: TextStyle(color: Colors.white), ), color: Colors.indigo, ), ) ], ), ); } void mapCreated(controller) { setState(() { _controller = controller; }); } moveCamera() { final pageIndex = _pageController?.page?.toInt() ?? 0; _controller?.animateCamera(CameraUpdate.newCameraPosition(CameraPosition( target: camDetails[pageIndex].locationCoords, zoom: 14.0, bearing: 45.0, tilt: 45.0))); } }
这样修改后,你的自定义卡片就会显示在屏幕中部,点击Marker还能自动切换到对应的卡片,同时保留了原来的滑动切换和相机移动逻辑。如果觉得居中位置不合适,还可以调整Align的alignment参数,比如用Alignment(0, -0.2)让它稍微靠上一点。
内容的提问来源于stack exchange,提问作者Al Mamun
相关产品推荐
相关产品推荐

