CustomPainter together with Paint()..blendMode gives weird blinking effect while scrolling - flutter

After launching the application, the green circle is not visible. In order for the circle to appear, you need to scroll up. Then if you scroll down, the circle starts blinking as long as the scrolloffset is within about 20 pixels. If the scrolloffset is larger than 20 pixels, the circle disappears and appears only when the scrolloffset is 0.
If wrap the widget in RepaintBoundary, then the circle appears on any scroll and then does not disappear.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class DemoPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
canvas.save();
final path = Path()..addOval(Rect.fromCircle(center: Offset.zero, radius: 50));
final paint = Paint()..color = Colors.green;
paint.blendMode = BlendMode.dstOver;
canvas.drawPath(path, paint);
canvas.restore();
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class TestWidget extends StatelessWidget {
const TestWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return SizedBox(
height: 80,
child: Center(
child: CustomPaint(
painter: DemoPainter(),
child: Text(
"Child widget",
style: TextStyle(color: Colors.blue[200], fontWeight: FontWeight.bold, fontSize: 20),
),
),
));
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.blue),
home: Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
const TestWidget(),
const TestWidget(),
const TestWidget(),
const TestWidget(),
const RepaintBoundary(child: TestWidget()),
Container(
height: 2000,
color: Colors.red,
)
],
),
),
),
);
}
}
The link to Dartpad, but i can't reproduce the effect on the web platform.
What could be the reason for such effect and how to get rid of it?

Related

Flutter diagonal buttons

I wanted to create diagonal buttons in a Row() similar to this as shown in this image. First and last buttons has vertical ends at the end, other edges will be diagonal.
These buttons can contain "text" or "icon". It would be ideal if we can make the Elevated button to this style, so that we can use other options in the elevated buttons as well.
https://ibb.co/M7sV6zW
is this possible with Flutter ?
Yes it is possible. You can use ClipPath with CustomClipper to achieve "any" appearance.
https://api.flutter.dev/flutter/widgets/ClipPath-class.html
void main() {
runApp(const CounterApp());
}
class CounterApp extends StatelessWidget {
const CounterApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const DiagnalButtonListView(),
);
}
}
class DiagnalButtonListView extends StatelessWidget {
const DiagnalButtonListView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("diagnal buttons"),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
itemCount: 5,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => Button(
title: "$index.Button",
),
),
),
);
}
}
class Button extends StatefulWidget {
final String title;
const Button({super.key, required this.title});
#override
State<Button> createState() => _ButtonState();
}
class _ButtonState extends State<Button> {
#override
Widget build(BuildContext context) {
return Container(
height: 72,
width: 72,
child: Stack(children: [
Container(color: Colors.white),
ClipPath(
child: Container(
width: 72,
color: Colors.yellow,
child: Center(child: Text(widget.title)),
height: 32,
),
clipper: CustomClipPath(),
)
]));
}
}
class CustomClipPath extends CustomClipper<Path> {
#override
Path getClip(Size size) {
Path path = Path();
path.lineTo(0, 60);
path.lineTo(60, 60);
path.lineTo(72, 0);
path.lineTo(12, 0);
path.lineTo(0, 60);
return path;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

How to make a custom bubble shape in flutter?

I am trying to create a custom tooltip with the triangle shape on either side. I have created a bubble but how to add the triangle in there without using any library?
class SdToolTip extends StatelessWidget {
final Widget child;
final String message;
const SdToolTip({
required this.message,
required this.child,
});
#override
Widget build(BuildContext context) {
return Center(
child: Tooltip(
child: child,
message: message,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blueAccent.withOpacity(0.6),
borderRadius: BorderRadius.circular(22)),
textStyle: const TextStyle(
fontSize: 15, fontStyle: FontStyle.italic, color: Colors.white),
),
);
}
}
You can do it by CustomPainter without any library.
Example 1:
Create Custom Painter Class,
class customStyleArrow extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = Colors.white
..strokeWidth = 1
..style = PaintingStyle.fill;
final double triangleH = 10;
final double triangleW = 25.0;
final double width = size.width;
final double height = size.height;
final Path trianglePath = Path()
..moveTo(width / 2 - triangleW / 2, height)
..lineTo(width / 2, triangleH + height)
..lineTo(width / 2 + triangleW / 2, height)
..lineTo(width / 2 - triangleW / 2, height);
canvas.drawPath(trianglePath, paint);
final BorderRadius borderRadius = BorderRadius.circular(15);
final Rect rect = Rect.fromLTRB(0, 0, width, height);
final RRect outer = borderRadius.toRRect(rect);
canvas.drawRRect(outer, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Wrap your text widget with CustomPaint,
return CustomPaint(
painter: customStyleArrow(),
child: Container(
padding: EdgeInsets.only(left: 15, right: 15, bottom: 20, top: 20),
child: Text("This is the custom painter for arrow down curve",
style: TextStyle(
color: Colors.black,
)),
),
);
Example 2:
Check below example code for tooltip shapedecoration
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Customize Tooltip'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Tooltip(
child: const IconButton(
icon: Icon(Icons.info, size: 30.0),
onPressed: null,
),
message: 'Hover Icon for Tooltip...',
padding: const EdgeInsets.all(20),
showDuration: const Duration(seconds: 10),
decoration: ShapeDecoration(
color: Colors.blue,
shape: ToolTipCustomShape(),
),
textStyle: const TextStyle(color: Colors.white),
preferBelow: false,
verticalOffset: 20,
),
),
);
}
}
class ToolTipCustomShape extends ShapeBorder {
final bool usePadding;
ToolTipCustomShape({this.usePadding = true});
#override
EdgeInsetsGeometry get dimensions =>
EdgeInsets.only(bottom: usePadding ? 20 : 0);
#override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) => Path();
#override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
rect =
Rect.fromPoints(rect.topLeft, rect.bottomRight - const Offset(0, 20));
return Path()
..addRRect(
RRect.fromRectAndRadius(rect, Radius.circular(rect.height / 3)))
..moveTo(rect.bottomCenter.dx - 10, rect.bottomCenter.dy)
..relativeLineTo(10, 20)
..relativeLineTo(10, -20)
..close();
}
#override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {}
#override
ShapeBorder scale(double t) => this;
}
Wrap your widget with CustomPaint refer to this article https://medium.com/flutter-community/a-deep-dive-into-custompaint-in-flutter-47ab44e3f216 and documentation for more info, should do the trick.
Try this package https://pub.dev/packages/shape_of_view_null_safe
ShapeOfView(
shape: BubbleShape(
position: BubblePosition.Bottom,
arrowPositionPercent: 0.5,
borderRadius: 20,
arrowHeight: 10,
arrowWidth: 10
),
//Your Data goes here
child: ...,
)

Flutter: CustomPainter paint method gets called several times instead of only once

I have a simple app that draws via a CustomPainter a red or green circle on a canvas, depending on which button is pressed in the AppBar:
The class ColorCircle extends CustomPainter and is responsible for drawing the colored circle:
class ColorCircle extends CustomPainter {
MaterialColor myColor;
ColorCircle({#required this.myColor});
#override
void paint(Canvas canvas, Size size) {
debugPrint('ColorCircle.paint, ${DateTime.now()}');
final paint = Paint()..color = myColor;
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 100, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
The drawing of the different colors works fine, but when I click (only once!) or hover over one of the buttons, the paint method gets called several times:
Further implementation details:
I use a StatefulWidget for storing the actualColor. In the build method actualColor is passed to the ColorCircle constructor:
class _MyHomePageState extends State<MyHomePage> {
MaterialColor actualColor = Colors.red;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
OutlinedButton(
onPressed: () => setState(() => actualColor = Colors.red),
child: Text('RedCircle'),
),
OutlinedButton(
onPressed: () => setState(() => actualColor = Colors.green),
child: Text('GreenCircle'),
),
],
),
body: Center(
child: CustomPaint(
size: Size(300, 300),
painter: ColorCircle(myColor: actualColor),
),
),
);
}
}
The complete source code with a running example can be found here: CustonPainter Demo
So why is paint called several times instead of only once? (And how could you implement it so that paint is called only once?).
All you need to do is to warp the CustomPaint with RepaintBoundary
Center(
child: RepaintBoundary(
child: CustomPaint(
size: Size(300, 300),
painter: ColorCircle(myColor: actualColor),
),
),
By default CustomPainter is in the same layer as every other widget on the same screen so it's paint method will get called if any other widget on the same screen repaint.
To fix this we can isolate the CustomPainter with RepaintBoundary so any repainting outside this RepaintBoundary wont effect it, or we can fix it by warping other widgets that would repaint with RepaintBoundary so they won't effect any other widgets (including the CustomPainter widget) when they get repaint, however it's better to just warp the CustomPainter with the RepaintBoundary instead of warping multiple widgets with RepaintBoundary since it's costly and sometimes have no effect.
You can get a better view and understanding of this by enabling Highlight repaints in the DevTools.
A poor solution might be to add a RepaintBoundary around the hover Widgets:
class _MyHomePageState extends State<MyHomePage> {
MaterialColor actualColor = Colors.red;
#override
Widget build(BuildContext context) {
print('Rebuilding with $actualColor');
return Scaffold(
appBar: AppBar(
title: Text('CustomPainter Demo'),
actions: <Widget>[
RepaintBoundary(
child: OutlinedButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(Colors.black)),
onPressed: () {
setState(() => actualColor = Colors.red);
},
child: Text('RedCircle')),
),
RepaintBoundary(
child: OutlinedButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(Colors.black)),
onPressed: () {
setState(() => actualColor = Colors.green);
},
child: Text('GreenCircle')),
),
],
),
body: Center(
child: CustomPaint(
size: Size(300, 300),
painter: ColorCircle(myColor: actualColor),
),
),
);
}
}
And then, to properly define the shouldRepaint method of the ColorCircle (currently returning false):
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return (oldDelegate as ColorCircle).myColor != myColor;
}
This seems to be a really poor solution. I would be interested to know of a better, more sustainable answer.
Full source code with RepaintBoundary workaround
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'CustomPainter Demo',
home: MyHomePage(),
);
}
}
class ColorCirle extends CustomPainter {
MaterialColor myColor;
ColorCirle({#required this.myColor});
#override
void paint(Canvas canvas, Size size) {
debugPrint('ColorCircle.paint, ${DateTime.now()}');
final paint = Paint()..color = myColor;
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 100, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return (oldDelegate as ColorCirle).myColor != myColor;
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
MaterialColor actualColor = Colors.red;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('CustomPainter Demo'),
actions: <Widget>[
RepaintBoundary(
child: OutlinedButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(Colors.black)),
onPressed: () {
setState(() => actualColor = Colors.red);
},
child: Text('RedCircle')),
),
RepaintBoundary(
child: OutlinedButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(Colors.black)),
onPressed: () {
setState(() => actualColor = Colors.green);
},
child: Text('GreenCircle')),
),
],
),
body: Center(
child: CustomPaint(
size: Size(300, 300),
painter: ColorCirle(myColor: actualColor),
),
),
);
}
}

Align widget center with AppBar bottom edge

I need to align a widget's center with the appbar bottom edge.
So it will be located vertically half on the appbar and half on the page body.
Right now I've added the widget into the AppBar bottom: but it wont align with it's horizontal center line.
Currently It looks like this:
While i want that the center of the SelectEnvironment button along with the horizontal white line will 'sit' exactly on the bottom edge of the appBar
The code for the appBar is like this:
class CustomAppBar extends AppBar {
final Widget appBarActionButton;
CustomAppBar({Widget title = AppUtils.EMPTY_TEXT_VIEW, this.appBarActionButton}): super(
title: title,
backgroundColor: Colors.blueGrey,
elevation: 0,
bottom: PreferredSize(
child: Stack( //The stack holds the horizontal line and the button aligned cente
alignment: Alignment.center,
children: <Widget>[
Container( //This is the horizontal line
color: Colors.GeneralDividerGray,
height: 1.0,
),
Align(
child: Container(
child: appBarActionButton, //This is the button widget
),
)
],
),
preferredSize: Size.fromHeight(4.0)),
);
}
If there a better way to achieve this by taking it outside of the appbar it's ok with me as long it will give the same effect.
I think you should use Stack and Column like this
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
typedef void OnWidgetSizeChange(Size size);
class MeasureSize extends StatefulWidget {
final Widget child;
final OnWidgetSizeChange onChange;
const MeasureSize({
Key key,
#required this.onChange,
#required this.child,
}) : super(key: key);
#override
_MeasureSizeState createState() => _MeasureSizeState();
}
class _MeasureSizeState extends State<MeasureSize> {
#override
Widget build(BuildContext context) {
SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);
return Container(
key: widgetKey,
child: widget.child,
);
}
var widgetKey = GlobalKey();
var oldSize;
void postFrameCallback(_) {
var context = widgetKey.currentContext;
if (context == null) return;
var newSize = context.size;
if (oldSize == newSize) return;
oldSize = newSize;
widget.onChange(newSize);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
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> {
Size s;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Column(
children: [
MeasureSize(
onChange: (size) {
setState(() {
s = size;
});
},
child: AppBar(
title: Text('title'),
),
),
SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - (s?.height ?? 0.0),
child: Center(child: Text('body')))
],
),
Positioned(
top: (s?.height ?? 0.0) - 16.0,
child: Container(
width: MediaQuery.of(context).size.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 32,
color: Colors.red[400],
padding: EdgeInsets.all(6),
child: Center(child: Text('Select Environment'))),
],
),
),
)
],
));
}
}
The best way is to use Slivers via a widget like below:
ScrollController scrollController = new ScrollController();
return Stack(
children: [
NestedScrollView(
controller: scrollController,
headerSliverBuilder: (context, value){
return [
// list of widgets in here
];
},
body: Container(
// here, your normal body goes
),
),
Positioned(
top: 50.0,
left: 100.0,
child: Container(
// your centered widget here
),
)
]
);
}
Instead of using a normal appBar, you have to use a SliverAppBar

How to move a widget in the screen in Flutter

I am using flutter and i have a container with the shape of a circle using this code
new Container(
width: 50.0,
height: 50.0,
decoration: new BoxDecoration(
shape: BoxShape.circle)
I want to make this circle move on the screen like this
how can I do this?
Here it is:
import 'package:flutter/material.dart';
class SecondScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Drag app"),
),
body: HomePage(),
),
);
}
}
class HomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomePageState();
}
}
class _HomePageState extends State<HomePage> {
double width = 100.0, height = 100.0;
Offset position ;
#override
void initState() {
super.initState();
position = Offset(0.0, height - 20);
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: position.dx,
//top: position.dy - height + 20,
child: Draggable(
child: Container(
width: width,
height: height,
color: Colors.blue,
child: Center(child: Text("Drag", style: Theme.of(context).textTheme.headline,),),
),
feedback: Container(
child: Center(
child: Text("Drag", style: Theme.of(context).textTheme.headline,),),
color: Colors.red[800],
width: width,
height: height,
),
onDraggableCanceled: (Velocity velocity, Offset offset){
setState(() => position = offset);
},
),
),
],
);
}
}
What you are looking for is Draggable widget. You can then handle the translation using onDraggableCanceled which is passed and offset that you can be used to update the placement
onDraggableCanceled :(velocity,offset){
//update the position here
}
Update
After checking the image you will need "Drop me here" part to be a DragTarget that has a method onAccept which will handles the logic when you drag and drop your Draggable
First, wrap your Container inside the Stack with Positioned.
Then, use Pan Gesture to implement a Pan in your Container and use onPan... methods to handle Pan Gesture
Here is code:
Offset position;
#override
void initState() {
super.initState();
position = Offset(10, 10);
}
#override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
double _height = _width * 9 / 16;
return GestureDetector(
onPanStart: (details) => _onPanStart(context, details),
onPanUpdate: (details) => _onPanUpdate(context, details, position),
onPanEnd: (details) => _onPanEnd(context, details),
onPanCancel: () => _onPanCancel(context),
child: SafeArea(
child: Stack(
children: <Widget>[
Positioned(
top: position.dy,
child: Container(
color: Colors.red,
width: _width,
height: _height,
),
),
],
),
),
);
}
void _onPanStart(BuildContext context, DragStartDetails details) {
print(details.globalPosition.dy);
}
void _onPanUpdate(BuildContext context, DragUpdateDetails details, Offset offset) {
setState(() {
position = details.globalPosition;
});
}
void _onPanEnd(BuildContext context, DragEndDetails details) {
print(details.velocity);
}
void _onPanCancel(BuildContext context) {
print("Pan canceled !!");
}
Hope this helps!
You can use Draggable class for dragging the item which you want to drag and for placing it or sticking it to somewhere on the screen you have to wrap that item with DragTarget class. In DragTarget class onAccept method is there where you can write the logic. You can also take a reference to my code here it is
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.indigo,
),
home: new MyHomePage(title: 'Flutter Demo Drag Box'),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body:
new DragGame(), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class DragGame extends StatefulWidget {
#override
_DragGameState createState() => new _DragGameState();
}
class _DragGameState extends State<DragGame> {
int boxNumberIsDragged;
#override
void initState() {
boxNumberIsDragged = null;
super.initState();
}
#override
Widget build(BuildContext context) {
return new Container(
constraints: BoxConstraints.expand(),
color: Colors.grey,
child: new Stack(
children: <Widget>[
buildDraggableBox(1, Colors.red, new Offset(30.0, 100.0)),
buildDraggableBox(2, Colors.yellow, new Offset(30.0, 200.0)),
buildDraggableBox(3, Colors.green, new Offset(30.0, 300.0)),
],
));
}
Widget buildDraggableBox(int boxNumber, Color color, Offset offset) {
return new Draggable(
maxSimultaneousDrags: boxNumberIsDragged == null || boxNumber == boxNumberIsDragged ? 1 : 0,
child: _buildBox(color, offset),
feedback: _buildBox(color, offset),
childWhenDragging: _buildBox(color, offset, onlyBorder: true),
onDragStarted: () {
setState((){
boxNumberIsDragged = boxNumber;
});
},
onDragCompleted: () {
setState((){
boxNumberIsDragged = null;
});
},
onDraggableCanceled: (_,__) {
setState((){
boxNumberIsDragged = null;
});
},
);
}
Widget _buildBox(Color color, Offset offset, {bool onlyBorder: false}) {
return new Container(
height: 50.0,
width: 50.0,
margin: EdgeInsets.only(left: offset.dx, top: offset.dy),
decoration: BoxDecoration(
color: !onlyBorder ? color : Colors.grey,
border: Border.all(color: color)),
);
}
}