setState not really settng the state [flutter] - flutter

So I have this code that is supposed to draw to the canvas.
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Home(),
);
}
}
// offsets
List<Offset?> offsets = [];
class Home extends StatefulWidget {
const Home({
Key? key,
}) : super(key: key);
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
// offsets
List<Offset?> offsets = [];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Drawing'),
),
body: GestureDetector(
onPanDown: (details) {
setState(() {
offsets.add(details.localPosition);
});
},
onPanUpdate: (details) {
setState(() {
offsets.add(details.localPosition);
});
},
onPanEnd: (details) {
setState(() {
offsets.add(null);
});
},
child: Container(
width: double.infinity,
height: MediaQuery.of(context).size.height,
color: Colors.grey,
child: CustomPaint(
foregroundPainter: Painter(offsets: offsets),
),
),
),
);
}
}
class Painter extends CustomPainter {
Painter({required this.offsets});
List<Offset?> offsets;
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..strokeWidth = 2
..style = PaintingStyle.stroke;
// for every offset and the next offset
for (int i = 0; i < offsets.length - 1; i++) {
// if both are not null
if (offsets[i] != null && offsets[i + 1] != null) {
canvas.drawLine(offsets[i]!, offsets[i + 1]!, paint);
} else if (offsets[i] != null && offsets[i + 1] == null) {
canvas.drawPoints(PointMode.points, [offsets[i]!], paint);
}
}
print('paint called');
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
How this is working is that it detects the gestures on the container and when there is a swipe on the container, it records the offsets of that and adds them to the offsets array. Later that array is being passed to the custom painter where we go through all the offsets and render the lines. So when I add the offsets to the array I use setState so that the new offsets are added, the screen is rendered again, new offsets are passed to the custom painter and it renders those lines. But it's not working when I make a swipe but the lines appear when I make a hot reload. Why is this? Why I need to manually hot reload to see those lines? Any help will be appreciated.
Edit: It seems like the offsets are being added properly and the setState is also working fine but somehow the custom painter is not being called.

NVM, returning true from shouldRepaint method solve the issue.
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}

Related

Is the build function creating new widgets to render or reusing?

I have a working snippet that I've wrote, but I kinda don't understand how flutter is (re)using the widgets creating in the build method:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 50.0;
static const squareHeight = 50.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<Offset> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
for(int i = 0; i < 20; i++) {
offsets.add(calculateNextOffset());
}
}
List<Widget> squareWidgets = [];
for (int j = 0; j < offsets.length; j++) {
squareWidgets.add(AnimatedPositioned(
left: offsets[j].dx,
top: offsets[j].dy,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
offsets.removeAt(j);
for (int k = 0; k < offsets.length; k++) {
offsets[k] = calculateNextOffset();
}
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color: Colors.blue,
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
Every time I click on a container, i expect my "offsets" state to be updated, but I also expect all the AnimationPositioned widgets, GestureDetector widgets and the square widgets that you see would be rerendered.
With rerendered i mean they would disappear from the screen and new ones would be rerendered (and the animation from the first widgets would be cancelled and never displayed)
However it works? Could someone explain this to me?
EDIT: I've updated my snippet of code in my question to match what i'm asking, which i'm also going to rephrase here:
Every time I click on a square, i want that square to disappear and all the other square to randomly animate to another position. But every time I click on a square, another random square is deleted, and the one i'm clicking is animating.
I want the square that I click on disappears and the rest will animate.
Following up from the comments - Actually, the square you click is disappears. However, to see this visually add this to your container color:
color:Colors.primaries[Random().nextInt(Colors.primaries.length)],
Now, your offsets are generating just fine and random. However, because you have an AnimatedContainer widget. This widget will remember the last x & y position of your square and animate the new square starting from that old x,y value to the new on you passed it. So if you really want the square you click on disappear - you will need to either use Positioned widget:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 100.0;
static const squareHeight = 100.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<Offset> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
offsets.add(calculateNextOffset());
}
print(offsets);
List<Widget> squareWidgets = [];
for (var offset in offsets) {
squareWidgets.add(Positioned(
left: offset.dx,
top: offset.dy,
//curve: Curves.easeIn,
//duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
for (var i = 0; i < offsets.length; i++) {
offsets[i] = calculateNextOffset();
}
offsets.add(calculateNextOffset());
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color:Colors.primaries[Random().nextInt(Colors.primaries.length)],
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
If you want the rest of the squares to animate while only the one that is clicked disappears. You will need to rethink your implementation and track all square perhaps using unique keys and custom animations. Hope that helps!
I've finally found it:
In the context of the snippet inside the question: Every time you click on a square, it will correctly remove that item, but the widgets are rerendered from that new list, and the last widget that was previously rendered will be removed instead of the one that I clicked.
This has to do because every widget in the widget tree is rendered as an element inside the element tree. If the state of the element inside the element tree is the same, it will not rerender that one. and they are all just blue squares in the end, so there is no distinction.
You can find a very nice video made by the flutter devs here:
When to Use Keys - Flutter Widgets 101 Ep. 4
Long story short: Here is the snippet with the fix, which is to add a Key to each widget, then the state will change on the element inside the element tree and it will rerender (and remove) the correct widgets/elements:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 50.0;
static const squareHeight = 50.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<OffsetData> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
for(int i = 0; i < 20; i++) {
offsets.add(OffsetData(UniqueKey(), calculateNextOffset()));
}
}
List<Widget> squareWidgets = [];
for (int j = 0; j < offsets.length; j++) {
squareWidgets.add(AnimatedPositioned(
key: offsets[j].key, // This line is the trick
left: offsets[j].offset.dx,
top: offsets[j].offset.dy,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
offsets.removeAt(j);
for (var offsetData in offsets) {
offsetData.offset = calculateNextOffset();
}
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color: Colors.blue,
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
class OffsetData {
Offset offset;
final Key key;
OffsetData(this.key, this.offset);
}

How to recognize diagonal swipes in flutter?

I attempted to detect diagonal swipe direction using GestureDetector. I used the onPanStart method to detect the screen's corners.
However, it is not perfect because it only considers the beginning of the gesture.
Right now, if I swipe bottom left to top right in the top right corner or part, it gives me the incorrect top right direction. In this case, the correct direction should be bottom left.
So, how can we identify diagonal swipe direction using GestureDetector or any other method?
I looked at several other StackOverflow posts, but they only detect top, left, right, and bottom directions.
Here is an example of code.
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
String? swipeDirection;
return Scaffold(
body: SafeArea(
child: Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
child: Container(),
onPanStart: (details) {
final halfScreenWidth = screenSize.width / 2;
final halfScreenHeight = screenSize.height / 2;
final position = details.localPosition;
if ((position.dx < halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeDirection = 'topLeft';
} else if ((position.dx > halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeDirection = 'topRight';
} else if ((position.dx < halfScreenWidth) &&
(position.dy > halfScreenHeight)) {
swipeDirection = 'bottomLeft';
} else {
swipeDirection = 'bottomRight';
}
},
),
),
));
}
}
You can use pan update to find the position of the pointer the same manner you did for pan start and get the starting and ending points. Then on pan end execute with the final results
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHome(),
);
}
}
class MyHome extends StatefulWidget {
const MyHome({Key? key}) : super(key: key);
#override
State<MyHome> createState() => _MyHomeState();
}
class _MyHomeState extends State<MyHome> {
String? swipeStart;
String? swipeEnd;
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return SafeArea(
child: Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.grey,
),
onPanStart: (details) {
final halfScreenWidth = screenSize.width / 2;
final halfScreenHeight = screenSize.height / 2;
final position = details.localPosition;
if ((position.dx < halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeStart = 'topLeft';
} else if ((position.dx > halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeStart = 'topRight';
} else if ((position.dx < halfScreenWidth) &&
(position.dy > halfScreenHeight)) {
swipeStart = 'bottomLeft';
} else {
swipeStart = 'bottomRight';
}
// print(swipeStart);
},
onPanUpdate: (details) {
final halfScreenWidth = screenSize.width / 2;
final halfScreenHeight = screenSize.height / 2;
final position = details.localPosition;
if ((position.dx < halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeEnd = 'topLeft';
} else if ((position.dx > halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeEnd = 'topRight';
} else if ((position.dx < halfScreenWidth) &&
(position.dy > halfScreenHeight)) {
swipeEnd = 'bottomLeft';
} else {
swipeEnd = 'bottomRight';
}
//print(swipeEnd);
},
onPanEnd: (details) {
print("Final direction was from $swipeStart to $swipeEnd");
},
),
),
);
}
}
I have printed output like this
flutter: Final direction was from topLeft to bottomRight
flutter: Final direction was from bottomLeft to topRight
EDIT
You can find the offset when the pan starts and updates. Then on pan end calculate the direction like this
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHome(),
);
}
}
class MyHome extends StatefulWidget {
const MyHome({Key? key}) : super(key: key);
#override
State<MyHome> createState() => _MyHomeState();
}
class _MyHomeState extends State<MyHome> {
String? horizontalDirection;
String? verticalDirection;
late Offset startPoint;
late Offset endPoint;
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.grey,
),
onPanStart: (details) {
startPoint = details.localPosition;
},
onPanUpdate: (details) {
endPoint = details.localPosition;
},
onPanEnd: (details) {
if (startPoint.dx < endPoint.dx) {
horizontalDirection = "right";
} else {
horizontalDirection = "left";
}
if (startPoint.dy < endPoint.dy) {
verticalDirection = "bottom";
} else {
verticalDirection = "top";
}
print("Final direction was $horizontalDirection$verticalDirection");
},
),
),
);
}
}
Looking into the GestureDetector widget there isn't any kind of feature your'e searching for. Also there is no other widget in my mind which is able to do that.
Maybe you can try to add the following:
Save the position of the onPanStart of one of the directions. Add onPanEnd and do the exact same you did for onPanStart and also save these positions. Now add onPanUpdate and with this you will compare the first and last point which was recognized. If you are working with screensizes as I can see, you can divide your screen in 4 blocks (top, left, right, bottom) and with the informations of the first and last position you can detect the diagonal swipe

How to avoid child rebuilding if parent updates

I have a strange requirement. I saw this this question on SO but i can't make it work for my case.
I have an animated container which represent my screen. On pressing the ADD icon. I'm transforming the screen like this (The column in right side image is below the homescreen) . But inside that AnimatedContainer there is a LIST(as child).
Every time I do transfromation. The list is re building itself. Is there any way i can avoid it. ?
You can imagine that the homescreen is pinned to wall with two nails in the left and right top. As I press FAB. The left top nail is pulled out and the screen hangs on right nail support. and when i again press FAB, the left top is again pinned with nail.
This is the widget I'm using
https://pub.dev/packages/matrix4_transform
Here is minimal code to see the rebuilding
import 'package:flutter/material.dart';
import 'package:matrix4_transform/matrix4_transform.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key? key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
_MyStatefulWidgetState? home;
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
DrawerManager drawerManager = DrawerManager();
callSetState() {
setState(() {});
}
#override
Widget build(BuildContext context) {
print('Rebuild');
home = this;
return AnimatedContainer(
transform: Matrix4Transform()
.translate(x: drawerManager.xOffSet, y: drawerManager.yOffSet)
.rotate(drawerManager.angle)
.matrix4,
duration: Duration(milliseconds: 500),
child: Scaffold(
body: MyList(drawerManager),
),
);
}
}
class MyList extends StatelessWidget {
final Data myData = Data();
final DrawerManager drawerManager;
MyList(this.drawerManager);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: myData.data.length+1,
itemBuilder: (context, index) {
print('Building list' + index.toString());
if(index == 4){
return GestureDetector(
child: IconButton(
icon: Icon(Icons.add),
onPressed: () {
drawerManager.callback(drawerManager.isOpened);
}),
);
}
else{return ListTile(
title: Text(myData.data[index]),
);}
},
),
);
}
}
class Data {
List<String> data = ['Hello1', 'Hello2', 'Hello3', 'Hello4'];
}
class DrawerManager {
double xOffSet = 0;
double yOffSet = 0;
double angle = 0;
bool isOpened = false;
void callback(bool isOpen) {
print('Tapped');
if (isOpen) {
xOffSet = 0;
yOffSet = 0;
angle = 0;
isOpened = false;
} else {
xOffSet = 150;
yOffSet = 80;
angle = -0.2;
isOpened = true;
}
callSetState();
}
void callSetState() {
home!.callSetState();
}
}
You can see that When you press that + icon. Screen transforms and the lists are rebuilding.
please use this class ,
https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html
Performance optimizations
If your builder function contains a subtree that does not depend on
the animation, it's more efficient to build that subtree once instead
of rebuilding it on every animation tick.
If you pass the pre-built subtree as the child parameter, the
AnimatedBuilder will pass it back to your builder function so that you
can incorporate it into your build.
You can easily achieve this using provider library.
Give it try 👍
I will recommend to use Getx if you r beginner and if
have experience in app development then I will
Recommend to use Bloc library check both of them on
Pub.

Flutter keep background canvas drawing to skip repaint when only foreground changes

I am working on a project that has a CustomPaint that draws shapes as a background, and when you tap the screen, it draws a circle on that specific position. I am using GestureDetector to get the tap data and send it as an argument to _MyCustomPainter Something similar to the code below:
class MyApp extends StatefulWidget{
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Offset _circlePosition = Offset(-1, -1);
void setCirclePosition(Offset newPosition) {
setState(() {
this._circlePosition = newPosition;
});
}
void clearCircle() {
setState(() {
this._circlePosition = Offset(-1, -1);
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.green,
title: Text('My app'),
),
body: Container(
child: GestureDetector(
onTapDown: (detail) {
setCirclePosition(detail.localPosition);
},
onHorizontalDragStart: (detail) {
setCirclePosition(detail.localPosition);
},
onHorizontalDragUpdate: (detail) {
setCirclePosition(detail.localPosition);
},
onVerticalDragStart: (detail) {
setCirclePosition(detail.localPosition);
},
onVerticalDragUpdate: (detail) {
setCirclePosition(detail.localPosition);
},
onTapUp: (detail) {
clearCircle();
},
onVerticalDragEnd: (detail) {
clearCircle();
},
onHorizontalDragEnd: (detail) {
clearCircle();
},
child: LimitedBox(
maxHeight: 400,
maxWidth: 300,
child: CustomPaint(
size: Size.infinite,
painter: new _MyCustomPainter(
circlePosition: circlePosition,
),
),
),
),
),
),
);
}
}
class _MyCustomPainter extends CustomPainter {
_MyCustomPainter({
this.circlePosition,
});
final Offset circlePosition;
void _drawBackground(Canvas canvas) {
// draw the background
}
#override
void paint(Canvas canvas, Size size) {
_drawBackground(canvas);
if (circlePosition.dx != -1 && circlePosition.dy != -1) {
// draws a circle on the position
var circlePaint = Paint()..color = Colors.green;
canvas.drawCircle(circlePosition, 5, circlePaint);
}
}
#override
bool shouldRepaint(_MyCustomPainter old) {
return circlePosition.dx != old.circlePosition.dx ||
circlePosition.dy != old.circlePosition.dy;
}
}
So the thing here is, every time the user moves the finger on the screen with a long tap the background that doesn't change is being repainted over and over, and there is a lot of code involved in painting this specific background. There is the shouldRepaint method but it signals to repaint the whole widget. How could I make so that the background is only drawn once, and than on repaint I just use the background I created previously? Is using a PictureRecorder to generate an image and than using that image on future repaints the best way to do this? and also should my CustomPainter extend ChangeNotifier for better performance? The current approach works, but I am wondering how could I improve it.
So the approach I used to achieve this is having two CustomPainters, one for the background (let's call it BackgroundPainter) and one for the foregroud (like ForegroundPainter, and when the BackgroundPaiter.shouldRepaint method returns true, you draw it in a canvas and save it to an image, and use the image on the ForegroundPainter. I had to do this for this candlesticks chart widget here. A short version of the code would be something like the example bellow:
class MyWidget extends StatefulWidget {
MyWidget({
this.foregroundParam,
this.backgroundParam,
});
final String foregroundParam;
final String backgroundParam;
#override
MyWidget createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Picture backgroundPicture;
_BackgroundPainter backgroundPainter;
Size parentSize;
Size oldParentSize;
#override
Widget Build(BuildContext context) {
// We need the context size, which is only available after
// the build method finishes.
WidgetsBinding.instance.addPostFrameCallback((_) {
// this code only runs after build method is run
if (parentSize != context.size) {
setState(() {
parentSize = context.size;
});
}
});
var newBackgroundPainter = _BackgroundPainter(
backgroundParam: widget.backgroundParam,
);
// update backgroundPicture and backgroundPainter if parantSize was updated
// (like after a screen rotation) or if backgroundPainter was updated
// based on the backgroundParam
if (parentSize != null &&
(oldParentSize != null && oldParentSize != parentSize ||
backgroundPainter == null ||
newBackgroundPainter.shouldRepaint(backgroundPainter) ||
backgroundPicture == null)
) {
oldParentSize = parentSize;
backgroundPainter = newBackgroundPainter;
var recorder = PictureRecorder();
var canvas = Canvas(recorder, Rect.fromLTWH(0, 0, parentSize.width, parentSize.height));
backgroundPainter.paint(canvas, parentSize);
setState(() {
backgroundPicture = recorder.endRecording();
});
}
// if there is no backgroundPicture, this must be the first run where
// parentSize was not set yet, so return a loading screen instead, but
// this is very quick and most likely will not even be noticed. Could return
// an empty Container as well
if (backgroundPicture == null) {
return Container(
width: double.infinity,
height: double.infinity,
child: Center(
child: CircularProgressIndicator(),
),
);
}
// if everything is set, return an instance of _ForegroundPainter passing
// the backgroundPicture as parameter
return CustomPaint(
size: Size.infinite,
painter: _ForegroundPainter(
backgroundPicture: backgroundPicture,
foregroundParam: widget.foregroundParam,
),
);
}
}
class _BackgroundPainter extends CustomPainter {
_BackgroundPainter({
this.backgroundParam,
});
final String backgroundParam;
#override
void paint(Canvas canvas, Size size) {
// paint background here
}
#override
bool shouldRepaint(_BackgroundPainter oldPainter) {
return backgroundParam != oldPainter.backgroundParam;
}
}
class _ForegroundPainter extends CustomPainter {
_ForegroundPainter({
this.backgroundPicture,
this.foregroundParam,
});
final Picture backgroundPicture;
final String foregroundParam;
#override
void paint(Canvas canvas, Size size) {
canvas.drawPicture(backgroundPicture);
// paint foreground here
}
#override
bool shouldRepaint(_ForegroundPainter oldPainter) {
return foregroundParam != oldPainter.foregroundParam ||
backgroundPicture != oldPainter.backgroundPicture;
}
}
This works but quickly turns appropriate if you implement an eraser.

getting the old scroll offset using the Package flutter_webview_plugin

i am trying to integrate a Webview in a Test app using the Package flutter_webview_plugin
my goal is to hide a bottomNavigationBar when the User scroll up in the Webview and show it, when the user scroll down.
in the mentioned Package there is a listner to listen to vertical Schroll changes :
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.onScrollYChanged.listen((double offsetY) { // latest offset value in vertical scroll
// compare vertical scroll changes here with old value
});
the offsetY value, is the current value, but how can't i get the old value, to compare it with the new value ? any idea ?
ok, i have implemented a solution to this.
i have defined a variable oldOffset = 0.0 and in the method trackOffsetChange i checked if oldOffset value smaller than the currentOffset value. if it's the case then oldOffset get the value of currentOffset and with setState i rebuild the widget to hide the BottomNavBar, else show the BottomNavBar.
here is the whole code of the test App, if someone new like me, is interested to see the source code:
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.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.blue,
),
home: WebInApp(),
);
}
}
class WebInApp extends StatefulWidget {
#override
_WebInAppState createState() => _WebInAppState();
}
class _WebInAppState extends State<WebInApp> {
bool _isVisible = true;
double oldOffset = 0.0;
final flutterWebOlugin = FlutterWebviewPlugin();
void trackOffsetChange(double currentOffset) {
print('current Offset --->> $currentOffset');
print('old Offset --->> $oldOffset');
if (oldOffset < currentOffset) {
print('old Offset In -- IF --->> $oldOffset');
oldOffset = currentOffset;
setState(() {
_isVisible = false;
});
} else {
setState(() {
_isVisible = true;
});
print('old Offset In -- ESLE --->> $oldOffset');
oldOffset = currentOffset;
}
}
#override
void initState() {
super.initState();
flutterWebOlugin.onScrollYChanged.listen((double yOffset) {
trackOffsetChange(yOffset);
});
}
#override
Widget build(BuildContext context) {
return WebviewScaffold(
url: "https://play.google.com/store/apps",
// hidden: true,
appBar: AppBar(
title: Text('WebView'),
),
bottomNavigationBar: AnimatedContainer(
duration: Duration(microseconds: 300),
height: _isVisible ? 60.0 : 0.0,
child: bottomNav(),
),
);
}
}
class bottomNav extends StatelessWidget {
const bottomNav({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.forward),
title: Text('back'),
),
BottomNavigationBarItem(
icon: Icon(Icons.arrow_back),
title: Text('forward'),
),
],
);
}
}