Related
Is there any way to display skew borders at the top and bottom?
I came up with the solution below by using two images (top_layout and bottom_layout.png). Is there any other way to make those color bars with shadows without using static images?
return Container(
color: const Color.fromARGB(255, 236, 0, 140),
child: Container(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.white,
margin:
EdgeInsets.only(top: 60.0, bottom: 20.0, left: 15.0, right: 15.0),
child: Stack(
children: <Widget>[
Positioned.fill(
child: Image.asset(
"assets/imgs/top_layout.png",
fit: BoxFit.fitWidth,
alignment: Alignment.topCenter,
),
),
Positioned.fill(
child: Image.asset(
"assets/imgs/xbottom_layout.png",
fit: BoxFit.fitWidth,
alignment: Alignment.bottomLeft,
),
),
],
),
),
),
);
}
How do draw lines in Flutter using the CustomPaint widget
To paint in Flutter you use the CustomPaint widget. The CustomPaint widget takes a CustomPainter object as a parameter. In that class you have to override the paint method, which gives you a canvas that you can paint on. Here is the code to draw the line in the image above.
#override
void paint(Canvas canvas, Size size) {
final p1 = Offset(50, 50);
final p2 = Offset(250, 150);
final paint = Paint()
..color = Colors.black
..strokeWidth = 4;
canvas.drawLine(p1, p2, paint);
}
Notes:
The drawLine method draws a line connecting the two points you give it.
An Offset is a pair of (dx, dy) doubles, offset from the top left corner of the CustomPaint widget.
Another option
You could do something similar with the drawPoints method using the PointMode.polygon option.
#override
void paint(Canvas canvas, Size size) {
final pointMode = ui.PointMode.polygon;
final points = [
Offset(50, 100),
Offset(150, 75),
Offset(250, 250),
Offset(130, 200),
Offset(270, 100),
];
final paint = Paint()
..color = Colors.black
..strokeWidth = 4
..strokeCap = StrokeCap.round;
canvas.drawPoints(pointMode, points, paint);
}
Context
Here is the main.dart code so that you can see it in context.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: HomeWidget(),
),
);
}
}
class HomeWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: CustomPaint( // <-- CustomPaint widget
size: Size(300, 300),
painter: MyPainter(),
),
);
}
}
class MyPainter extends CustomPainter { // <-- CustomPainter class
#override
void paint(Canvas canvas, Size size) {
// <-- Insert your painting code here.
}
#override
bool shouldRepaint(CustomPainter old) {
return false;
}
}
See also
See this article for my fuller answer.
In this case, you would like to use Custom Painter widget instead. You can draw the shape based on coordinates.
Refer this tutorial for more info.
Drawing Custom Shapes in Flutter using CustomPainter
I have a weird flutter issue with a certain animation I'm trying to create.
I am trying to animate an image onto the screen.
I want it to move on the x-axis, and I want it to slowly fade in as well.
So I figured - Positioned and Opacity, and animate their value with a tween.
Both the Positioned and Opacity widgets work great on their own, but when I combine the two - I get a weird animation, that only starts to draw after a while (about 3 seconds).
I tried printing the animation.value and it seems to be fine, slowly climbing from 0.0 to 1.0 - but the clouds suddenly appear only after like 3 seconds.
I tried separating them to different controllers, thought that maybe somehow that was the culprit, but nope.
TLDR Videos:
Opacity Animation Only
Positioned Animation Only
Both Animations Combined - NOT GOOD
Here's the widget's code:
import 'package:app/weather/widgets/CloudProperties.dart';
import 'package:flutter/widgets.dart';
class WeatherCloudWidget extends StatefulWidget {
final double sunSize;
final CloudProperties properties;
WeatherCloudWidget({Key key, this.properties, this.sunSize})
: super(key: key);
#override
State<StatefulWidget> createState() => _WeatherCloudWidget();
}
class _WeatherCloudWidget extends State<WeatherCloudWidget>
with TickerProviderStateMixin {
AnimationController controller;
AnimationController controller2;
Animation<double> position;
Animation<double> opacity;
#override
initState() {
super.initState();
_startAnimation();
}
#override
Widget build(BuildContext context) {
// screen width and height
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final properties = widget.properties;
var vertical =
screenHeight * 0.5 + (widget.sunSize * properties.verticalOffset * -1);
var horizontal = (screenWidth * 0.5) + (widget.sunSize * position.value);
print(opacity.value);
// both Positioned & Opacity widgets
return Positioned(
left: horizontal,
top: vertical,
child: Opacity(
opacity: opacity.value,
child: Image.asset(
properties.path,
width: properties.getScaledWidth(widget.sunSize),
height: properties.getScaledHeight(widget.sunSize),
),
));
// Positioned only
return Positioned(
left: horizontal,
top: vertical,
child: Image.asset(
properties.path,
width: properties.getScaledWidth(widget.sunSize),
height: properties.getScaledHeight(widget.sunSize),
));
// Opacity only
return Positioned(
left: (screenWidth * 0.5) + (widget.sunSize * properties.tween[1]),
top: vertical,
child: Opacity(
opacity: opacity.value,
child: Image.asset(
properties.path,
width: properties.getScaledWidth(widget.sunSize),
height: properties.getScaledHeight(widget.sunSize),
),
));
}
#override
dispose() {
controller.dispose();
controller2.dispose();
super.dispose();
}
void _startAnimation() {
controller = AnimationController(
duration: const Duration(milliseconds: 5000), vsync: this);
controller2 = AnimationController(
duration: const Duration(milliseconds: 5000), vsync: this);
position = Tween(
begin: widget.properties.tween[0], end: widget.properties.tween[1])
.animate(
new CurvedAnimation(parent: controller, curve: Curves.decelerate))
..addListener(() => setState(() {}));
opacity = Tween(begin: 0.0, end: 1.0).animate(controller2)
..addListener(() => setState(() {}));
controller.forward();
controller2.forward();
}
}
Alright guys. I managed to sort this using SlideTransition and FadeTransition.
I guess we should only use Transition widgets for... transitions? while things like Positioned and Opacity are for more static widgets? Not sure...
What it looks like: https://youtu.be/hj7PkjXrgfg
Anyways, here's the replacement code, if anyone's looking for reference:
class WeatherCloudWidget extends StatefulWidget {
final double sunSize;
final CloudProperties properties;
WeatherCloudWidget({Key key, this.properties, this.sunSize})
: super(key: key);
#override
State<StatefulWidget> createState() => _WeatherCloudWidget();
}
class _WeatherCloudWidget extends State<WeatherCloudWidget>
with TickerProviderStateMixin {
AnimationController controller;
Animation<Offset> position;
Animation<double> opacity;
final alphaTween = new Tween(begin: 0.0, end: 1.0);
#override
initState() {
super.initState();
_startAnimation();
}
#override
Widget build(BuildContext context) {
// screen width and height
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final properties = widget.properties;
var vertical = (screenHeight * 0.5) +
(widget.sunSize * properties.verticalOffset * -1);
var horizontal =
(screenWidth * 0.5) + (widget.sunSize * properties.tweenEnd);
return Positioned(
left: horizontal,
top: vertical,
child: SlideTransition(
position: position,
child: FadeTransition(
opacity: opacity,
child: Image.asset(
properties.path,
width: properties.getScaledWidth(widget.sunSize),
height: properties.getScaledHeight(widget.sunSize),
),
)),
);
}
#override
dispose() {
controller.dispose();
super.dispose();
}
void _startAnimation() {
controller = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
position = new Tween<Offset>(
begin: new Offset(widget.properties.tweenStart, 0.0),
end: new Offset(0.0, 0.0),
).animate(new CurvedAnimation(parent: controller, curve: Curves.decelerate));
opacity = alphaTween.animate(controller);
controller.forward();
}
}
I'm trying to animate a custom dialog box in dart so that when it pops up it create some animations. There is a library in Android that is having animated dialog boxes, is there any similar library in Flutter Sweet Alert Dialog
how can we achieve the same functionality in flutter?
To create dialog boxes you can use the Overlay or Dialog classes. If you want to add animations like in the given framework you can use the AnimationController like in the following example. The CurvedAnimation class is used to create the bouncing effect on the animation.
Update: In general it is better to show dialogs with the showDialog function, because the closing and gesture are handled by Flutter. I have updated the example, it is now running with showDialog and you are able to close the dialog by tapping on the background.
You can copy & paste the following code into a new project and adjust it. It is runnable on it's own.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(title: 'Flutter Demo', theme: ThemeData(), home: Page());
}
}
class Page extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton.icon(
onPressed: () {
showDialog(
context: context,
builder: (_) => FunkyOverlay(),
);
},
icon: Icon(Icons.message),
label: Text("PopUp!")),
),
);
}
}
class FunkyOverlay extends StatefulWidget {
#override
State<StatefulWidget> createState() => FunkyOverlayState();
}
class FunkyOverlayState extends State<FunkyOverlay>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> scaleAnimation;
#override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 450));
scaleAnimation =
CurvedAnimation(parent: controller, curve: Curves.elasticInOut);
controller.addListener(() {
setState(() {});
});
controller.forward();
}
#override
Widget build(BuildContext context) {
return Center(
child: Material(
color: Colors.transparent,
child: ScaleTransition(
scale: scaleAnimation,
child: Container(
decoration: ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0))),
child: Padding(
padding: const EdgeInsets.all(50.0),
child: Text("Well hello there!"),
),
),
),
),
);
}
}
Just use 'showGeneralDialog()' No need use extra lib or widget.
You can get more animatated dialog reference from This Link
void _openCustomDialog() {
showGeneralDialog(barrierColor: Colors.black.withOpacity(0.5),
transitionBuilder: (context, a1, a2, widget) {
return Transform.scale(
scale: a1.value,
child: Opacity(
opacity: a1.value,
child: AlertDialog(
shape: OutlineInputBorder(
borderRadius: BorderRadius.circular(16.0)),
title: Text('Hello!!'),
content: Text('How are you?'),
),
),
);
},
transitionDuration: Duration(milliseconds: 200),
barrierDismissible: true,
barrierLabel: '',
context: context,
pageBuilder: (context, animation1, animation2) {});
}
I tried to do the animation shown in your gif. Gonna post the code to help people who want it, its not perfect so if anyone wants to help improving it go for it.
How it looks:
Code:
import 'package:flutter/material.dart';
import 'package:angles/angles.dart';
import 'dart:math';
import 'dart:core';
class CheckAnimation extends StatefulWidget {
final double size;
final VoidCallback onComplete;
CheckAnimation({this.size, this.onComplete});
#override
_CheckAnimationState createState() => _CheckAnimationState();
}
class _CheckAnimationState extends State<CheckAnimation>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> curve;
#override
void initState() {
// TODO: implement initState
super.initState();
_controller =
AnimationController(duration: Duration(seconds: 2), vsync: this);
curve = CurvedAnimation(parent: _controller, curve: Curves.bounceInOut);
_controller.addListener(() {
setState(() {});
if(_controller.status == AnimationStatus.completed && widget.onComplete != null){
widget.onComplete();
}
});
_controller.forward();
}
#override
Widget build(BuildContext context) {
return Container(
height: widget.size ?? 100,
width: widget.size ?? 100,
color: Colors.transparent,
child: CustomPaint(
painter: CheckPainter(value: curve.value),
),
);
}
#override
void dispose() {
// TODO: implement dispose
_controller.dispose();
super.dispose();
}
}
class CheckPainter extends CustomPainter {
Paint _paint;
double value;
double _length;
double _offset;
double _secondOffset;
double _startingAngle;
CheckPainter({this.value}) {
_paint = Paint()
..color = Colors.greenAccent
..strokeWidth = 5.0
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
assert(value != null);
_length = 60;
_offset = 0;
_startingAngle = 205;
}
#override
void paint(Canvas canvas, Size size) {
// Background canvas
var rect = Offset(0, 0) & size;
_paint.color = Colors.greenAccent.withOpacity(.05);
double line1x1 = size.width / 2 +
size.width * cos(Angle.fromDegrees(_startingAngle).radians) * .5;
double line1y1 = size.height / 2 +
size.height * sin(Angle.fromDegrees(_startingAngle).radians) * .5;
double line1x2 = size.width * .45;
double line1y2 = size.height * .65;
double line2x1 =
size.width / 2 + size.width * cos(Angle.fromDegrees(320).radians) * .35;
double line2y1 = size.height / 2 +
size.height * sin(Angle.fromDegrees(320).radians) * .35;
canvas.drawArc(rect, Angle.fromDegrees(_startingAngle).radians,
Angle.fromDegrees(360).radians, false, _paint);
canvas.drawLine(Offset(line1x1, line1y1), Offset(line1x2, line1y2), _paint);
canvas.drawLine(Offset(line2x1, line2y1), Offset(line1x2, line1y2), _paint);
// animation painter
double circleValue, checkValue;
if (value < .5) {
checkValue = 0;
circleValue = value / .5;
} else {
checkValue = (value - .5) / .5;
circleValue = 1;
}
_paint.color = const Color(0xff72d0c3);
double firstAngle = _startingAngle + 360 * circleValue;
canvas.drawArc(
rect,
Angle.fromDegrees(firstAngle).radians,
Angle.fromDegrees(
getSecondAngle(firstAngle, _length, _startingAngle + 360))
.radians,
false,
_paint);
double line1Value = 0, line2Value = 0;
if (circleValue >= 1) {
if (checkValue < .5) {
line2Value = 0;
line1Value = checkValue / .5;
} else {
line2Value = (checkValue - .5) / .5;
line1Value = 1;
}
}
double auxLine1x1 = (line1x2 - line1x1) * getMin(line1Value, .8);
double auxLine1y1 =
(((auxLine1x1) - line1x1) / (line1x2 - line1x1)) * (line1y2 - line1y1) +
line1y1;
if (_offset < 60) {
auxLine1x1 = line1x1;
auxLine1y1 = line1y1;
}
double auxLine1x2 = auxLine1x1 + _offset / 2;
double auxLine1y2 =
(((auxLine1x1 + _offset / 2) - line1x1) / (line1x2 - line1x1)) *
(line1y2 - line1y1) +
line1y1;
if (checkIfPointHasCrossedLine(Offset(line1x2, line1y2),
Offset(line2x1, line2y1), Offset(auxLine1x2, auxLine1y2))) {
auxLine1x2 = line1x2;
auxLine1y2 = line1y2;
}
if (_offset > 0) {
canvas.drawLine(Offset(auxLine1x1, auxLine1y1),
Offset(auxLine1x2, auxLine1y2), _paint);
}
// SECOND LINE
double auxLine2x1 = (line2x1 - line1x2) * line2Value;
double auxLine2y1 =
((((line2x1 - line1x2) * line2Value) - line1x2) / (line2x1 - line1x2)) *
(line2y1 - line1y2) +
line1y2;
if (checkIfPointHasCrossedLine(Offset(line1x1, line1y1),
Offset(line1x2, line1y2), Offset(auxLine2x1, auxLine2y1))) {
auxLine2x1 = line1x2;
auxLine2y1 = line1y2;
}
if (line2Value > 0) {
canvas.drawLine(
Offset(auxLine2x1, auxLine2y1),
Offset(
(line2x1 - line1x2) * line2Value + _offset * .75,
((((line2x1 - line1x2) * line2Value + _offset * .75) - line1x2) /
(line2x1 - line1x2)) *
(line2y1 - line1y2) +
line1y2),
_paint);
}
}
double getMax(double x, double y) {
return (x > y) ? x : y;
}
double getMin(double x, double y) {
return (x > y) ? y : x;
}
bool checkIfPointHasCrossedLine(Offset a, Offset b, Offset point) {
return ((b.dx - a.dx) * (point.dy - a.dy) -
(b.dy - a.dy) * (point.dx - a.dx)) >
0;
}
double getSecondAngle(double angle, double plus, double max) {
if (angle + plus > max) {
_offset = angle + plus - max;
return max - angle;
} else {
_offset = 0;
return plus;
}
}
#override
bool shouldRepaint(CheckPainter old) {
return old.value != value;
}
}
I used angles package
Whenever you want to show Dialog with some Animation, the best way is to use showGeneralDialog()
NOTE: ALL PARAMETERS MUST BE PROVIDED OTHERWISE SOME ERROR WILL OCCUR.
showGeneralDialog(
barrierColor: Colors.black.withOpacity(0.5), //SHADOW EFFECT
transitionBuilder: (context, a1, a2, widget) {
return Center(
child: Container(
height: 100.0 * a1.value, // USE PROVIDED ANIMATION
width: 100.0 * a1.value,
color: Colors.blue,
),
);
},
transitionDuration: Duration(milliseconds: 200), // DURATION FOR ANIMATION
barrierDismissible: true,
barrierLabel: 'LABEL',
context: context,
pageBuilder: (context, animation1, animation2) {
return Text('PAGE BUILDER');
});
}, child: Text('Show Dialog'),),
If you need more customization, then extend PopupRoute and create your own _DialogRoute<T> and showGeneralDialog()
EDIT
Edited answer of Niklas with functionality to close Overlay :)
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(title: 'Flutter Demo', theme: ThemeData(), home: Page());
}
}
class Page extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton.icon(
onPressed: () {
OverlayEntry overlayEntry;
overlayEntry = OverlayEntry(builder: (c) {
return FunkyOverlay(onClose: () => overlayEntry.remove());
});
Overlay.of(context).insert(overlayEntry);
},
icon: Icon(Icons.message),
label: Text("PopUp!")),
),
);
}
}
class FunkyOverlay extends StatefulWidget {
final VoidCallback onClose;
const FunkyOverlay({Key key, this.onClose}) : super(key: key);
#override
State<StatefulWidget> createState() => FunkyOverlayState();
}
class FunkyOverlayState extends State<FunkyOverlay>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> opacityAnimation;
Animation<double> scaleAnimatoin;
#override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 450));
opacityAnimation = Tween<double>(begin: 0.0, end: 0.4).animate(
CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn));
scaleAnimatoin =
CurvedAnimation(parent: controller, curve: Curves.elasticInOut);
controller.addListener(() {
setState(() {});
});
controller.forward();
}
#override
Widget build(BuildContext context) {
return Material(
color: Colors.black.withOpacity(opacityAnimation.value),
child: Center(
child: ScaleTransition(
scale: scaleAnimatoin,
child: Container(
decoration: ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0))),
child: Padding(
padding: const EdgeInsets.all(50.0),
child: OutlineButton(onPressed: widget.onClose, child: Text('Close!'),),
),
),
),
),
);
}
}
Add the below function in your code which shows a animated Dialog with Scale and Fade transition
void _openCustomDialog(BuildContext context) {
showGeneralDialog(
barrierColor: Colors.black.withOpacity(0.5),
transitionBuilder: (context, a1, a2, widget) {
return ScaleTransition(
scale: Tween<double>(begin: 0.5, end: 1.0).animate(a1),
child: FadeTransition(
opacity: Tween<double>(begin: 0.5, end: 1.0).animate(a1),
child: const AboutPasteLog(
title: 'About',
),
));
},
transitionDuration: const Duration(milliseconds: 300),
barrierDismissible: true,
barrierLabel: '',
context: context,
pageBuilder: (context, animation1, animation2) {
return Container();
});
}
And invoke it as below
onPressed:(){
_openCustomDialog(context);
}
The final result
I'm trying to extend the ShapeBorder class to add some functionality. But just playing around with the paint method, I found something that I did not expect:
The corners of the border and the corners of the rectangle do not seem to match. I used the following code:
class CustomRoundedRectangleBorder extends ShapeBorder {
final double borderWidth;
final BorderRadius borderRadius;
const CustomRoundedRectangleBorder({
this.borderWidth: 1.0,
this.borderRadius: BorderRadius.zero,
})
: assert(borderRadius != null);
#override
EdgeInsetsGeometry get dimensions {
return new EdgeInsets.all(borderWidth);
}
#override
ShapeBorder scale(double t) {
return new CustomRoundedRectangleBorder(
borderWidth: borderWidth * (t),
borderRadius: borderRadius * (t),
);
}
#override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
assert(t != null);
if (a is CustomRoundedRectangleBorder) {
return new CustomRoundedRectangleBorder(
borderWidth: ui.lerpDouble(a.borderWidth, borderWidth, t),
borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t),
);
}
return super.lerpFrom(a, t);
}
#override
ShapeBorder lerpTo(ShapeBorder b, double t) {
assert(t != null);
if (b is CustomRoundedRectangleBorder) {
return new CustomRoundedRectangleBorder(
borderWidth: ui.lerpDouble(borderWidth, b.borderWidth, t),
borderRadius: BorderRadius.lerp(borderRadius, b.borderRadius, t),
);
}
return super.lerpTo(b, t);
}
#override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRRect(borderRadius.resolve(textDirection).toRRect(rect).deflate(
borderWidth));
}
#override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
}
#override
void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
rect = rect.deflate(borderWidth / 2.0);
Paint paint;
final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect);
paint = new Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = borderWidth;
canvas.drawRRect(borderRect, paint);
}
}
And created the rectangle as follows:
new Container(
height: 100.0,
width: 200.0,
padding: new EdgeInsets.all(10.0),
decoration: new ShapeDecoration(
color: Colors.black,
shape: new CustomRoundedRectangleBorder(
borderRadius: new BorderRadius.all(new Radius.circular(20.0)),
borderWidth: 10.0,
),
// side: new BorderSide(color: Colors.white)
),
child: new Center(child: new Text("My Button"),),
),
I feel like the Flutter source code takes a similar approach, but perhaps I'm not seeing something.
EDIT
Changing the style of my paint to PaintingStyle.fill thus drawing a rectangle over the original rectangle instead of borders, I do seem to get the correct borders:
void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
// rect = rect.deflate(borderWidth / 2.0);
Paint paint;
final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect);
paint = new Paint()
..color = Colors.red.withOpacity(0.25)
..style = PaintingStyle.fill
..strokeWidth = borderWidth;
canvas.drawRRect(borderRect, paint);
}
I'm still puzzled on how to do this...
You can use canvas.drawRRect :
canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - gap - smallMarkWidth - 15,gap * 8,gap + 70,gap * 5,),Radius.circular(15.0)),backgroundPaint);
You should use canvas.drawPath not drawRect
Paint paint = new Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = borderWidth;
canvas.drawPath(getOuterPath(rect), paint);
also if you just want a border, its enough to use
#override
Path getInnerPath(Rect rect, {TextDirection textDirection}) {
return new Path()
..fillType = PathFillType.evenOdd
..addPath(getOuterPath(rect), Offset.zero);
}
This worked for me!
class MyButton extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DecoratedBox(
decoration:
ShapeDecoration(
shape: RoundedRectangleBorder(
side:new BorderSide(color: Color(0xFF2A8068)),
borderRadius: new BorderRadius.all(new Radius.circular(4))),
color: Color(0xFF2A8068)),
child: Theme(
data: Theme.of(context).copyWith(
buttonTheme: ButtonTheme.of(context).copyWith(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap)),
child: OutlineButton(
shape: RoundedRectangleBorder(
side:new BorderSide(color: Color(0xFF2A8068)), //the outline color
borderRadius: new BorderRadius.all(new Radius.circular(4))),
child: Text(
"ابدأ", //your text here
style: new TextStyle(
color: Colors.white, //your textColor
),
),
onPressed: () => {},
),
),
);
Draw custom rounded border with shadow.
new Container(
decoration:
new BoxDecoration(
borderRadius: new BorderRadius.circular(10.0),
color: Colors.white,
boxShadow: [
new BoxShadow(
color: Colors.grey,
blurRadius: 3.0,
offset: new Offset(1.0, 1.0))
],
),
)
Draw custom rounded border without shadow.
new Container(
decoration:
new BoxDecoration(
borderRadius: new BorderRadius.circular(10.0),
color: Colors.grey,
),
)
You can use ClipRRect widget instead of drawRect and it is simple to use.
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(),
),
I published a simple package based off #Bram Vanbilsen's code, which gives you control on how you want to draw the shape and it's border whichever way you want.
The package can be found on pub.dev here: https://pub.dev/packages/custom_rounded_rectangle_border
I want to make if floating action button pressed, it show tooltip. But i don't know how to show it programmatically.
Is there a way to show it?
Currently there's no official way to do this.
BUT, there's a workaround : use ensureTooltipVisible from _TooltipState using a GlobalKey to fetch it.
Typically you'd the following field inside the widget instantiating Tooltip :
final key = new GlobalKey();
Then, on your tooltip, you'll assign this key :
new Tooltip(
key: key,
...
),
And finally inside the onPressed of your FloatingButton you can do :
onPressed: () {
final dynamic tooltip = key.currentState;
tooltip.ensureTooltipVisible();
},
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Tooltip Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: TooltipSample(title: _title),
);
}
}
class TooltipSample extends StatelessWidget {
const TooltipSample({super.key, required this.title});
final String title;
#override
Widget build(BuildContext context) {
final GlobalKey<TooltipState> tooltipkey = GlobalKey<TooltipState>();
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Tooltip(
// Provide a global key with the "TooltipState" type to show
// the tooltip manually when trigger mode is set to manual.
key: tooltipkey,
triggerMode: TooltipTriggerMode.manual,
showDuration: const Duration(seconds: 1),
message: 'I am a Tooltip',
child: const Text('Tap on the FAB'),
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
// Show Tooltip programmatically on button tap.
tooltipkey.currentState?.ensureTooltipVisible();
},
label: const Text('Show Tooltip'),
),
);
}
}
/////// Call from your page
CustomTooltip(
message: "tooltip message",
show: value, ( send true or false)
margin: EdgeInsets.only(
bottom: 30,
right: Dimens.horizontalOffset,
left: Dimens.horizontalOffset),
padding: EdgeInsets.all(Dimens.verticalOffset),
textStyle: TextStyle(
fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(8),
bottomLeft: Radius.circular(0),
topRight: Radius.circular(8),
topLeft: Radius.circular(8))),
preferBelow: true,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(buttonHeight * 0.36)),
child: Container(
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(buttonHeight * 0.36)),
border: Border.all(color: Colors.white, width: 1)),
child: Container(
width: buttonHeight,
height: buttonHeight,
padding: EdgeInsets.all(7),
child: Center(
child: Image.asset(
"images/ic_hint_snowflake.png",
),
),
),
),
onTap: () {
},
),
),
);
// custom_widget.dart
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'custom_triangle.dart';
///
/// * <https://material.io/design/components/tooltips.html>
/// * [TooltipTheme] or [ThemeData.tooltipTheme]
class CustomTooltip extends StatefulWidget {
/// Creates a tooltip.
///
/// By default, tooltips should adhere to the
/// [Material specification](https://material.io/design/components/tooltips.html#spec).
/// If the optional constructor parameters are not defined, the values
/// provided by [TooltipTheme.of] will be used if a [TooltipTheme] is present
/// or specified in [ThemeData].
///
/// All parameters that are defined in the constructor will
/// override the default values _and_ the values in [TooltipTheme.of].
const CustomTooltip(
{Key? key,
required this.message,
required this.show,
this.height,
this.padding,
this.margin,
this.verticalOffset,
this.preferBelow,
this.excludeFromSemantics,
this.decoration,
this.textStyle,
this.waitDuration,
this.showDuration,
this.child})
: assert(message != null),
super(key: key);
/// show control
final bool show;
/// The text to display in the tooltip.
final String message;
/// The height of the tooltip's [child].
///
/// If the [child] is null, then this is the tooltip's intrinsic height.
final double? height;
/// The amount of space by which to inset the tooltip's [child].
///
/// Defaults to 16.0 logical pixels in each direction.
final EdgeInsetsGeometry? padding;
/// The empty space that surrounds the tooltip.
///
/// Defines the tooltip's outer [Container.margin]. By default, a
/// long tooltip will span the width of its window. If long enough,
/// a tooltip might also span the window's height. This property allows
/// one to define how much space the tooltip must be inset from the edges
/// of their display window.
///
/// If this property is null, then [TooltipThemeData.margin] is used.
/// If [TooltipThemeData.margin] is also null, the default margin is
/// 0.0 logical pixels on all sides.
final EdgeInsetsGeometry? margin;
/// The vertical gap between the widget and the displayed tooltip.
///
/// When [preferBelow] is set to true and tooltips have sufficient space to
/// display themselves, this property defines how much vertical space
/// tooltips will position themselves under their corresponding widgets.
/// Otherwise, tooltips will position themselves above their corresponding
/// widgets with the given offset.
final double? verticalOffset;
/// Whether the tooltip defaults to being displayed below the widget.
///
/// Defaults to true. If there is insufficient space to display the tooltip in
/// the preferred direction, the tooltip will be displayed in the opposite
/// direction.
final bool? preferBelow;
/// Whether the tooltip's [message] should be excluded from the semantics
/// tree.
///
/// Defaults to false. A tooltip will add a [Semantics] label that is set to
/// [CustomTooltip.message]. Set this property to true if the app is going to
/// provide its own custom semantics label.
final bool? excludeFromSemantics;
/// The widget below this widget in the tree.
///
/// {#macro flutter.widgets.ProxyWidget.child}
final Widget? child;
/// Specifies the tooltip's shape and background color.
///
/// The tooltip shape defaults to a rounded rectangle with a border radius of
/// 4.0. Tooltips will also default to an opacity of 90% and with the color
/// [Colors.grey[700]] if [ThemeData.brightness] is [Brightness.dark], and
/// [Colors.white] if it is [Brightness.light].
final Decoration? decoration;
/// The style to use for the message of the tooltip.
///
/// If null, the message's [TextStyle] will be determined based on
/// [ThemeData]. If [ThemeData.brightness] is set to [Brightness.dark],
/// [TextTheme.bodyText2] of [ThemeData.textTheme] will be used with
/// [Colors.white]. Otherwise, if [ThemeData.brightness] is set to
/// [Brightness.light], [TextTheme.bodyText2] of [ThemeData.textTheme] will be
/// used with [Colors.black].
final TextStyle? textStyle;
/// The length of time that a pointer must hover over a tooltip's widget
/// before the tooltip will be shown.
///
/// Once the pointer leaves the widget, the tooltip will immediately
/// disappear.
///
/// Defaults to 0 milliseconds (tooltips are shown immediately upon hover).
final Duration? waitDuration;
/// The length of time that the tooltip will be shown after a long press
/// is released.
///
/// Defaults to 1.5 seconds.
final Duration? showDuration;
#override
_CustomTooltipState createState() => _CustomTooltipState();
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('message', message, showName: false));
properties.add(DoubleProperty('height', height, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding,
defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin,
defaultValue: null));
properties.add(
DoubleProperty('vertical offset', verticalOffset, defaultValue: null));
properties.add(FlagProperty('position',
value: preferBelow,
ifTrue: 'below',
ifFalse: 'above',
showName: true,
defaultValue: null));
properties.add(FlagProperty('semantics',
value: excludeFromSemantics,
ifTrue: 'excluded',
showName: true,
defaultValue: null));
properties.add(DiagnosticsProperty<Duration>('wait duration', waitDuration,
defaultValue: null));
properties.add(DiagnosticsProperty<Duration>('show duration', showDuration,
defaultValue: null));
}
}
class _CustomTooltipState extends State<CustomTooltip>
with SingleTickerProviderStateMixin {
static const double _defaultVerticalOffset = 24.0;
static const bool _defaultPreferBelow = true;
static const EdgeInsetsGeometry _defaultMargin = EdgeInsets.zero;
static const Duration _fadeInDuration = Duration(milliseconds: 150);
static const Duration _fadeOutDuration = Duration(milliseconds: 75);
static const Duration _defaultShowDuration = Duration(milliseconds: 1500);
static const Duration _defaultWaitDuration = Duration.zero;
static const bool _defaultExcludeFromSemantics = false;
late double height;
late bool show;
late EdgeInsetsGeometry padding;
late EdgeInsetsGeometry margin;
late Decoration decoration;
late TextStyle textStyle;
late double verticalOffset;
late bool preferBelow;
late bool excludeFromSemantics;
late AnimationController _controller;
OverlayEntry? _entry;
Timer? _hideTimer;
Timer? _showTimer;
late Duration showDuration;
late Duration waitDuration;
late bool _mouseIsConnected;
bool _longPressActivated = false;
#override
void initState() {
super.initState();
_mouseIsConnected = RendererBinding.instance!.mouseTracker.mouseIsConnected;
_controller = AnimationController(
duration: _fadeInDuration,
reverseDuration: _fadeOutDuration,
vsync: this,
)..addStatusListener(_handleStatusChanged);
// Listen to see when a mouse is added.
RendererBinding.instance!.mouseTracker
.addListener(_handleMouseTrackerChange);
// Listen to global pointer events so that we can hide a tooltip immediately
// if some other control is clicked on.
GestureBinding.instance!.pointerRouter.addGlobalRoute(_handlePointerEvent);
}
// https://material.io/components/tooltips#specs
double _getDefaultTooltipHeight() {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
return 24.0;
default:
return 32.0;
}
}
EdgeInsets _getDefaultPadding() {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
return const EdgeInsets.symmetric(horizontal: 8.0);
default:
return const EdgeInsets.symmetric(horizontal: 16.0);
}
}
double _getDefaultFontSize() {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
return 10.0;
default:
return 14.0;
}
}
// Forces a rebuild if a mouse has been added or removed.
void _handleMouseTrackerChange() {
if (!mounted) {
return;
}
final bool mouseIsConnected =
RendererBinding.instance!.mouseTracker.mouseIsConnected;
if (mouseIsConnected != _mouseIsConnected) {
setState(() {
_mouseIsConnected = mouseIsConnected;
});
}
}
void _handleStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.dismissed) {
_hideTooltip(immediately: true);
}
}
void _hideTooltip({bool immediately = false}) {
_showTimer?.cancel();
_showTimer = null;
if (immediately) {
_removeEntry();
return;
}
if (_longPressActivated) {
// Tool tips activated by long press should stay around for the showDuration.
_hideTimer ??= Timer(showDuration, _controller.reverse);
} else {
// Tool tips activated by hover should disappear as soon as the mouse
// leaves the control.
_controller.reverse();
}
_longPressActivated = false;
}
void _showTooltip({bool immediately = false}) {
_hideTimer?.cancel();
_hideTimer = null;
if (immediately) {
ensureTooltipVisible();
return;
}
_showTimer ??= Timer(waitDuration, ensureTooltipVisible);
}
/// Shows the tooltip if it is not already visible.
///
/// Returns `false` when the tooltip was already visible or if the context has
/// become null.
bool ensureTooltipVisible() {
_showTimer?.cancel();
_showTimer = null;
if (_entry != null) {
// Stop trying to hide, if we were.
_hideTimer?.cancel();
_hideTimer = null;
_controller.forward();
return false; // Already visible.
}
_createNewEntry();
_controller.forward();
return true;
}
void _createNewEntry() {
final OverlayState overlayState = Overlay.of(
context,
debugRequiredFor: widget,
)!;
final RenderBox box = context.findRenderObject()! as RenderBox;
final Offset target = box.localToGlobal(
box.size.center(Offset.zero),
ancestor: overlayState.context.findRenderObject(),
);
// We create this widget outside of the overlay entry's builder to prevent
// updated values from happening to leak into the overlay when the overlay
// rebuilds.
final Widget overlay = Directionality(
textDirection: Directionality.of(context),
child: _TooltipOverlay(
message: widget.message,
height: height,
padding: padding,
margin: margin,
decoration: decoration,
textStyle: textStyle,
animation: CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn,
),
target: target,
verticalOffset: verticalOffset,
preferBelow: preferBelow,
),
);
_entry = OverlayEntry(builder: (BuildContext context) => overlay);
overlayState.insert(_entry!);
SemanticsService.tooltip(widget.message);
}
void _removeEntry() {
_hideTimer?.cancel();
_hideTimer = null;
_showTimer?.cancel();
_showTimer = null;
_entry?.remove();
_entry = null;
}
void _handlePointerEvent(PointerEvent event) {
if (_entry == null) {
return;
}
if (event is PointerUpEvent || event is PointerCancelEvent) {
_hideTooltip();
} else if (event is PointerDownEvent) {
_hideTooltip(immediately: true);
}
}
#override
void deactivate() {
if (_entry != null) {
_hideTooltip(immediately: true);
}
_showTimer?.cancel();
super.deactivate();
}
#override
void dispose() {
GestureBinding.instance!.pointerRouter
.removeGlobalRoute(_handlePointerEvent);
RendererBinding.instance!.mouseTracker
.removeListener(_handleMouseTrackerChange);
if (_entry != null) _removeEntry();
_controller.dispose();
super.dispose();
}
void _handleLongPress() {
_longPressActivated = true;
final bool tooltipCreated = ensureTooltipVisible();
if (tooltipCreated) Feedback.forLongPress(context);
}
#override
Widget build(BuildContext context) {
assert(Overlay.of(context, debugRequiredFor: widget) != null);
final ThemeData theme = Theme.of(context);
final TooltipThemeData tooltipTheme = TooltipTheme.of(context);
final TextStyle defaultTextStyle;
final BoxDecoration defaultDecoration;
if (theme.brightness == Brightness.dark) {
defaultTextStyle = theme.textTheme.bodyText2!.copyWith(
color: Colors.black,
fontSize: _getDefaultFontSize(),
);
defaultDecoration = BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: const BorderRadius.all(Radius.circular(4)),
);
} else {
defaultTextStyle = theme.textTheme.bodyText2!.copyWith(
color: Colors.white,
fontSize: _getDefaultFontSize(),
);
defaultDecoration = BoxDecoration(
color: Colors.grey[700]!.withOpacity(0.9),
borderRadius: const BorderRadius.all(Radius.circular(4)),
);
}
height = widget.height ?? tooltipTheme.height ?? _getDefaultTooltipHeight();
padding = widget.padding ?? tooltipTheme.padding ?? _getDefaultPadding();
show = widget.show;
margin = widget.margin ?? tooltipTheme.margin ?? _defaultMargin;
verticalOffset = widget.verticalOffset ??
tooltipTheme.verticalOffset ??
_defaultVerticalOffset;
preferBelow =
widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow;
excludeFromSemantics = widget.excludeFromSemantics ??
tooltipTheme.excludeFromSemantics ??
_defaultExcludeFromSemantics;
decoration =
widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration;
textStyle = widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle;
waitDuration = widget.waitDuration ??
tooltipTheme.waitDuration ??
_defaultWaitDuration;
showDuration = widget.showDuration ??
tooltipTheme.showDuration ??
_defaultShowDuration;
Widget result = GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPress: _handleLongPress,
excludeFromSemantics: true,
child: Semantics(
label: excludeFromSemantics ? null : widget.message,
child: widget.child,
),
);
if (show)
_showTooltip();
else
_hideTooltip();
// Only check for hovering if there is a mouse connected.
if (_mouseIsConnected) {
result = MouseRegion(
onEnter: (PointerEnterEvent event) => _showTooltip(),
onExit: (PointerExitEvent event) => _hideTooltip(),
child: result,
);
}
return result;
}
}
/// A delegate for computing the layout of a tooltip to be displayed above or
/// bellow a target specified in the global coordinate system.
class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
/// Creates a delegate for computing the layout of a tooltip.
///
/// The arguments must not be null.
_TooltipPositionDelegate({
required this.target,
required this.verticalOffset,
required this.preferBelow,
}) : assert(target != null),
assert(verticalOffset != null),
assert(preferBelow != null);
/// The offset of the target the tooltip is positioned near in the global
/// coordinate system.
final Offset target;
/// The amount of vertical distance between the target and the displayed
/// tooltip.
final double verticalOffset;
/// Whether the tooltip is displayed below its widget by default.
///
/// If there is insufficient space to display the tooltip in the preferred
/// direction, the tooltip will be displayed in the opposite direction.
final bool preferBelow;
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) =>
constraints.loosen();
#override
Offset getPositionForChild(Size size, Size childSize) {
return positionDependentBox(
size: size,
childSize: childSize,
target: target,
verticalOffset: verticalOffset,
preferBelow: preferBelow,
);
}
#override
bool shouldRelayout(_TooltipPositionDelegate oldDelegate) {
return target != oldDelegate.target ||
verticalOffset != oldDelegate.verticalOffset ||
preferBelow != oldDelegate.preferBelow;
}
}
class _TooltipOverlay extends StatelessWidget {
const _TooltipOverlay({
Key? key,
required this.message,
required this.height,
this.padding,
this.margin,
this.decoration,
this.textStyle,
required this.animation,
required this.target,
required this.verticalOffset,
required this.preferBelow,
}) : super(key: key);
final String message;
final double height;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final Decoration? decoration;
final TextStyle? textStyle;
final Animation<double> animation;
final Offset target;
final double verticalOffset;
final bool preferBelow;
#override
Widget build(BuildContext context) {
return Positioned.fill(
child: IgnorePointer(
child: CustomSingleChildLayout(
delegate: _TooltipPositionDelegate(
target: target,
verticalOffset: verticalOffset,
preferBelow: preferBelow,
),
child: Stack(
children: [
Positioned(
bottom: 15,
left: 48,
child: RotatedBox(
quarterTurns: 2,
child: CustomPaint(
painter: TrianglePainter(
strokeColor: Colors.white,
strokeWidth: 10,
paintingStyle: PaintingStyle.fill,
),
child: Container(
height: 18,
width: 18,
),
),
),
),
FadeTransition(
opacity: animation,
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: height),
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyText2!,
child: Stack(
children: [
Container(
decoration: decoration,
padding: padding,
margin: margin,
child: Center(
widthFactor: 1.0,
heightFactor: 1.0,
child: Text(
message,
style: textStyle,
),
),
),
],
),
),
),
),
],
),
),
),
);
}
}
FloatingActionButton already has a tooltip property.
floatingActionButton: new FloatingActionButton(
tooltip: "ADDED",
onPressed: (){},
child: new Icon(Icons.add),),
Is what you are asking for different than this ?