how to change TextPainter on Flutter? - flutter

Is it possible to change text paint like the below image? I need to add two lines and without rotating.
Container(
foregroundDecoration: const BadgeDecoration(
badgeColor: Colors.green,
badgeSize: 50,
textSpan: TextSpan(
text: '42',
style: TextStyle(color: Colors.black, fontSize: 10),
),
),
),
Decoration
import 'package:flutter/material.dart';
import 'dart:math' as math;
class BadgeDecoration extends Decoration {
final Color badgeColor;
final double badgeSize;
final TextSpan textSpan;
const BadgeDecoration({#required this.badgeColor, #required this.badgeSize, #required this.textSpan});
#override
BoxPainter createBoxPainter([onChanged]) => _BadgePainter(badgeColor, badgeSize, textSpan);
}
class _BadgePainter extends BoxPainter {
static const double BASELINE_SHIFT = 1;
static const double CORNER_RADIUS = 4;
final Color badgeColor;
final double badgeSize;
final TextSpan textSpan;
_BadgePainter(this.badgeColor, this.badgeSize, this.textSpan);
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
canvas.save();
canvas.translate(offset.dx + configuration.size.width - badgeSize, offset.dy);
canvas.drawPath(buildBadgePath(), getBadgePaint());
// draw text
final hyp = math.sqrt(badgeSize * badgeSize + badgeSize * badgeSize);
final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr, textAlign: TextAlign.center);
textPainter.layout(minWidth: hyp, maxWidth: hyp);
final halfHeight = textPainter.size.height / 2;
final v = math.sqrt(halfHeight * halfHeight + halfHeight * halfHeight) + BASELINE_SHIFT;
canvas.translate(v, -v);
canvas.rotate(0.785398); // 45 degrees
textPainter.paint(canvas, Offset.zero);
canvas.restore();
}
Paint getBadgePaint() => Paint()
..isAntiAlias = true
..color = badgeColor;
Path buildBadgePath() => Path.combine(
PathOperation.difference,
Path()..addRRect(RRect.fromLTRBAndCorners(0, 0, badgeSize, badgeSize, topRight: Radius.circular(CORNER_RADIUS))),
Path()
..lineTo(0, badgeSize)
..lineTo(badgeSize, badgeSize)
..close());
}

You can copy paste run full code below
You can paint before rotate and provide offset
code snippet
TextSpan span = new TextSpan(style: new TextStyle(color: Colors.black, fontSize: 10), text: "Point");
final textPainter1 = TextPainter(text: span, textDirection: TextDirection.ltr, textAlign: TextAlign.center);
textPainter1.layout(minWidth: hyp, maxWidth: hyp);
textPainter1.paint(canvas, Offset(0.0, 5.0));
textPainter.paint(canvas, Offset(3.0, 17.0));
canvas.translate(v, -v);
canvas.rotate(0.785398);
working demo
full code
import 'package:flutter/material.dart';
import 'dart:math' as math;
class BadgeDecoration extends Decoration {
final Color badgeColor;
final double badgeSize;
final TextSpan textSpan;
const BadgeDecoration({#required this.badgeColor, #required this.badgeSize, #required this.textSpan});
#override
BoxPainter createBoxPainter([onChanged]) => _BadgePainter(badgeColor, badgeSize, textSpan);
}
class _BadgePainter extends BoxPainter {
static const double BASELINE_SHIFT = 1;
static const double CORNER_RADIUS = 4;
final Color badgeColor;
final double badgeSize;
final TextSpan textSpan;
_BadgePainter(this.badgeColor, this.badgeSize, this.textSpan);
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
canvas.save();
canvas.translate(offset.dx + configuration.size.width - badgeSize, offset.dy);
canvas.drawPath(buildBadgePath(), getBadgePaint());
// draw text
final hyp = math.sqrt(badgeSize * badgeSize + badgeSize * badgeSize);
final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr, textAlign: TextAlign.center);
textPainter.layout(minWidth: hyp, maxWidth: hyp);
final halfHeight = textPainter.size.height / 2;
final v = math.sqrt(halfHeight * halfHeight + halfHeight * halfHeight) + BASELINE_SHIFT;
TextSpan span = new TextSpan(style: new TextStyle(color: Colors.black, fontSize: 10), text: "Point");
final textPainter1 = TextPainter(text: span, textDirection: TextDirection.ltr, textAlign: TextAlign.center);
textPainter1.layout(minWidth: hyp, maxWidth: hyp);
textPainter1.paint(canvas, Offset(0.0, 5.0));
textPainter.paint(canvas, Offset(3.0, 17.0));
canvas.translate(v, -v);
canvas.rotate(0.785398);
canvas.restore();
}
Paint getBadgePaint() => Paint()
..isAntiAlias = true
..color = badgeColor;
Path buildBadgePath() => Path.combine(
PathOperation.difference,
Path()..addRRect(RRect.fromLTRBAndCorners(0, 0, badgeSize, badgeSize, topRight: Radius.circular(CORNER_RADIUS))),
Path()
..lineTo(0, badgeSize)
..lineTo(badgeSize, badgeSize)
..close());
}
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> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
foregroundDecoration: const BadgeDecoration(
badgeColor: Colors.green,
badgeSize: 50,
textSpan: TextSpan(
text: '42',
style: TextStyle(color: Colors.black, fontSize: 10),
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

Related

How to make a button with ripple effect?

Here's what I've got
Positioned(
bottom: 15,
child: InkWell(
onTap: () {},
child: Material(
type: MaterialType.circle,
color: Color(0xFF246DE9),
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
'GO',
style: TextStyle(
fontSize: 25,
color: Colors.white,
),
),
),
),
),
),
Positioned(
bottom: 30,
child: CustomPaint(
size: Size(50, 50),
painter: CirclePainter(),
),
),
Circle Painter
class CirclePainter extends CustomPainter {
final _paint = Paint()
..color = Colors.white
..strokeWidth = 2
..style = PaintingStyle.stroke;
#override
void paint(Canvas canvas, Size size) {
canvas.drawOval(
Rect.fromLTWH(0, 0, size.width, size.height),
_paint,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
I created a stack, and tried to stack the white circle and the 'GO' button together, but I've no idea how to create that animation. The size of the white circle need to gradually increase, and it has to become invisible.
Can anyone help?
I have prepared one ripple class. Inspired by https://pub.dev/packages/ripple_animation. Please check it as below. Here, please update minRadius as per your child widget.
import 'dart:async';
import 'package:flutter/material.dart';
/// You can use whatever widget as a [child], when you don't need to provide any
/// [child], just provide an empty Container().
/// [delay] is using a [Timer] for delaying the animation, it's zero by default.
/// You can set [repeat] to true for making a paulsing effect.
class RippleAnimation extends StatefulWidget {
final Widget child;
final Duration delay;
final double minRadius;
final Color color;
final int ripplesCount;
final Duration duration;
final bool repeat;
const RippleAnimation({
required this.child,
required this.color,
Key? key,
this.delay = const Duration(milliseconds: 0),
this.repeat = false,
this.minRadius = 25,
this.ripplesCount = 5,
this.duration = const Duration(milliseconds: 2300),
}) : super(key: key);
#override
_RippleAnimationState createState() => _RippleAnimationState();
}
class _RippleAnimationState extends State<RippleAnimation>
with TickerProviderStateMixin {
late AnimationController _controller;
#override
void initState() {
_controller = AnimationController(
duration: widget.duration,
vsync: this,
lowerBound: 0.7,
upperBound: 1.0
);
// repeating or just forwarding the animation once.
Timer(widget.delay, () {
widget.repeat ? _controller?.repeat() : _controller?.forward();
});
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: CirclePainter(
_controller,
color: widget.color ?? Colors.black,
minRadius: 25,
wavesCount: widget.ripplesCount,
),
child: widget.child,
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
}
// Creating a Circular painter for clipping the rects and creating circle shapes
class CirclePainter extends CustomPainter {
CirclePainter(
this._animation, {
required this.minRadius,
this.wavesCount,
required this.color,
}) : super(repaint: _animation);
final Color color;
final double minRadius;
final wavesCount;
final Animation<double> _animation;
final _paint = Paint()
..color = Colors.white
..strokeWidth = 2
..style = PaintingStyle.stroke;
#override
void paint(Canvas canvas, Size size) {
final Rect rect = Rect.fromLTRB(0.0, 0.0, 100, 100);
for (int wave = 0; wave <= wavesCount; wave++) {
circle(canvas, rect, minRadius, wave, _animation.value, wavesCount);
}
}
// animating the opacity according to min radius and waves count.
void circle(Canvas canvas, Rect rect, double minRadius, int wave,
double value, int length) {
Color _color;
double r;
if (wave != 0) {
double opacity = (1 - ((wave - 1) / length) - value).clamp(0.0, 1.0);
_color = color.withOpacity(opacity);
r = minRadius * (1 + ((wave * value))) * value;
print("value >> r >> $r min radius >> $minRadius value>> $value");
final Paint paint = Paint()..color = _color;
paint..strokeWidth = 2
..style = PaintingStyle.stroke;
canvas.drawCircle(rect.center, r, paint);
}
}
#override
bool shouldRepaint(CirclePainter oldDelegate) => true;
}
Example:
RippleAnimation(
ripplesCount: 1,
repeat: true,
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle
),
width: 100,
height: 100,
child: Center(
child: Text(
'GO',
style: TextStyle(
fontSize: 25,
color: Colors.white,
),
),
),
),
color: Colors.white)
Please let me know if it doesn't work for you.

Flutter arrow in border

I want to add a lightweight navigation bar to switch between Login and Registration.
The result should look like this:
The arrow should indicate the current page selected.
Refer to this question, the UI in the question is similar to yours and the concept used in the answers can be tweaked to your desire.
This is what worked for me:
class TapBarDesign extends StatefulWidget {
const TapBarDesign({Key? key}) : super(key: key);
#override
_TapBarDesignState createState() => _TapBarDesignState();
}
class _TapBarDesignState extends State<TapBarDesign>
with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: Colors.blue,
appBar: AppBar(
toolbarHeight: 0,
backgroundColor: Colors.transparent,
elevation: 0,
bottom: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
indicator: ArrowTabBarIndicator(),
tabs: const <Widget>[
Tab(
child: Text(
'Page 1',
),
),
Tab(
child: Text(
'Page 2',
),
),
],
),
),
body: const TabBarView(
children: <Widget>[
Center(child: Text('Page 1')),
Center(child: Text('Page 2')),
],
),
),
);
}
}
class ArrowTabBarIndicator extends Decoration {
final BoxPainter _painter;
ArrowTabBarIndicator({double width = 20, double height = 10})
: _painter = _ArrowPainter(width, height);
#override
BoxPainter createBoxPainter([VoidCallback? onChanged]) => _painter;
}
class _ArrowPainter extends BoxPainter {
final Paint _paint;
final double width;
final double height;
_ArrowPainter(this.width, this.height)
: _paint = Paint()
..color = Colors.white
..strokeWidth = 1
..strokeCap = StrokeCap.round;
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
const pointMode = ui.PointMode.polygon;
if (cfg.size != null) {
final points = [
Offset(0, cfg.size!.height),
Offset(cfg.size!.width / 2 - (width / 2), cfg.size!.height) + offset,
Offset(cfg.size!.width / 2, (cfg.size!.height + height)) + offset,
Offset(cfg.size!.width / 2 + (width / 2), cfg.size!.height) + offset,
Offset(cfg.size!.width * 2, cfg.size!.height),
];
canvas.drawPoints(pointMode, points, _paint);
}
}
}

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: setState is not updating a custom built widget

I built a custom timer widget and am calling it through the main.dart file. My timer widget essentially takes an argument totalDuration and using that starts running the timer. In the main.dart file I created a variable called counter and am passing it as the value to totalDuration. Till here it works fine. Now when I create a button, which on being clicked increments the counter variable and calls the setState method, my counter varibale is being incremented but widget is not being rebuilt. Why is that so and how could I go about solving this problem? For reference I have attached the codes from my both my main and timer file here.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_app_test_counter/timer.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,
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> {
int _counter = 60;
void _incrementCounter() {
setState(() {
print(_counter);
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Expanded(
child: Timer(
totalDuration: _counter,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
timer.dart
import 'dart:ui';
import 'package:flutter/material.dart';
class Timer extends StatefulWidget {
final int totalDuration;
const Timer({Key key, this.totalDuration}) : super(key: key);
#override
_Timer createState() => _Timer();
}
class _Timer extends State<Timer> with TickerProviderStateMixin {
double _progress = 0.0;
bool _reversed = true;
bool _stopped = false;
Duration duration;
Animation<double> animation;
AnimationController controller;
String get _timeRemaining {
if (controller.lastElapsedDuration != null) {
duration = _reversed
? controller.duration - controller.lastElapsedDuration
: controller.lastElapsedDuration + Duration(seconds: 1);
}
return '${(duration.inHours).toString().padLeft(2, '0')}:${(duration.inMinutes % 60).toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}';
}
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: widget.totalDuration),
);
animation = Tween(begin: 1.0, end: 0.0).animate(controller)
..addListener(() {
setState(() {
_progress = animation.value;
});
});
controller.forward();
}
#override
void dispose() {
controller.dispose();
_stopped = !_stopped;
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _reversed = !_reversed,
child: Scaffold(
body: CustomPaint(
painter:
ShapePainter(progress: _progress, timeRemaining: _timeRemaining),
child: Container(),
),
),
);
}
}
class ShapePainter extends CustomPainter {
double progress;
String timeRemaining;
ShapePainter({this.progress, this.timeRemaining});
#override
void paint(Canvas canvas, Size size) {
final rectBounds = Rect.fromLTRB(0, 0, size.width, size.height);
final rectPaint = Paint()
..strokeWidth = 1
..style = PaintingStyle.fill
..color = Colors.orange;
canvas.drawRRect(
RRect.fromRectAndRadius(rectBounds, Radius.circular(10)),
rectPaint,
);
var paintProgressBar = Paint()
..color = Colors.white
..strokeWidth = 6
..strokeCap = StrokeCap.round;
Offset progressStartingPoint = Offset(42, size.height - 60);
Offset progressEndingPoint = Offset(size.width - 42, size.height - 60);
canvas.drawLine(
progressStartingPoint, progressEndingPoint, paintProgressBar);
var paintDoneBar = Paint()
..color = Colors.deepOrange
..strokeWidth = 6
..strokeCap = StrokeCap.round;
Offset doneStartingPoint = Offset(42, size.height - 60);
Offset doneEndingPoint =
Offset(((size.width - 84) * (1.0 - progress) + 42), size.height - 60);
canvas.drawLine(doneStartingPoint, doneEndingPoint, paintDoneBar);
final timerTextStyle = TextStyle(
color: Colors.indigo,
fontSize: 30,
);
final timerTextSpan = TextSpan(
text: timeRemaining,
style: timerTextStyle,
);
final timerTextPainter = TextPainter(
text: timerTextSpan,
textDirection: TextDirection.ltr,
);
timerTextPainter.layout(
minWidth: 0,
maxWidth: size.width,
);
final timerOffset = Offset(size.width / 2, size.height / 2 - 40);
timerTextPainter.paint(canvas, timerOffset);
final textStyle = TextStyle(
color: Colors.black,
fontSize: 30,
);
final textSpan = TextSpan(
text: 'time left',
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
textPainter.layout(
minWidth: 0,
maxWidth: size.width,
);
final offset = Offset((size.width - 20) / 2, (size.height - 20) / 2);
textPainter.paint(canvas, offset);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
One quick dirty fix is to add a key to the Timer widget
Timer(
key: UniqueKey(),
totalDuration: _counter,
),
Other way is to use didUpdateWidget function in your child Timer() widget.
In that function, you can make the changes.
#override
void didUpdateWidget(Timer oldWidget) {
print("didUpdateWidget called");
super.didUpdateWidget(oldWidget);
}
https://api.flutter.dev/flutter/widgets/State/didUpdateWidget.html

Flutter arrow tab bar?

How I can make the cursor of tab bar with an arrow
like this?
You can achieve your desire indicator using custom painter and tabindicator.
import 'package:flutter/material.dart';
class Delete extends StatefulWidget {
Delete({Key key}) : super(key: key);
#override
_DeleteState createState() => _DeleteState();
}
class _DeleteState extends State<Delete> with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
indicator: CircleTabIndicator(color: Colors.orange),
tabs: <Widget>[
Tab(
child: Text(
'fruits',
),
),
Tab(
child: Text(
'vegetables',
),
),
Tab(
child: Text(
'berries',
),
),
],
),
),
body: TabBarView(
children: <Widget>[
Center(child: Text('Tab 1')),
Center(child: Text('Tab 2')),
Center(child: Text('Tab 3')),
],
),
),
);
}
}
class CircleTabIndicator extends Decoration {
final BoxPainter _painter;
CircleTabIndicator({#required Color color})
: _painter = _CirclePainter(color);
#override
BoxPainter createBoxPainter([onChanged]) => _painter;
}
class _CirclePainter extends BoxPainter {
final Paint _paint;
_CirclePainter(Color color)
: _paint = Paint()
..color = color
..isAntiAlias = true;
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
Path _trianglePath = Path();
_trianglePath.moveTo(cfg.size.width / 2 - 10, cfg.size.height);
_trianglePath.lineTo(cfg.size.width / 2 + 10, cfg.size.height);
_trianglePath.lineTo(cfg.size.width / 2, cfg.size.height - 10);
_trianglePath.lineTo(cfg.size.width / 2 - 10, cfg.size.height);
_trianglePath.close();
canvas.drawPath(_trianglePath, _paint);
}
}
Building on Virens' answer I've made this version of the painter which addresses the issues in the comments and is null safe for use with newer versions of Flutter.
It may also serve to more clearly illustrate what's going on in the paint method.
import 'package:flutter/material.dart';
class ArrowTabBarIndicator extends Decoration {
final BoxPainter _painter;
ArrowTabBarIndicator({double width = 20, double height = 10})
: _painter = _ArrowPainter(width, height);
#override
BoxPainter createBoxPainter([VoidCallback? onChanged]) => _painter;
}
class _ArrowPainter extends BoxPainter {
final Paint _paint;
final double width;
final double height;
_ArrowPainter(this.width, this.height)
: _paint = Paint()
..color = Colors.white
..isAntiAlias = true;
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
Path _trianglePath = Path();
if (cfg.size != null){
Offset centerTop = Offset(cfg.size!.width / 2, cfg.size!.height - height) + offset;
Offset bottomLeft = Offset(cfg.size!.width / 2 - (width/2), cfg.size!.height) + offset;
Offset bottomRight = Offset(cfg.size!.width / 2 + (width/2), cfg.size!.height) + offset;
_trianglePath.moveTo(bottomLeft.dx, bottomLeft.dy);
_trianglePath.lineTo(bottomRight.dx, bottomRight.dy);
_trianglePath.lineTo(centerTop.dx, centerTop.dy);
_trianglePath.lineTo(bottomLeft.dx, bottomLeft.dy);
_trianglePath.close();
canvas.drawPath(_trianglePath, _paint);
}
}
}
The main issue with the original answer was that it didn't take into account the 'offset' parameter which controls in this case which tab the indicator is drawn under.