Related
I want to create a moving point that moves across an image. The picture is drawn on canvas. The point should approach the specified x and y coordinates.Image and point are generated. The point should move to the new specified position every 500ms by a timer.But it only moves when I move the mouse over buttons on the app.
class five_og extends StatefulWidget {
#override
State createState() {
return _five_og();
}
}
class _five_og extends State<five_og> {
ui.Image? image;
Timer? timer;
void refreshImage() {
if (i >= xList.length - 1) {
i = 0;
}
ImagePainter(image!);
i++;
}
#override
void dispose() {
super.dispose();
timer!.cancel();
}
#override
void initState() {
super.initState();
loadImage('assets/images/Erdgeschoss.png');
timer = Timer.periodic(
Duration(milliseconds: 500),
(timer) {
refreshImage();
},
);
}
Future loadImage(String path) async {
final data = await rootBundle.load(path);
final bytes = data.buffer.asUint8List();
final image = await decodeImageFromList(bytes);
setState(() {
this.image = image;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test'),
),
body: Center(
child: image == null
? CircularProgressIndicator()
: Container(
height: 600,
width: 600,
child: FittedBox(
child: SizedBox(
width: image!.width.toDouble(),
height: image!.width.toDouble(),
child: CustomPaint(
painter: ImagePainter(image!),
),
),
)
)
),
);
}
}
class ImagePainter extends CustomPainter {
final ui.Image image;
const ImagePainter(this.image);
#override
void paint(Canvas canvas, Size size) {
//final paint = Paint();
canvas.drawImage(image, Offset.zero, Paint());
final paint = Paint()
..color = Color.fromARGB(255, 15, 182, 253)
..style = PaintingStyle.fill;
canvas.drawCircle(Offset(xList[i], yList[i]), 13, paint);
}
#override
bool shouldRepaint(ImagePainter oldDelegate) {
return false;
}
}
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.
Here is the code of Customized Three Containers using ClipPath for obtaining alphabet "A". This Code can also draw inside these three containers. But the drawing can be done in any direction. I want to make this code into drawing from top to bottom inside first container ,when the first drawing done then the second container is activated, after drawing of second Container then the third one to be activated (Tracing Alphabet). I think there should be 3 pairs of Draggable and DragTarget. Drawing is done by using GestureDetector. Is there is any way draw line using Draggable widget, Please respond anyone konws the solution
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setEnabledSystemUIOverlays ([]);
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight,DeviceOrientation.landscapeLeft]).then((_){
runApp(MyApp());
});
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Offset> points1 = [];
List<Offset> points2 = [];
List<Offset> points3 = [];
#override
Widget build(BuildContext context) {
final double width = MediaQuery.of(context).size.width;
final double height = MediaQuery.of(context).size.height;
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: (){
points1.clear();
points2.clear();
points3.clear();
},
),
backgroundColor: Colors.teal,
body: SafeArea(
child:Center(
child: Stack(
children: [
Positioned(
left: MediaQuery.of(context).size.width*0.381,
top:MediaQuery.of(context).size.height*0.560,
child: ClipPath(
clipper: CPath3(),
child: Container(
height: height*0.07,
width: width*0.18,
child: GestureDetector(
onPanStart: (details){
this.setState(() {
points3.add(details.localPosition);
});
},
onPanUpdate: (details){
this.setState(() {
points3.add(details.localPosition);
});
},
onPanEnd: (details){
this.setState(() {
points3.add(null);
});
},
child: CustomPaint(
painter: MyPainter3(points3: points3),
),
),
),
),
),//THIRD PATH
Positioned(
left: MediaQuery.of(context).size.width*0.283,
// bottom: 0,
top: MediaQuery.of(context).size.height*0.194,
//right: 0,
child: ClipPath(
clipper: CPath1(),
child: Container(
height: height*0.60,
width: width*0.20,
child: GestureDetector(
onPanStart: (details){
this.setState(() {
points1.add(details.localPosition);
});
},
onPanUpdate: (details){
this.setState(() {
points1.add(details.localPosition);
});
},
onPanEnd: (details){
this.setState(() {
points1.add(null);
});
},
child: CustomPaint(
painter: MyPainter1(points1: points1),
),
),
),
),
),//FIRST PATH
Positioned(
left: MediaQuery.of(context).size.width*0.467,
// bottom: 0,
top: MediaQuery.of(context).size.height*0.194,
//right: 0,
child: ClipPath(
clipper: CPath2(),
child: Container(
height: height*0.60,
width: width*0.20,
child: GestureDetector(
onPanStart: (details){
this.setState(() {
points2.add(details.localPosition);
});
},
onPanUpdate: (details){
this.setState(() {
points2.add(details.localPosition);
});
},
onPanEnd: (details){
this.setState(() {
points2.add(null);
});
},
child: CustomPaint(
painter: MyPainter2(points2: points2),
),
),
),
),
),//SECOND PATH
],
),
),
),
);
}
}
class CPath1 extends CustomClipper<Path> {
#override
Path getClip(Size size) {
var path_0 = Path();
path_0.moveTo(0,size.height);
path_0.lineTo(size.width*0.2983628,size.height);
path_0.lineTo(size.width,0);
path_0.lineTo(size.width*0.7591676,0);
path_0.lineTo(0,size.height);
path_0.close();
return path_0;
}
#override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
// TODO: implement shouldReclip
return true;
}
}
class MyPainter1 extends CustomPainter {
List<Offset> points1;
MyPainter1({this.points1});
#override
void paint(Canvas canvas, Size size) {
Paint background = Paint()..color = Colors.white;
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRect(rect, background);
Paint paint = Paint();
paint.color = Colors.deepPurpleAccent;
paint.strokeWidth = 70;
paint.isAntiAlias = true;
paint.strokeCap = StrokeCap.round;
for(int x=0;x<points1.length-1;x++)
{
if(points1[x] != null && points1[x+1] != null)
{
canvas.drawLine(points1[x], points1[x+1], paint);
}
else if(points1[x] != null && points1[x+1] == null)
{
canvas.drawPoints(ui.PointMode.points,[points1[x]], paint);
}
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
class CPath2 extends CustomClipper<Path> {
#override
Path getClip(Size size) {
var path_0 = Path();
path_0.moveTo(size.width*0.6738603,size.height);
path_0.lineTo(size.width,size.height);
path_0.lineTo(size.width*0.1934179,0);
path_0.lineTo(size.width*0.08001079,0);
path_0.lineTo(0,size.height*0.1078064);
path_0.lineTo(size.width*0.6738603,size.height);
path_0.close();
return path_0;
}
#override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
// TODO: implement shouldReclip
return true;
}
}
class MyPainter2 extends CustomPainter {
List<Offset> points2;
MyPainter2({this.points2});
#override
void paint(Canvas canvas, Size size) {
Paint background = Paint()..color = Colors.white;
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRect(rect, background);
Paint paint = Paint();
paint.color = Colors.deepPurpleAccent;
paint.strokeWidth = 70;
paint.isAntiAlias = true;
paint.strokeCap = StrokeCap.round;
for(int x=0;x<points2.length-1;x++)
{
if(points2[x] != null && points2[x+1] != null)
{
canvas.drawLine(points2[x], points2[x+1], paint);
}
else if(points2[x] != null && points2[x+1] == null)
{
canvas.drawPoints(ui.PointMode.points,[points2[x]], paint);
}
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
class CPath3 extends CustomClipper<Path> {
#override
Path getClip(Size size) {
var path_0 = Path();
path_0.moveTo(size.width*0.9108546,0);
path_0.lineTo(size.width*0.08746101,0);
path_0.lineTo(0,size.height);
path_0.lineTo(size.width,size.height*0.9771505);
path_0.lineTo(size.width*0.9108546,0);
path_0.close();
return path_0;
}
#override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
// TODO: implement shouldReclip
return true;
}
}
class MyPainter3 extends CustomPainter {
List<Offset> points3;
MyPainter3({this.points3});
#override
void paint(Canvas canvas, Size size) {
Paint background = Paint()..color = Colors.white;
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRect(rect, background);
Paint paint = Paint();
paint.color = Colors.deepPurpleAccent;
paint.strokeWidth = 70;
paint.isAntiAlias = true;
paint.strokeCap = StrokeCap.round;
for(int x=0;x<points3.length-1;x++)
{
if(points3[x] != null && points3[x+1] != null)
{
canvas.drawLine(points3[x], points3[x+1], paint);
}
else if(points3[x] != null && points3[x+1] == null)
{
canvas.drawPoints(ui.PointMode.points,[points3[x]], paint);
}
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
Hello can anyone tell me why the background to this masking attempt is black. This must be close but I just can't kill the background. I've seen others reference that saveLayer(rect, paint) is the key here as that shoves the whole canvas rect in to the masking operation. This question (no masking operation) and this one (no actual answer) are similar but were no use to me.
main.dart
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ui.Image mask;
ui.Image image;
#override
void initState() {
super.initState();
load('images/squircle.png').then((i) {
setState(() {
mask = i;
});
});
load('images/noodlejpg.jpg').then((i) {
setState(() {
image = i;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Colors.blue, title: Text('I am a title')),
body: SafeArea(
child: SizedBox(
width: 200.0,
height: 200.0,
child: CustomPaint(painter: OverlayPainter(mask, image)),
),
),
);
}
Future<ui.Image> load(String asset) async {
ByteData data = await rootBundle.load(asset);
ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
ui.FrameInfo fi = await codec.getNextFrame();
return fi.image;
}
}
class OverlayPainter extends CustomPainter {
ui.Image mask;
ui.Image image;
OverlayPainter(this.mask, this.image);
#override
void paint(Canvas canvas, Size size) {
var paint = new Paint();
paint.isAntiAlias = true;
if (image != null) {
var rect = Rect.fromLTRB(0, 0, 200, 200);
Size outputSize = rect.size;
Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
final FittedSizes fittedSizes =
applyBoxFit(BoxFit.cover, inputSize, outputSize);
final Size sourceSize = fittedSizes.source;
canvas.save();
final Rect sourceRect = Alignment.center.inscribe(
sourceSize,
Offset.zero & inputSize,
);
canvas.drawImageRect(image, sourceRect, rect, paint);
canvas.restore();
}
if (mask != null) {
var rect = Rect.fromLTRB(0, 0, 200, 200);
Size outputSize = rect.size;
Size inputSize = Size(mask.width.toDouble(), mask.height.toDouble());
final FittedSizes fittedSizes =
applyBoxFit(BoxFit.cover, inputSize, outputSize);
final Size sourceSize = fittedSizes.source;
canvas.saveLayer(rect, Paint()..blendMode = BlendMode.dstIn);
final Rect sourceRect = Alignment.center.inscribe(
sourceSize,
Offset.zero & inputSize,
);
canvas.drawImageRect(mask, sourceRect, rect, paint);
canvas.restore();
}
}
#override
bool shouldRepaint(OverlayPainter oldDelegate) {
return mask != oldDelegate.mask || image != oldDelegate.image;
}
}
noodlejpg.jpg
squircle.jpg
result
The key is in when to call saveLayer and when to call restore.
From here:
When using Canvas.saveLayer and Canvas.restore, the blend mode of the Paint given to the Canvas.saveLayer will be applied when Canvas.restore is called. Each call to Canvas.saveLayer introduces a new layer onto which shapes and images are painted; when Canvas.restore is called, that layer is then composited onto the parent layer, with the source being the most-recently-drawn shapes and images, and the destination being the parent layer. (For the first Canvas.saveLayer call, the parent layer is the canvas itself.)
Working code
#override
void paint(Canvas canvas, Size size) {
if (image != null && mask != null) {
var rect = Rect.fromLTRB(0, 0, 200, 200);
Size outputSize = rect.size;
Paint paint = new Paint();
//Mask
Size maskInputSize = Size(mask.width.toDouble(), mask.height.toDouble());
final FittedSizes maskFittedSizes =
applyBoxFit(BoxFit.cover, maskInputSize, outputSize);
final Size maskSourceSize = maskFittedSizes.source;
final Rect maskSourceRect = Alignment.center
.inscribe(maskSourceSize, Offset.zero & maskInputSize);
canvas.saveLayer(rect, paint);
canvas.drawImageRect(mask, maskSourceRect, rect, paint);
//Image
Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
final FittedSizes fittedSizes =
applyBoxFit(BoxFit.cover, inputSize, outputSize);
final Size sourceSize = fittedSizes.source;
final Rect sourceRect =
Alignment.center.inscribe(sourceSize, Offset.zero & inputSize);
canvas.drawImageRect(
image, sourceRect, rect, paint..blendMode = BlendMode.srcIn);
canvas.restore();
}
}
Result:
as an option without any CustomPainters
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: SafeArea(
child: Center(
child: SizedBox(
width: 300,
height: 300,
child: MaskedImage(asset: 'images/noodlejpg.jpeg', mask: 'images/circle.png'),
),
),
),
),
);
}
}
class MaskedImage extends StatelessWidget {
final String asset;
final String mask;
MaskedImage({#required this.asset, #required this.mask});
#override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return FutureBuilder<List>(
future: _createShaderAndImage(asset, mask, constraints.maxWidth, constraints.maxHeight),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox.shrink();
return ShaderMask(
blendMode: BlendMode.dstATop,
shaderCallback: (rect) => snapshot.data[0],
child: snapshot.data[1],
);
},
);
});
}
Future<List> _createShaderAndImage(String asset, String mask, double w, double h) async {
ByteData data = await rootBundle.load(asset);
ByteData maskData = await rootBundle.load(mask);
Codec codec = await instantiateImageCodec(maskData.buffer.asUint8List(), targetWidth: w.toInt(), targetHeight: h.toInt());
FrameInfo fi = await codec.getNextFrame();
ImageShader shader = ImageShader(fi.image, TileMode.clamp, TileMode.clamp, Matrix4.identity().storage);
Image image = Image.memory(data.buffer.asUint8List(), fit: BoxFit.cover, width: w, height: h);
return [shader, image];
}
}
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!