I am trying to add multiple Rectangles in the Canvas and rotate them with user pan action. But the Constructor I found till now for Rect is all to draw them without Rotation. and I found a method canvas.rotate() which will rotate the whole canvas.
How to achieve this? Any code where rotation of the Rectangle is dealt with user pan action without using canvas.rotate() will be helpful.
The solution is simple as #pskink answered in the comment above.
There is only canvas.rotate() and canvas.transform() to rotate anything in the flutter canvas and there is canvas.scale() to scale them.
now if you want to rotate one object 120, and another 40 degrees you need to draw them inside a canvas.save() ... canvas.restore() block. then your objects will be rotated at a different angles. look at the below code for example:
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:ui' as ui;
const kCanvasSize = 300.0;
class ImageInsideRectPage extends StatefulWidget {
const ImageInsideRectPage({Key? key}) : super(key: key);
#override
_ImageInsideRectPageState createState() => _ImageInsideRectPageState();
}
class _ImageInsideRectPageState extends State<ImageInsideRectPage> {
ui.Image? image;
#override
void initState() {
_load('assets/img.png');
super.initState();
}
void _load(String path) async {
var bytes = await rootBundle.load(path);
image = await decodeImageFromList(bytes.buffer.asUint8List());
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.greenAccent, width: 2)),
height: kCanvasSize,
width: kCanvasSize,
child: CustomPaint(
painter: ImageInsideRectangle(context: context, image: image),
child: SizedBox.expand(),
),
),
),
);
}
}
class ImageInsideRectangle extends CustomPainter {
ImageInsideRectangle({required this.context, required this.image});
ui.Image? image;
final BuildContext context;
#override
void paint(Canvas canvas, Size size) async {
canvas.clipRRect(ui.RRect.fromRectXY(
Rect.fromPoints(Offset(0, 0), Offset(kCanvasSize - 4, kCanvasSize - 4)),
0,
0,
));
Paint greenBrush = Paint()..color = Colors.greenAccent;
if (image != null) {
canvas.save();
rotate(
canvas: canvas,
cx: image!.width.toDouble() / 2,
cy: image!.height.toDouble() / 2,
angle: -0.3);
canvas.scale(kCanvasSize / image!.height);
canvas.drawImage(image!, Offset(0, 0), greenBrush);
canvas.restore();
}
canvas.save();
rotate(canvas: canvas, cx: 200 + 50, cy: 100 + 50, angle: 0.5);
canvas.drawRect(Rect.fromLTWH(200, 100, 100, 100), greenBrush);
canvas.restore();
}
void rotate(
{required Canvas canvas,
required double cx,
required double cy,
required double angle}) {
canvas.translate(cx, cy);
canvas.rotate(angle);
canvas.translate(-cx, -cy);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Future<ui.Image> loadUiImage(String imageAssetPath) async {
final ByteData data = await rootBundle.load(imageAssetPath);
final Completer<ui.Image> completer = Completer();
ui.decodeImageFromList(Uint8List.view(data.buffer), (ui.Image img) {
return completer.complete(img);
});
return completer.future;
}
This way you can rotate multiple objects in multiple directions. also, there is an example of loading an image from local asset and rotating it around its own center.
I'm looking for a solution to draw a Flutter Widget tree in a separate graphic pipeline that will never be displayed on the screen. I want to be able to define the width/height constraints to any size regardless of the display.
The goal is to get a png image of this Widget that is not dependent on the device, to send it to a printer. I know it's possible to use RepaintBundary to build an image but that's only for something displayed on the screen.
I tested something like this, with no luck:
Future<void> capture(BuildContext context, Widget widget) async {
final rect = Rect.fromLTWH(0, 0, 200, 200);
final root = OffscreenPainter(
size: Size(200, 200),
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 200, height: 200),
child: widget,
));
final ro = root.createRenderObject(context);
final el = root.createElement();
// el.activate();
el.attachRenderObject(ro);
final o = PipelineOwner();
// ro.attach(o);
o.rootNode = ro;
ro.scheduleInitialLayout();
final rootLayer = OffsetLayer();
rootLayer.attach(o);
ro.scheduleInitialPaint(rootLayer);
el.updateChildren();
ro.layout(BoxConstraints(maxWidth: rect.width, maxHeight: rect.height));
o.flushLayout();
o.flushCompositingBits();
o.flushPaint();
final im = await ro.toImage();
final bd = await im.toByteData(format: ImageByteFormat.png);
final f = File('image.png');
f.writeAsBytesSync(bd.buffer.asUint8List());
print('saved to ${f.absolute}');
}
class OffscreenPainter extends SingleChildRenderObjectWidget {
/// Creates a widget that isolates repaints.
const OffscreenPainter({Key key, Widget child, #required this.size})
: super(key: key, child: child);
final Size size;
#override
RenderOffscreenPainter createRenderObject(BuildContext context) =>
RenderOffscreenPainter(size);
}
class RenderOffscreenPainter extends RenderRepaintBoundary {
RenderOffscreenPainter(this._size);
final Size _size;
#override
void performLayout() {
if (child != null) {
child.layout(BoxConstraints.tight(_size));
size = child.size;
} else {
size = _size;
}
}
}
You can use the Transform Widget to push your widget off-screen. According to the docs, Transform.translate moves the widget before it's painted.
In this example, I added an Icon to the bottom navigation bar and shifted it down. It doesn't show but I can still access the RepaintBoundary.
Transform.translate(
offset: Offset(0,200),
child: RepaintBoundary(
key: iconKey,
child: IconButton(icon: Icon(Icons.star),
onPressed: () {
// Do something
}),
),
),
I am beginner in Flutter and I am trying to figure how I can detect touch enter, move and exit when a user runs their finger across a custom shape and/or across multiple stacked custom shapes. Something like below
Ideally I would like to get touch events when users enter/exit pixel boundaries of each custom shape, but I wanted to get it working at-least with a MBR of the shapes. Below is the code that I have. What am I doing wrong? All it seems to do is print move when touch begins within the shapes. I have tried GestureDetector too with similar results.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.deepOrange,
),
home: MyHomePage(title: 'Flutter Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Stack(
children: <Widget>[
Listener(
onPointerSignal: (PointerEvent details) {
print("Signal yellow");
},
onPointerMove: (PointerEvent details) {
print("Move yellow");
},
onPointerHover: (PointerEvent details) {
print("Hover yellow");
},
onPointerEnter: (PointerEvent details) {
print("Enter yellow");
},
onPointerExit: (PointerEvent details) {
print("Exit yellow");
},
child: CustomPaint(
painter: ShapesPainter(),
child: Container(
height: 400,
width: 400,
),
),
),
Listener(
onPointerEnter: (PointerEvent details) {
print("Enter red");
},
onPointerExit: (PointerEvent details) {
print("Exit red");
},
child: CustomPaint(
painter: ShapesPainter1(),
child: Container(
height: 200,
width: 200,
),
),
),
],
),
],
),
),
);
}
}
class ShapesPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint();
// set the color property of the paint
paint.color = Colors.yellow;
// center of the canvas is (x,y) => (width/2, height/2)
var center = Offset(size.width / 2, size.height / 2);
// draw the circle on centre of canvas having radius 75.0
canvas.drawCircle(center, size.width / 2, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
#override
bool hitTest(Offset position) {
// TODO: implement hitTest
return super.hitTest(position);
}
}
class ShapesPainter1 extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint();
// set the color property of the paint
paint.color = Colors.red;
// center of the canvas is (x,y) => (width/2, height/2)
var center = Offset(size.width / 2, size.height / 2);
// draw the circle on centre of canvas having radius 75.0
canvas.drawCircle(center, size.width / 2, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
#override
bool hitTest(Offset position) {
// TODO: implement hitTest
return super.hitTest(position);
}
}
That's because you are using one Listener per CustomPainter, you should use just one Listener for all your Stack.
And if you want to know if the current touch event is inside each Circle , you could use GlobalKeys to get the RenderBox for each Circle, then you have the renderBox, and the PointerEvent, you can easily check the HitTest, check the code:
class _MyHomePageState extends State<MyHomePage> {
GlobalKey _keyYellow = GlobalKey();
GlobalKey _keyRed = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text("title"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Listener(
onPointerMove: (PointerEvent details) {
final RenderBox box = _keyRed.currentContext.findRenderObject();
final RenderBox boxYellow =
_keyYellow.currentContext.findRenderObject();
final result = BoxHitTestResult();
Offset localRed = box.globalToLocal(details.position);
Offset localYellow = boxYellow.globalToLocal(details.position);
if (box.hitTest(result, position: localRed)) {
print("HIT...RED ");
} else if (boxYellow.hitTest(result, position: localYellow)) {
print("HIT...YELLOW ");
}
},
child: Stack(
children: <Widget>[
CustomPaint(
key: _keyYellow,
painter: ShapesPainter(),
child: Container(
height: 400,
width: 400,
),
),
CustomPaint(
key: _keyRed,
painter: ShapesPainter1(),
child: Container(
height: 200,
width: 200,
),
),
],
),
),
],
),
),
);
}
}
Also I modified the hitTest method of your CustomPainters to ignore the touchs outside the circle.
class ShapesPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint();
// set the color property of the paint
paint.color = Colors.yellow;
// center of the canvas is (x,y) => (width/2, height/2)
final center = Offset(size.width / 2, size.height / 2);
// draw the circle on centre of canvas having radius 75.0
canvas.drawCircle(center, size.width / 2, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
#override
bool hitTest(Offset position) {
final Offset center = Offset(200, 200);
Path path = Path();
path.addRRect(RRect.fromRectAndRadius(
Rect.fromCenter(center: center, width: 400, height: 400),
Radius.circular(center.dx)));
path.close();
return path.contains(position);
}
}
class ShapesPainter1 extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint();
// set the color property of the paint
paint.color = Colors.red;
// center of the canvas is (x,y) => (width/2, height/2)
var center = Offset(size.width / 2, size.height / 2);
// draw the circle on centre of canvas having radius 75.0
canvas.drawCircle(center, size.width / 2, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
#override
bool hitTest(Offset position) {
final Offset center = Offset(100, 100);
Path path = Path();
path.addRRect(RRect.fromRectAndRadius(
Rect.fromCenter(center: center, width: 200, height: 200),
Radius.circular(center.dx)));
path.close();
return path.contains(position);
}
}
I have developed a library called
touchable for the purpose of adding gesture callbacks to each individual shape you draw on the canvas.
Here's what you can do to detect touch and drag on your circle.
Just Wrap your CustomPaint widget with CanvasTouchDetector. It takes a builder function as argument that expects your CustomPaint widget as shown below.
import 'package:touchable/touchable.dart';
CanvasTouchDetector(
builder: (context) =>
CustomPaint(
painter: MyPainter(context)
)
)
Inside your CustomPainter class's paint method , create and use the TouchyCanvas object (using the context obtained from the CanvasTouchDetector and canvas) to draw your shape and you can give gesture callbacks like onPanUpdate , onTapDown here to detect your drag events.
var myCanvas = TouchyCanvas(context,canvas);
myCanvas.drawRect( rect , Paint() , onPanUpdate: (detail){
//This callback runs when you drag this rectangle. Details of the location can be got from the detail object.
//Do stuff here. Probably change your state and animate
});
I am working on Google Map Markers in Flutter.
On the click of each Marker, I want to show a Custom Info Window which can include a button, image etc. But in Flutter there is a property TextInfoWindow which only accept String.
How can i achieve adding buttons, images to the map marker's InfoWindow.
Stumbled across this problem and found a solution which works for me:
To solve it I did write a Custom Info Widget, feel free to customize it. For example with some shadow via ClipShadowPath.
Implementation
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'custom_info_widget.dart';
void main() => runApp(MyApp());
class PointObject {
final Widget child;
final LatLng location;
PointObject({this.child, this.location});
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: "/",
routes: {
"/": (context) => HomePage(),
},
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
PointObject point = PointObject(
child: Text('Lorem Ipsum'),
location: LatLng(47.6, 8.8796),
);
StreamSubscription _mapIdleSubscription;
InfoWidgetRoute _infoWidgetRoute;
GoogleMapController _mapController;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.green,
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: const LatLng(47.6, 8.6796),
zoom: 10,
),
circles: Set<Circle>()
..add(Circle(
circleId: CircleId('hi2'),
center: LatLng(47.6, 8.8796),
radius: 50,
strokeWidth: 10,
strokeColor: Colors.black,
)),
markers: Set<Marker>()
..add(Marker(
markerId: MarkerId(point.location.latitude.toString() +
point.location.longitude.toString()),
position: point.location,
onTap: () => _onTap(point),
)),
onMapCreated: (mapController) {
_mapController = mapController;
},
/// This fakes the onMapIdle, as the googleMaps on Map Idle does not always work
/// (see: https://github.com/flutter/flutter/issues/37682)
/// When the Map Idles and a _infoWidgetRoute exists, it gets displayed.
onCameraMove: (newPosition) {
_mapIdleSubscription?.cancel();
_mapIdleSubscription = Future.delayed(Duration(milliseconds: 150))
.asStream()
.listen((_) {
if (_infoWidgetRoute != null) {
Navigator.of(context, rootNavigator: true)
.push(_infoWidgetRoute)
.then<void>(
(newValue) {
_infoWidgetRoute = null;
},
);
}
});
},
),
),
);
}
/// now my _onTap Method. First it creates the Info Widget Route and then
/// animates the Camera twice:
/// First to a place near the marker, then to the marker.
/// This is done to ensure that onCameraMove is always called
_onTap(PointObject point) async {
final RenderBox renderBox = context.findRenderObject();
Rect _itemRect = renderBox.localToGlobal(Offset.zero) & renderBox.size;
_infoWidgetRoute = InfoWidgetRoute(
child: point.child,
buildContext: context,
textStyle: const TextStyle(
fontSize: 14,
color: Colors.black,
),
mapsWidgetSize: _itemRect,
);
await _mapController.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(
point.location.latitude - 0.0001,
point.location.longitude,
),
zoom: 15,
),
),
);
await _mapController.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(
point.location.latitude,
point.location.longitude,
),
zoom: 15,
),
),
);
}
}
CustomInfoWidget:
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:meta/meta.dart';
class _InfoWidgetRouteLayout<T> extends SingleChildLayoutDelegate {
final Rect mapsWidgetSize;
final double width;
final double height;
_InfoWidgetRouteLayout(
{#required this.mapsWidgetSize,
#required this.height,
#required this.width});
/// Depending of the size of the marker or the widget, the offset in y direction has to be adjusted;
/// If the appear to be of different size, the commented code can be uncommented and
/// adjusted to get the right position of the Widget.
/// Or better: Adjust the marker size based on the device pixel ratio!!!!)
#override
Offset getPositionForChild(Size size, Size childSize) {
// if (Platform.isIOS) {
return Offset(
mapsWidgetSize.center.dx - childSize.width / 2,
mapsWidgetSize.center.dy - childSize.height - 50,
);
// } else {
// return Offset(
// mapsWidgetSize.center.dx - childSize.width / 2,
// mapsWidgetSize.center.dy - childSize.height - 10,
// );
// }
}
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
//we expand the layout to our predefined sizes
return BoxConstraints.expand(width: width, height: height);
}
#override
bool shouldRelayout(_InfoWidgetRouteLayout oldDelegate) {
return mapsWidgetSize != oldDelegate.mapsWidgetSize;
}
}
class InfoWidgetRoute extends PopupRoute {
final Widget child;
final double width;
final double height;
final BuildContext buildContext;
final TextStyle textStyle;
final Rect mapsWidgetSize;
InfoWidgetRoute({
#required this.child,
#required this.buildContext,
#required this.textStyle,
#required this.mapsWidgetSize,
this.width = 150,
this.height = 50,
this.barrierLabel,
});
#override
Duration get transitionDuration => Duration(milliseconds: 100);
#override
bool get barrierDismissible => true;
#override
Color get barrierColor => null;
#override
final String barrierLabel;
#override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return MediaQuery.removePadding(
context: context,
removeBottom: true,
removeLeft: true,
removeRight: true,
removeTop: true,
child: Builder(builder: (BuildContext context) {
return CustomSingleChildLayout(
delegate: _InfoWidgetRouteLayout(
mapsWidgetSize: mapsWidgetSize, width: width, height: height),
child: InfoWidgetPopUp(
infoWidgetRoute: this,
),
);
}),
);
}
}
class InfoWidgetPopUp extends StatefulWidget {
const InfoWidgetPopUp({
Key key,
#required this.infoWidgetRoute,
}) : assert(infoWidgetRoute != null),
super(key: key);
final InfoWidgetRoute infoWidgetRoute;
#override
_InfoWidgetPopUpState createState() => _InfoWidgetPopUpState();
}
class _InfoWidgetPopUpState extends State<InfoWidgetPopUp> {
CurvedAnimation _fadeOpacity;
#override
void initState() {
super.initState();
_fadeOpacity = CurvedAnimation(
parent: widget.infoWidgetRoute.animation,
curve: Curves.easeIn,
reverseCurve: Curves.easeOut,
);
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeOpacity,
child: Material(
type: MaterialType.transparency,
textStyle: widget.infoWidgetRoute.textStyle,
child: ClipPath(
clipper: _InfoWidgetClipper(),
child: Container(
color: Colors.white,
padding: EdgeInsets.only(bottom: 10),
child: Center(child: widget.infoWidgetRoute.child),
),
),
),
);
}
}
class _InfoWidgetClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
Path path = Path();
path.lineTo(0.0, size.height - 20);
path.quadraticBezierTo(0.0, size.height - 10, 10.0, size.height - 10);
path.lineTo(size.width / 2 - 10, size.height - 10);
path.lineTo(size.width / 2, size.height);
path.lineTo(size.width / 2 + 10, size.height - 10);
path.lineTo(size.width - 10, size.height - 10);
path.quadraticBezierTo(
size.width, size.height - 10, size.width, size.height - 20);
path.lineTo(size.width, 10.0);
path.quadraticBezierTo(size.width, 0.0, size.width - 10.0, 0.0);
path.lineTo(10, 0.0);
path.quadraticBezierTo(0.0, 0.0, 0.0, 10);
path.close();
return path;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
I stumbled across the same problem just today, I couldn't get a multiline string to show properly in TextInfoWindow. I ended up circumventing the problem by implementing a modal bottom sheet (https://docs.flutter.io/flutter/material/showModalBottomSheet.html) that shows when you click on a marker, which in my case worked out quite nicely.
I can also imagine many use cases where you'd want to fully customize the marker's info window, but reading this issue on GitHub (https://github.com/flutter/flutter/issues/23938) it looks like it's currently not possible, because the InfoWindow is not a Flutter widget.
You can display marker made of widgets as custom 'info window'. Basically you are creating png image of your widget and displaying it as a marker.
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class MarkerInfo extends StatefulWidget {
final Function getBitmapImage;
final String text;
MarkerInfo({Key key, this.getBitmapImage, this.text}) : super(key: key);
#override
_MarkerInfoState createState() => _MarkerInfoState();
}
class _MarkerInfoState extends State<MarkerInfo> {
final markerKey = GlobalKey();
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => getUint8List(markerKey)
.then((markerBitmap) => widget.getBitmapImage(markerBitmap)));
}
Future<Uint8List> getUint8List(GlobalKey markerKey) async {
RenderRepaintBoundary boundary =
markerKey.currentContext.findRenderObject();
var image = await boundary.toImage(pixelRatio: 2.0);
ByteData byteData = await image.toByteData(format: ImageByteFormat.png);
return byteData.buffer.asUint8List();
}
#override
Widget build(BuildContext context) {
return RepaintBoundary(
key: markerKey,
child: Container(
padding: EdgeInsets.only(bottom: 29),
child: Container(
width: 100,
height: 100,
color: Color(0xFF000000),
child: Text(
widget.text,
style: TextStyle(
color: Color(0xFFFFFFFF),
),
),
),
),
);
}
}
If you use this approach you have to make sure you render the widget, because this will not work otherwise. For converting widgets to images - widget has to be rendered in order to convert it. I'm hiding my widget under the map in Stack.
return Stack(
children: <Widget>[
MarkerInfo(
text: tripMinutes.toString(),
getBitmapImage: (img) {
customMarkerInfo = img;
}),
GoogleMap(
markers: markers,
...
Last step is to create a Marker. Data passed from the widget is saved in customMarkerInfo - bytes, so convert it to Bitmap.
markers.add(
Marker(
position: position,
icon: BitmapDescriptor.fromBytes(customMarkerInfo),
markerId: MarkerId('MarkerID'),
),
);
Example
Here’s a solution to create custom marker that doesn’t rely on InfoWindow. Although, this approch won’t allow you to add a button on custom marker.
Flutter google maps plugin lets us use image data / asset to create a custom marker. So, this approach uses drawing on Canvas to create a custom marker and using PictureRecorder to convert the same to a picture, which later on would be used by google maps plugin to render a custom marker.
Sample code to draw on Canvas and convert the same to Image data that can be used by the plugin.
void paintTappedImage() async {
final ui.PictureRecorder recorder = ui.PictureRecorder();
final Canvas canvas = Canvas(recorder, Rect.fromPoints(const Offset(0.0, 0.0), const Offset(200.0, 200.0)));
final Paint paint = Paint()
..color = Colors.black.withOpacity(1)
..style = PaintingStyle.fill;
canvas.drawRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(0.0, 0.0, 152.0, 48.0), const Radius.circular(4.0)),
paint);
paintText(canvas);
paintImage(labelIcon, const Rect.fromLTWH(8, 8, 32.0, 32.0), canvas, paint,
BoxFit.contain);
paintImage(markerImage, const Rect.fromLTWH(24.0, 48.0, 110.0, 110.0), canvas,
paint, BoxFit.contain);
final Picture picture = recorder.endRecording();
final img = await picture.toImage(200, 200);
final pngByteData = await img.toByteData(format: ImageByteFormat.png);
setState(() {
_customMarkerIcon = BitmapDescriptor.fromBytes(Uint8List.view(pngByteData.buffer));
});
}
void paintText(Canvas canvas) {
final textStyle = TextStyle(
color: Colors.white,
fontSize: 24,
);
final textSpan = TextSpan(
text: '18 mins',
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
textPainter.layout(
minWidth: 0,
maxWidth: 88,
);
final offset = Offset(48, 8);
textPainter.paint(canvas, offset);
}
void paintImage(
ui.Image image, Rect outputRect, Canvas canvas, Paint paint, BoxFit fit) {
final Size imageSize =
Size(image.width.toDouble(), image.height.toDouble());
final FittedSizes sizes = applyBoxFit(fit, imageSize, outputRect.size);
final Rect inputSubrect =
Alignment.center.inscribe(sizes.source, Offset.zero & imageSize);
final Rect outputSubrect =
Alignment.center.inscribe(sizes.destination, outputRect);
canvas.drawImageRect(image, inputSubrect, outputSubrect, paint);
}
once the marker is tapped, we can replace the tapped image with the new image generated from Canvas. Sample code for the same taken from google maps plugin example app.
void _onMarkerTapped(MarkerId markerId) async {
final Marker tappedMarker = markers[markerId];
if (tappedMarker != null) {
if (markers.containsKey(selectedMarker)) {
final Marker resetOld =
markers[selectedMarker].copyWith(iconParam: _markerIconUntapped);
setState(() {
markers[selectedMarker] = resetOld;
});
}
Marker newMarker;
selectedMarker = markerId;
newMarker = tappedMarker.copyWith(iconParam: _customMarkerIcon);
setState(() {
markers[markerId] = newMarker;
});
tappedCount++;
}
}
Reference:
How to convert a flutter canvas to Image.
Flutter plugin example app.
Bellow is 4 step I had implemented for custom InfoWindow on my project
Step 1: Create a stack for GoogleMap and Info Window Custom.
Stack(
children: <Widget>[
Positioned.fill(child: GoogleMap(...),),
Positioned(
top: {offsetY},
left: {offsetX},
child: YourCustomInfoWidget(...),
)
]
)
Step 2: When user click Marker calculator position of marker on screen with func:
screenCoordinate = await _mapController.getScreenCoordinate(currentPosition.target)
Step 3: Calculator offsetY, offsetX and setState.
Relate issue: https://github.com/flutter/flutter/issues/41653
devicePixelRatio = Platform.isAndroid ? MediaQuery.of(context).devicePixelRatio : 1.0;
offsetY = (screenCoordinate?.y?.toDouble() ?? 0) / devicePixelRatio - infoWidget.size.width;
offsetX = (screenCoordinate?.x?.toDouble() ?? 0) / devicePixelRatio - infoWidget.size.height;
Step 4: Disable Marker auto move camera when tap
Marker(
...
consumeTapEvents: true,)
To create a widget-based info window you need to stack the widget on google map. With the help of ChangeNotifierProvider, ChangeNotifier, and Consumer you can easily rebuild your widget even when the camera moves on google map.
InfoWindowModel class:
class InfoWindowModel extends ChangeNotifier {
bool _showInfoWindow = false;
bool _tempHidden = false;
User _user;
double _leftMargin;
double _topMargin;
void rebuildInfoWindow() {
notifyListeners();
}
void updateUser(User user) {
_user = user;
}
void updateVisibility(bool visibility) {
_showInfoWindow = visibility;
}
void updateInfoWindow(
BuildContext context,
GoogleMapController controller,
LatLng location,
double infoWindowWidth,
double markerOffset,
) async {
ScreenCoordinate screenCoordinate =
await controller.getScreenCoordinate(location);
double devicePixelRatio =
Platform.isAndroid ? MediaQuery.of(context).devicePixelRatio : 1.0;
double left = (screenCoordinate.x.toDouble() / devicePixelRatio) -
(infoWindowWidth / 2);
double top =
(screenCoordinate.y.toDouble() / devicePixelRatio) - markerOffset;
if (left < 0 || top < 0) {
_tempHidden = true;
} else {
_tempHidden = false;
_leftMargin = left;
_topMargin = top;
}
}
bool get showInfoWindow =>
(_showInfoWindow == true && _tempHidden == false) ? true : false;
double get leftMargin => _leftMargin;
double get topMargin => _topMargin;
User get user => _user;
}
Complete Example is available on my blog!