Flutter: Hero transition + widget animation at the same time? - animation

So, I'm having a bit of an issue with Flutter in regards to a specific animation case.
Basically, what I'm trying to do is simultaneously have both a hero transition run for a route change and a custom animation on an adjacent widget.
Broken down, I have a custom InheritedWidget at my root which is fed an app state from a StatefulWidget parent. Nested within my InheritedWidget, I have a WidgetsApp and an adjacent sibling for a custom tab navigation. The tree looks something like this:
Root Widget (Stateful)
|
|__InheritedWidget
|
|__WidgetsApp (Handles routing)
|
|__Navigation Bar (Overlay)
My issue arises when I on my WidgetsApp perform a route change which uses a Hero transition. While this is happening, I'm trying to also animate the Navigation Bar to either be shown or hidden depending on what view the user is on. But, since I'm using a bool variable on my app state to either show or hide the Navigation Bar via an animation, the SetState call there 'overwrites' the hero transition since the tree is rebuilt in the process (is what I'm thinking).
My initial thought was that the InheritedWidget would catch the app state change and only rebuild the Navigation Bar via updateShouldNotify, but alas this isn't what I'm seeing as the desired effect :(
So - has anyone tried anything similar, or have an idea as to how this could be handled gracefully? :)

I have done something similar, but unfortunately my code also contains a bunch of other stuff & this is relatively convoluted to do, so I'd have to split things out to make an example which is a bit more than I can do right now. I'll explain the general concept of what I did though. There may also be better ways of doing this.
You want to write a StatefulWidget with a State that also extends NavigatorObserver (you may be able to use a stateless widget but I don't think so). I personally put this above the navigator in the tree (i.e. it builds the navigator in its' build function), but you could most likely also have it 'beside' the navigator.
Override the didPush, didRemove, didPop etc methods from NavigatorObserver. Within each of these, call a setState and save the animation & other paramters, something like this:
class NavigationFaderState extends State<NavigationFader> with NavigatorObserver {
Animation _animation;
// whatever else you need, maybe starting/finishing opacity or position etc.
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
setState(() {
_animation = route.animation;
}
route.animation.addStatusListener((status) {
if (status = AnimationStatus.completed) {
setState(() {
_animation = null;
});
}
});
}
....
}
In your build function you'll want to check the _animation and animate based on whether it exists, and any other parameters you might want to set (i.e. a flag whether to animate, and whether the is going forward or backwards could be helpful - I believe the 'pop' animation have have started at 0 and gone to 1 the same as the push one but I could be wrong).
You can then hook up this animation to however you want to animate your navigation bar, probably using an AnimatedBuilder or hooking up the animation directly, or something. If there are any specific questions about how this all works, comment and I'll add some comments etc.
Hope that helps =)
EDIT: With full code example. For the record, I don't propose that this code is all that good, or that this is something you should do. But it is a way of solving the problem. Before using it in a real app, it would be worth testing it and probably adding some assertions to check for states etc.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
PushListener listener = new PushListener();
#override
Widget build(BuildContext context) {
return new WidgetsApp(
locale: new Locale("en"),
navigatorObservers: [listener],
builder: (context, child) {
// this is here rather than outside the WidgetsApp so that it
// gets access to directionality, text styles, etc
return new Scaffold(
body: child,
bottomNavigationBar:
new ColorChangingNavigationBar(key: listener.navBarKey),
);
},
onGenerateRoute: (settings) {
switch (settings.name) {
case '/':
return new MaterialPageRoute(
settings: settings,
builder: (context) => Column(
children: <Widget>[
new Text(
"I have a green nav bar when you open me and blue when you come back"),
new RaisedButton(
onPressed: () {
Navigator.pushNamed(context, "/red");
},
child: new Text("Next"),
),
],
),
);
case '/red':
return new MaterialPageRoute(
settings: settings,
builder: (context) => Column(
children: <Widget>[
new Text("I have a red nav bar"),
new RaisedButton(
onPressed: () {
Navigator.pop(context);
},
)
],
),
);
}
},
color: Colors.blue,
);
}
}
class PushListener extends NavigatorObserver {
GlobalKey<ColorChangingNavigationBarState> navBarKey = new GlobalKey();
#override
void didPop(Route route, Route previousRoute) {
if (route is ModalRoute && navBarKey.currentState != null) {
var name = route.settings.name;
var color = name == "/" ? Colors.red.shade500 : Colors.blue.shade500;
var animation = new ReverseAnimation(route.animation);
print("Popping & changing color to: ${name == "/" ? "red" : "blue"}");
navBarKey.currentState.setAnimating(animation, color);
}
}
#override
void didPush(Route route, Route previousRoute) {
if (route is ModalRoute && navBarKey.currentState != null) {
var name = route.settings.name;
var color = name == "/" ? Colors.blue.shade500 : Colors.red.shade500;
print("Pushing & changing color to: ${name == "/" ? "red" : "blue"}");
var animation = route.animation;
navBarKey.currentState.setAnimating(animation, color);
}
}
#override
void didRemove(Route route, Route previousRoute) {
// probably don't need
}
#override
void didStartUserGesture() {
// might want to do if gestures are supported with whichever type of
// route you're using.
}
#override
void didStopUserGesture() {
// if you implement didStartUserGesture
}
}
class ColorChangingNavigationBar extends StatefulWidget {
final Color startColor;
ColorChangingNavigationBar(
{Key key, this.startColor = const Color.fromRGBO(0, 255, 0, 1.0)})
: super(key: key);
#override
State<StatefulWidget> createState() => new ColorChangingNavigationBarState();
}
class _ColorAnimationInfo {
final Animation animation;
final Tween<Color> colorTween;
final AnimationStatusListener statusListener;
_ColorAnimationInfo(this.animation, this.colorTween, this.statusListener);
}
class ColorChangingNavigationBarState
extends State<ColorChangingNavigationBar> {
#override
void initState() {
_toColor = widget.startColor;
super.initState();
}
Color _toColor;
_ColorAnimationInfo _colorAnimationInfo;
void setAnimating(Animation animation, Color to) {
var fromColor;
if (_colorAnimationInfo != null) {
fromColor = _colorAnimationInfo.colorTween
.lerp(_colorAnimationInfo.animation.value);
_colorAnimationInfo.animation
.removeStatusListener(_colorAnimationInfo.statusListener);
} else {
fromColor = _toColor;
}
var statusListener = (state) {
if (state == AnimationStatus.completed ||
state == AnimationStatus.dismissed) {
setState(() {
_colorAnimationInfo = null;
});
}
};
animation.addStatusListener(statusListener);
setState(() {
_toColor = to;
Tween<Color> colorTween = new ColorTween(begin: fromColor, end: to);
_colorAnimationInfo =
new _ColorAnimationInfo(animation, colorTween, statusListener);
});
}
#override
Widget build(BuildContext context) {
if (_colorAnimationInfo != null) {
return new AnimatedBuilder(
animation: _colorAnimationInfo.animation,
builder: (context, child) {
return new Container(
color: _colorAnimationInfo.colorTween
.lerp(_colorAnimationInfo.animation.value),
height: 30.0,
);
});
} else {
return new Container(
color: _toColor,
height: 30.0,
);
}
}
#override
void dispose() {
if (_colorAnimationInfo != null) {
_colorAnimationInfo.animation.removeStatusListener(_colorAnimationInfo.statusListener);
}
_colorAnimationInfo = null;
super.dispose();
}
}

Related

Flutter How do I change the scroll speed in ListView on the mouse wheel?

I'm a beginner. I'm writing an application on Flutter under Windows. The problem is that the text in the ListView scrolls too slowly by the mouse clip. I tried to override ScrollPhysics, but it didn't work. Please give a working way to change the scrolling speed.
For people finding this post:
Based on the accepted answer above, this custom class that can be thrown in and used throughout an application.
Customized AdjustableScrollController
// scrollcontroller.dart
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
class AdjustableScrollController extends ScrollController {
AdjustableScrollController([int extraScrollSpeed = 40]) {
super.addListener(() {
ScrollDirection scrollDirection = super.position.userScrollDirection;
if (scrollDirection != ScrollDirection.idle) {
double scrollEnd = super.offset +
(scrollDirection == ScrollDirection.reverse
? extraScrollSpeed
: -extraScrollSpeed);
scrollEnd = min(super.position.maxScrollExtent,
max(super.position.minScrollExtent, scrollEnd));
jumpTo(scrollEnd);
}
});
}
}
Usage
// your_file.dart
ListView(
controller: AdjustableScrollController() // default is 40
or
// your_file.dart
ListView(
controller: AdjustableScrollController(80) // scroll even faster
class ScrollViewTest extends StatelessWidget{
static const _extraScrollSpeed = 80; // your "extra" scroll speed
final ScrollController _scrollController = ScrollController();
// Constructor
ScrollViewTest({Key? key}) : super(key: key)
{
_scrollController.addListener(() {
ScrollDirection scrollDirection = _scrollController.position.userScrollDirection;
if (scrollDirection != ScrollDirection.idle)
{
double scrollEnd = _scrollController.offset + (scrollDirection == ScrollDirection.reverse
? _extraScrollSpeed
: -_extraScrollSpeed);
scrollEnd = min(
_scrollController.position.maxScrollExtent,
max(_scrollController.position.minScrollExtent, scrollEnd));
_scrollController.jumpTo(scrollEnd);
}
});
}
#override
Widget build(BuildContext context)
{
return SingleChildScrollView(
controller: _scrollController,
child: Container(...),
);
}
}

Weird issue with AppBar shadow while navigating

I face an issue with my AppBar shadow when I navigate to a page and then pop back to the main page.
The structure is :
HomePage : Scaffold with custom sliding AppBar and PageView as body :
- sliding AppBar is made of a Stack with some AppBar widgets as children, which appear with animation
- PageView shows the pages content, and its controller is linked to the AppBar's animation
SearchPage ==> access through tap on the FAB calling Navigator.push(context, MaterialPageRoute(builder: (context) => SearchPage()))
Here is the AppBar bug : if I go to the SearchPage and then pop back, the shadow becomes deeper each time, like if AppBar's elevation was growing... See GIF below :
This does not appear if I use standard AppBar instead of my custom sliding AppBar. Its code is here :
class MenuAppBar extends StatefulWidget implements PreferredSizeWidget
{
final PageController pageController;
final List<Widget> children;
MenuAppBar({#required this.pageController, #required this.children});
#override
Size get preferredSize => Size.fromHeight(56.0);
#override
State<StatefulWidget> createState() => _MenuAppBarState();
}
class _MenuAppBarState extends State<MenuAppBar> with SingleTickerProviderStateMixin
{
final List<Widget> sliders = new List<Widget>();
AnimationController _animationController;
#override
void initState()
{
super.initState();
this._animationController = new AnimationController(upperBound: widget.children.length.toDouble() - 1, vsync: this);
widget.pageController.addListener(_onPageSwitch);
}
void _onPageSwitch()
{
this._animationController.value = widget.pageController.page;
}
#override
Widget build(BuildContext context)
{
for(int i = 0; i < widget.children.length; i++)
{
this.sliders.add(this._buildSlider(index: i.toDouble(), child: widget.children[i]));
}
return Stack(children: this.sliders);
}
Widget _buildSlider({double index, Widget child})
{
return SlideTransition(
position: Tween<Offset>(
begin: Offset(index, 0),
end: Offset(index - 1, 0)
).animate(this._animationController),
child: child
);
}
}
Any idea of what could go wrong ? :(
Thanks !
Well, seems not very inspiring... Yet someone gave me some advice and it solved the problems, which may appear as a bug in the framework.
What solved the issue was to define a SlideTransitionWidget which manages its own Tween and receive a child Widget and an AnimationController. I think it may change the way AppBar are build taking into account animation/transition while navigating, I don't really know.

Flutter: Image not updating

Im doing a school project with flutter which has a list view of cards. The card has an image, which are stored in maps.
The map is this Map<String,Image> _imgMap, the key is the name attribute on the _objectList, which is a dynamic list. I pass the image to another class called Loader, which is a stateful widget that returns a container with circularProgress indicator while the image is loading and returns the image if finished.
ListView.builder(
itemCount: _objectList.length,
itemBuilder: (context,index){
return Card(
...
Row(
children<Widget>[
Container(child: Loader(image: _imgMap[_objectList[index].name]))
]
...
Loader Class:
class Loader extends StatefulWidget {
Image image;
Loader({Key key,this.image}) : super(key : key);
#override
_LoaderState createState() => _LoaderState(image);
}
class _LoaderState extends State<Loader> {
Image _image;
bool wait=true;
_LoaderState(this._image);
void initState(){
_image.image.resolve(ImageConfiguration()).addListener(
ImageStreamListener(
(info, call) {
setState(() {
wait = false;
});
},
),
);
}
#override
Widget build(BuildContext context) {
return wait ?CircleAvatar(
backgroundColor: Colors.transparent,
radius: 50,
child: simpleCircleLoading() //this is the circleprogress widget,
): Image(image: _image.image);
}
}
It works fine the first time, shows the correct image. But after adding a search function which lets the user search by name and then updates the list with only the matching one, the resulting image is the wrong one.
void search(String value) {
if(value.isNotEmpty){
List<dynamic> result = List<dynamic>();
_backUp.forEach((obj) { //_backup is the complete list of objects, will not change
if(obj.name.toLowerCase().contains(value.toLowerCase())) {
result.add(obj);
}
});
setState(() {
_objectList = result;
});
}else {
setState(() {
_objectList = _backUp;
});
}
}
Examples:
The initial list
Search result
As you can see the information on the card is updating but the image not.The problem is somewhere in the loader class i think since replacing the Loader in the listview with a Image widget fix the error, but i want to show a loader.
Found the error, i had to pass an UniqueKey to Loader class.

Call state class from widget in flutter

I think I have a flawed architecture but I'm struggling to see how/why. I'm very new to flutter, so bear with me please.
I have a map, and a drawer. I'm loading a list of coordinates in the drawer, and I'd like to do stuff on the map once I press one of those coordinates.
So my problem is that I don't know what to call where to be clean code AND working. Of course I could just expose everything to everyone but that wouldn't solve the main issue : me understanding
My map is drawn in the same place as my drawer, so there I already think I'm cheating but I think that's okay. To be honest I'm not even sure that part is really correct.
Drawing the map :
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
var _map = MapView();
var _stationService = StationService();
....
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.map)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike))
],
),
title: Text('Floctta Plus'),
),
drawer: _drawer(),
body: TabBarView(
physics: NeverScrollableScrollPhysics(),
children: [
IncrementView(),
_map,
Icon(Icons.directions_transit),
Icon(Icons.directions_bike)
],
),
),
),
);
}
In my drawer code :
ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, position) {
return ListTile(
title: new Text(snapshot.data[position].titleFR),
leading: new Icon(Icons.pin_drop),
onTap: () {
_map.GoToStation(); <<= Here, calling the MapView class
},
);
},
),
All the MapView.dart code
class MapView extends StatefulWidget {
MapView({Key key}) : super(key: key);
void GoToStation() {
print("I'm reaching this point with success!");
}
#override
_MapViewState createState() => _MapViewState();
}
class _MapViewState extends State<MapView> {
CameraPosition _initialPosition =
CameraPosition(target: LatLng(50.8267018, 4.3532732), zoom: 10.0);
Completer<GoogleMapController> _controller = Completer();
var _markers;
void AddMarkers(List<Station> stations) {
setState(() {
_markers = new List.generate(
stations.length,
(index) => Marker(
markerId: MarkerId(index.toString()),
position: new LatLng(double.parse(stations[index].latitude),
double.parse(stations[index].longitude)),
infoWindow: InfoWindow(
title: stations[index].titleFR,
snippet: stations[index].city,
),
icon: BitmapDescriptor.defaultMarker,
));
});
}
void _onMapCreated(GoogleMapController controller) {
_controller.complete(controller);
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: _initialPosition,
markers: _markers,
myLocationEnabled: true,
),
],
);
// );
}
}
I think there is enough code.
My question is :
Im in my Mapview class now, and I'd like to call AddMarkers that is in my State class. But I can't. I'm not really sure how I'm supposed to proceed.
Should I call the state class directly from the drawer? If so, how?
Should I call the state from the view class ? If so, how?
Should I be doing something else entirely? If so, what ?
Right!
I will assume that MapView and Drawer are in different widget subtrees under Scaffold. One is the drawer, the other appears somewhere under body.
My understanding is that when someone clicks on a ListTile in the Drawer, the MapView should update.
In general, the principle you want to employ in that case is introducing a 3rd party object to mediate between the 2 components.
AWidget <--> SState <--> MapView
The most ill-advised approach of it would be to introduce a global variable (please, don't ever do, just mentioning it so that you get the idea). So chit-chat between the ListTiles and your MapView should happen through this mediator object.
Let's call this object SState. (Double SS is on purpose to differentiate from Flutter's State class). This SState will be responsible for keeping track of the params of the map building. (I'm not actually familiar with the insights of your app, so that is completely made up)
So, this indirection of action handling will mean:
tap on ListTile in drawer -> update SState object -> let Flutter know stuff happened and should update those widgets that are sensitive to that change (MapView)
Updating SState object is easy, it is just a function call/setter call to it. But how do you let Flutter know that it should update MapView? And where should you place that SState object instance so that both can reach it without a global var?
To answer that, there are a number of solutions: scoped_model, InheritedWidget, Provider, BLoC pattern. What is common in all of this is that they employ a principle called lifting state up. Basically means, that the SState object should be created in the tree above both of them.
Provider example:
class SState with ChangeNotifier {
String _someMapParam;
String get someMapParam => _someMapParam;
set someMapParam(String val) {
_someMapParam = val;
if(hasListeners) notifyListeners();
}
}
// [...] somewhere in your app
Widget build(BuildContext context) {
return ChangeNotifierProvider<SState>(
builder: (ctx) => new SState(),
child: Scaffold(
drawer: _drawer(),
body: Consumer<SState>(
builder: (_, sStateInstance, __) => Text(sStateInstance.someMapParam),
),
),
);
}
More details on provider: https://pub.dev/packages/provider

Saving picture of Canvas with PictureRecorder results in empty image

First things first, the goal of this program is to allow user to sign officials document with a phone or tablet.
The program has to save the image as a png.
I use Flutter(and dart) and VS Code to develop this app.
What works :
-The user can draw on the canvas.
What don't works:
-the image can't be saved as a png
What i found:
-The **Picture** get by ending the **PictureRecoder** of the canvas is empty (i tried to display it but no success)
-I tried to save it as a PNG using **PictureRecorder.EndRecording().toImage(100.0,100.0).toByteDate(EncodingFormat.png())** but the size is really small, and it can't be displayed.
If some of you could give me some hint about where the problem could be, it would be really nice.
FYI : Flutter is at the latest version on the dev channel
Here's the full code :
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
///This class is use just to check if the Image returned by
///the PictureRecorder of the first Canvas is not empty.
///FYI : The image is not displayed.
class CanvasImageDraw extends CustomPainter {
ui.Picture picture;
CanvasImageDraw(this.picture);
#override
void paint(ui.Canvas canvas, ui.Size size) {
canvas.drawPicture(picture);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
///Class used to display the second canvas
class SecondCanvasView extends StatelessWidget {
ui.Picture picture;
var canvas;
var pictureRecorder= new ui.PictureRecorder();
SecondCanvasView(this.picture) {
canvas = new Canvas(pictureRecorder);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Image Debug'),
),
body: new Column(
children: <Widget>[
new Text('Top'),
CustomPaint(
painter: new CanvasImageDraw(picture),
),
new Text('Bottom'),
],
));
}
}
///This is the CustomPainter of the first Canvas which is used
///to draw to display the users draw/sign.
class SignaturePainter extends CustomPainter {
final List<Offset> points;
Canvas canvas;
ui.PictureRecorder pictureRecorder;
SignaturePainter(this.points, this.canvas, this.pictureRecorder);
void paint(canvas, Size size) {
Paint paint = new Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null)
canvas.drawLine(points[i], points[i + 1], paint);
}
}
bool shouldRepaint(SignaturePainter other) => other.points != points;
}
///Classes used to get the user draw/sign, send it to the first canvas,
///then display it.
///'points' list of position returned by the GestureDetector
class Signature extends StatefulWidget {
ui.PictureRecorder pictureRecorder = new ui.PictureRecorder();
Canvas canvas;
List<Offset> points = [];
Signature() {
canvas = new Canvas(pictureRecorder);
}
SignatureState createState() => new SignatureState(canvas, points);
}
class SignatureState extends State<Signature> {
List<Offset> points = <Offset>[];
Canvas canvas;
SignatureState(this.canvas, this.points);
Widget build(BuildContext context) {
return new Stack(
children: [
GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
setState(() {
points = new List.from(points)..add(localPosition);
});
},
onPanEnd: (DragEndDetails details) => points.add(null),
),
new Text('data'),
CustomPaint(
painter:
new SignaturePainter(points, canvas, widget.pictureRecorder)),
new Text('data')
],
);
}
}
///The main class which display the first Canvas, Drawing/Signig area
///
///Floating action button used to stop the PictureRecorder's recording,
///then send the Picture to the next view/Canvas which should display it
class DemoApp extends StatelessWidget {
Signature sign = new Signature();
Widget build(BuildContext context) => new Scaffold(
body: sign,
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.save),
onPressed: () async {
ui.Picture picture = sign.pictureRecorder.endRecording();
Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondCanvasView(picture)));
},
),
);
}
void main() => runApp(new MaterialApp(home: new DemoApp()));
First off, thanks for the interesting question and the self-contained answer. That is refreshing to see and a lot easier to help out with!
There's a few issues with your code. The first is that you shouldn't be passing the canvas & points from Signature to SignatureState; that's an antipattern in flutter.
This code here works. I've done a few little things that were unnecessary to the answer to clean it up as well.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
///This class is use just to check if the Image returned by
///the PictureRecorder of the first Canvas is not empty.
class CanvasImageDraw extends CustomPainter {
ui.Image image;
CanvasImageDraw(this.image);
#override
void paint(ui.Canvas canvas, ui.Size size) {
// simple aspect fit for the image
var hr = size.height / image.height;
var wr = size.width / image.width;
double ratio;
double translateX;
double translateY;
if (hr < wr) {
ratio = hr;
translateX = (size.width - (ratio * image.width)) / 2;
translateY = 0.0;
} else {
ratio = wr;
translateX = 0.0;
translateY = (size.height - (ratio * image.height)) / 2;
}
canvas.translate(translateX, translateY);
canvas.scale(ratio, ratio);
canvas.drawImage(image, new Offset(0.0, 0.0), new Paint());
}
#override
bool shouldRepaint(CanvasImageDraw oldDelegate) {
return image != oldDelegate.image;
}
}
///Class used to display the second canvas
class SecondView extends StatelessWidget {
ui.Image image;
var pictureRecorder = new ui.PictureRecorder();
SecondView({this.image});
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Image Debug'),
),
body: new Column(
children: <Widget>[
new Text('Top'),
CustomPaint(
painter: new CanvasImageDraw(image),
size: new Size(200.0, 200.0),
),
new Text('Bottom'),
],
));
}
}
///This is the CustomPainter of the first Canvas which is used
///to draw to display the users draw/sign.
class SignaturePainter extends CustomPainter {
final List<Offset> points;
final int revision;
SignaturePainter(this.points, [this.revision = 0]);
void paint(canvas, Size size) {
if (points.length < 2) return;
Paint paint = new Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null)
canvas.drawLine(points[i], points[i + 1], paint);
}
}
// simplified this, but if you ever modify points instead of changing length you'll
// have to revise.
bool shouldRepaint(SignaturePainter other) => other.revision != revision;
}
///Classes used to get the user draw/sign, send it to the first canvas,
///then display it.
///'points' list of position returned by the GestureDetector
class Signature extends StatefulWidget {
Signature({Key key}): super(key: key);
#override
State<StatefulWidget> createState()=> new SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset> _points = <Offset>[];
int _revision = 0;
ui.Image get rendered {
var pictureRecorder = new ui.PictureRecorder();
Canvas canvas = new Canvas(pictureRecorder);
SignaturePainter painter = new SignaturePainter(_points);
var size = context.size;
// if you pass a smaller size here, it cuts off the lines
painter.paint(canvas, size);
// if you use a smaller size for toImage, it also cuts off the lines - so I've
// done that in here as well, as this is the only place it's easy to get the width & height.
return pictureRecorder.endRecording().toImage(size.width.floor(), size.height.floor());
}
void _addToPoints(Offset position) {
_revision++;
_points.add(position);
}
Widget build(BuildContext context) {
return new Stack(
children: [
GestureDetector(
onPanStart: (DragStartDetails details) {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
setState(() {
_addToPoints(localPosition);
});
},
onPanUpdate: (DragUpdateDetails details) {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
setState(() {
_addToPoints(localPosition);
});
},
onPanEnd: (DragEndDetails details) => setState(() => _addToPoints(null)),
),
CustomPaint(painter: new SignaturePainter(_points, _revision)),
],
);
}
}
///The main class which display the first Canvas, Drawing/Signing area
///
///Floating action button used to stop the PictureRecorder's recording,
///then send the Picture to the next view/Canvas which should display it
class DemoApp extends StatelessWidget {
GlobalKey<SignatureState> signatureKey = new GlobalKey();
Widget build(BuildContext context) => new Scaffold(
body: new Signature(key: signatureKey),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.save),
onPressed: () async {
var image = signatureKey.currentState.rendered;
Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondView(image: image)));
},
),
);
}
void main() => runApp(new MaterialApp(home: new DemoApp()));
The biggest issue you had is that you were holding your own canvas objects all over the place - that's not something you should ever do really. CustomPainter provides its own canvas. When you were painting, I don't think it was actually painting to the right place.
Also, you were recreating the list each time you added a point. I'm going to assume that's because your canvas wouldn't draw otherwise due to the other.points != points. I've changed it to take a revision int which is incremented each time something is added to the list. Even better would be to use your own list subclass that did this on its own, but that's a bit much for this example. You could also just use other.points.length != points.length if you're for sure never going to modify any of the elements in the list.
There were a few things to do considering the size of the image as well. I've gone the easiest route and made it so that the canvas is just the same size as its parent, so that the rendering is easier as it can just use that same size again (and therefore renders an image the size of the phone's screen). If you didn't want to do this but rather render your own size, it can be done. You'd have to pass in something to the SignaturePainter so it would know how to scale the points so they fit within the new size (you could adapt the aspect fit code in CanvasImageDraw for that if you so wished).
If you have any other questions about what I did, feel free to ask.

Resources