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);
}
}
}
Related
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: ...,
)
I'm trying to Drag a widget on top of an InteractiveViewer.
The code works fine when the scale is 1.0.
However, if zoomed in, when I drag the circle:
the feedback widget is offset by a few pixels to the bottom right
when I let go, the circle moves up
Why does that happen?
Here's a demo illustrating what I'm talking about:
Below is the code of this app
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
enum _Action { scale, pan }
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
const _defaultMarkerSize = 48.0;
class _MyHomePageState extends State<MyHomePage> {
Offset _pos = Offset.zero; // Position could go from -1 to 1 in both directions
_Action action; // pan or pinch, useful to know if we need to scale down pin
final _transformationController = TransformationController();
#override
Widget build(BuildContext context) {
final scale = _transformationController.value.getMaxScaleOnAxis();
final size = _defaultMarkerSize / scale;
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: InteractiveViewer(
transformationController: _transformationController,
maxScale: 5,
minScale: 1,
child: Stack(
children: [
Center(child: Image.asset('image/board.png')),
DraggablePin(
pos: _pos,
size: size,
onDragEnd: (details) {
final matrix = Matrix4.inverted(_transformationController.value);
final height = AppBar().preferredSize.height;
final sceneY = details.offset.dy - height;
final viewportPoint = MatrixUtils.transformPoint(
matrix,
Offset(details.offset.dx, sceneY) + Offset(_defaultMarkerSize / 2, _defaultMarkerSize / 2),
);
final screenSize = MediaQuery.of(context).size;
final x = viewportPoint.dx * 2 / screenSize.width - 1;
final y = viewportPoint.dy * 2 / screenSize.height - 1;
setState(() {
_pos = Offset(x, y);
});
},
),
],
),
onInteractionStart: (details) {
// No need to call setState as we don't need to rebuild
action = null;
},
onInteractionUpdate: (details) {
if (action == null) {
if (details.scale == 1)
action = _Action.pan;
else
action = _Action.scale;
}
if (action == _Action.scale) {
// Need to resize the pins so that they keep the same size
setState(() {});
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.restore),
onPressed: () {
setState(() {
_pos = Offset.zero;
});
},
),
);
}
}
class DraggablePin extends StatelessWidget {
final Offset pos;
final double size;
final void Function(DraggableDetails) onDragEnd;
const DraggablePin({this.pos, this.size, this.onDragEnd, Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final offset = size / 2;
Widget pinWidget = Pin(size);
final screenSize = MediaQuery.of(context).size;
final height = screenSize.height - AppBar().preferredSize.height;
pinWidget = Draggable(
child: pinWidget,
feedback: Pin(_defaultMarkerSize),
childWhenDragging: Container(),
onDragEnd: onDragEnd,
);
return Positioned(
top: pos.dy * height / 2 + height / 2 - offset,
left: pos.dx * screenSize.width / 2 + screenSize.width / 2 - offset,
child: pinWidget,
);
}
}
class Pin extends StatelessWidget {
const Pin(this.size, {Key key}) : super(key: key);
final double size;
#override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Center(
child: Ink(
decoration: ShapeDecoration(
color: Colors.green,
shape: const CircleBorder(),
),
child: IconButton(
constraints: BoxConstraints.tightFor(width: size, height: size),
padding: const EdgeInsets.all(0),
iconSize: size,
splashRadius: size / 2,
splashColor: Colors.white,
icon: Container(),
onPressed: () {},
),
),
),
);
}
}
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.
I have a problem to implement the CustomPainter class, don't know my mistake in every single tutorial they did the same like me hope someone know the solution.
class _MyHomepageState extends State<MyHomepage> {
var _name;
final nameCon = new TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'App',
style: TextStyle(fontWeight: FontWeight.w900),
),
backgroundColor: Colors.brown[500],
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: nameCon,
decoration: InputDecoration(hintText: "Name"),
),
RaisedButton(
onPressed: () {
setState(() {
_name = nameCon.text;
});
},
child: Text("Los"),
),
Text("hier $_name "),
Expanded(
child: LayoutBuilder(
builder: (_, constraints) => Container(
width: constraints.widthConstraints().maxWidth,
height: constraints.heightConstraints().maxHeight,
color: Colors.yellow,
child: CustomPaint(painter: Thermometer()),
),
),
),
],
),
),
),
);
}
}
class Thermometer extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.brown
..style = PaintingStyle.stroke
..style=PaintingStyle.fill
..strokeWidth = 4;
canvas.drawRRect(
RRect.fromRectAndRadius(Rect.fromLTWH(-140, -60, 40, 290),
Radius.circular(20)),
paint,);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false ;
}
You can copy paste run full code below
please change left from -140 to 0 or other value
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, -60, 40, 290), Radius.circular(20)),
paint,
);
working demo
working demo
full code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
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> {
var _name;
final nameCon = new TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'App',
style: TextStyle(fontWeight: FontWeight.w900),
),
backgroundColor: Colors.brown[500],
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: nameCon,
decoration: InputDecoration(hintText: "Name"),
),
RaisedButton(
onPressed: () {
setState(() {
_name = nameCon.text;
});
},
child: Text("Los"),
),
Text("hier $_name "),
Expanded(
child: LayoutBuilder(
builder: (_, constraints) => Container(
width: constraints.widthConstraints().maxWidth,
height: constraints.heightConstraints().maxHeight,
color: Colors.yellow,
child: CustomPaint(painter: Thermometer()),
),
),
),
],
),
),
),
);
}
}
class Thermometer extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.brown
..style = PaintingStyle.stroke
..style = PaintingStyle.fill
..strokeWidth = 4;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, -60, 40, 290), Radius.circular(20)),
paint,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
I want to create a tabBar with triangular indicator, just like the following
Here is the code.
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
indicator: <What should I put in here?>,
tabs: <Widget>[
Tab(text: "Tab1"),
Tab(text: "Tab2"),
Tab(text: "Tab3")
],
),
),
body: TabBarView(
children: <Widget>[
Text("Content1"),
Text("Content2"),
Text("Content3")
],
),
),
);
}
I have tried BoxDecoration with image. But the image won't show until you click on the tab. For FlutterLogoDecoration, it seems no way to change the icon.
Thank you very much.
You can copy paste run full code below
You need custom indicator
code snippet
TabBar(
indicator: TriangleTabIndicator(color: kMainColor),
...
class TriangleTabIndicator extends Decoration {
...
class DrawTriangle extends BoxPainter {
Working demo
full code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
const kMainColor = Color(0xFF573851);
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Flutter Custom Tab Indicator Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Colors.white,
bottom: TabBar(
indicator: TriangleTabIndicator(color: kMainColor),
tabs: <Widget>[
Tab(
child: Text('fruits', style: TextStyle(color: kMainColor)),
),
Tab(
child: Text('vegetables', style: TextStyle(color: kMainColor)),
),
Tab(
child: Text('berries', style: TextStyle(color: kMainColor)),
),
],
),
),
body: TabBarView(
children: <Widget>[
Center(child: Text('Tab 1')),
Center(child: Text('Tab 2')),
Center(child: Text('Tab 3')),
],
),
),
);
}
}
class TriangleTabIndicator extends Decoration {
final BoxPainter _painter;
TriangleTabIndicator({#required Color color, #required double radius})
: _painter = DrawTriangle(color);
#override
BoxPainter createBoxPainter([onChanged]) => _painter;
}
class DrawTriangle extends BoxPainter {
Paint _paint;
DrawTriangle(Color color) {
_paint = Paint()
..color = color
..style = PaintingStyle.fill;
}
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
final Offset triangleOffset =
offset + Offset(cfg.size.width / 2, cfg.size.height - 10);
var path = Path();
path.moveTo(triangleOffset.dx, triangleOffset.dy);
path.lineTo(triangleOffset.dx + 10, triangleOffset.dy + 10);
path.lineTo(triangleOffset.dx - 10, triangleOffset.dy + 10);
path.close();
canvas.drawPath(path, _paint);
}
}