I am trying to recreate this UI:
Collapsed
Expanded
I first created the custom container shape using a ClipPath with a CustomClipper:
class FolderClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
Path path = Path();
Offset c1 = Offset(size.width * .0825, size.height * .048);
Offset c2 = Offset(size.width * .021, size.height * .0075);
Offset end = Offset(size.width * .12, size.height * 0);
path.moveTo(0, size.height * 0.0775);
path.cubicTo(c1.dx, c1.dy, c2.dx, c2.dy, end.dx, end.dy);
path.lineTo(size.width * .71, end.dy);
Offset c1_2 = Offset(size.width * .829, size.height * .0002);
Offset c2_2 = Offset(size.width * .7497, size.height * .06495);
Offset end_2 = Offset(size.width * .8602, size.height * .0652);
path.cubicTo(c1_2.dx, c1_2.dy, c2_2.dx, c2_2.dy, end_2.dx, end_2.dy);
path.lineTo(size.width, end_2.dy);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
return path;
}
#override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return false;
}
This draws the shape as expected when it takes up the full screen:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.only(top: 16.0),
child: ClipPath(
clipper: FolderClipper(),
child: Container(
height: MediaQuery.of(context).size.height,
decoration: const BoxDecoration(
color: Colors.grey,
),
child: const Center(
child: Text('Text'),
),
),
),
),
);
shape drawn with a large height
Approach 1
To put this shape into an expandable list, I used the containers with different content (child) in a CustomScrollView.
I had to use a collapsed and expanded container, which will basically be the same custom clipped container that I created earlier in the full screen, except the expanded one will have the grid of cards. I set a height when the container is in the collapsed state because otherwise it won't show if it has no children (and if I add a child that takes a large space, the collapsed item will take up a large height, which is not what I want). Setting that height however distorts the shape and so it doesn't look right (it only looks right when it has a relatively large height). I put the custom shape in a stack and added a container with a color that matches the preceding container's color under it so that it looks as if it's stacked.
Issues:
Given different heights, the custom shape becomes distorted and does not have the same proportions (the curves don't look the same for different heights).
There are lines that appear between widgets (same colored widgets that touch) that I think might be a bug in flutter (I'm using flutter 2.10.4 and it will be difficult to upgrade). I think these lines are due to the height and pixel density not quite matching and so the background can be seen.
[kinda worked] I tried to fix this by using Transform.translate with a y-axis offset of -8 on the background container that has the color of the preceding list item. This seems to work for the most part (since I don't care about the bottom of the container being shifted up). This however doesn't fix the elevation issue (issue #3).
I also tried to fix this by adding a similar CustomScrollView below the current one so that the background color would always match the expandable panel, but how will I know the size of the panel when it expands so that the scrollview with the background colors would match (I had their scroll controllers linked), but that wasn't the best and added complexity.
The elevation is not working on the last item (which is supposed to be an inverse-ish shape that I haven't done yet, I'm just reusing the shape I have to test it out). I added a border to try and make it more prominent, but that background line (between same colored widgets) is messing things up.
In general the shadow/elevation for each item in the list was not showing. I tried wrapping the container in a Material widget and gave it an elevation and I also tried using BoxShadow.
Here's what the code looks like after my fix attempts:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'folder_clipper.dart';
import 'folder_clipper2.dart';
class MyAttempt extends StatelessWidget {
MyAttempt({Key? key}) : super(key: key);
RxInt _selectedIndex = (-1).obs;
final List<Color> colors = [
Color(0xFFBDBDBD),
Colors.grey,
Color(0xFF757575)
];
final int optionsCount = 5;
final int gridOptionCount = 2;
ScrollController sc = ScrollController();
//ScrollController scBack = ScrollController();
#override
Widget build(BuildContext context) {
// sc.addListener(() {
// scBack.jumpTo(sc.offset);
// });
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Stack(
children: [
// CustomScrollView(
// controller: sc,
// slivers: [
// SliverList(
// delegate: SliverChildBuilderDelegate(
// (BuildContext context, int index) {
// return Container(
// decoration: BoxDecoration(
// color: index == 0
// ? Colors.transparent
// : colors[(index - 1) % colors.length],
// ),
// width: double.infinity,
// height: _selectedIndex.value == index
// ? null
// : (Get.height * .1),
// child: Obx(() => _selectedIndex.value == index
// ? gridView(index)
// : const SizedBox.shrink()),
// );
// },
// childCount: optionsCount,
// ),
// ),
// SliverFillRemaining(
// child: GestureDetector(
// onTap: () {
// if (_selectedIndex.value == 2)
// _selectedIndex.value = -1;
// else
// _selectedIndex.value = 2;
// },
// child: Stack(
// children: [
// Container(
// color: colors[(optionsCount - 1) % colors.length],
// width: double.infinity,
// ),
// ClipPath(
// clipper: FolderClipper2(),
// child: Material(
// elevation: 10,
// child: Container(
// decoration: BoxDecoration(
// color: colors[(optionsCount - 1) % colors.length],
// border: const Border(
// top:
// BorderSide(width: 1.0, color: Colors.black),
// ),
// ),
// ),
// ),
// ),
// ],
// ),
// ),
// )
// ],
// ),
CustomScrollView(
controller: sc,
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Stack(
children: [
Transform.translate(
offset: Offset(0, -8),
child: Container(
decoration: BoxDecoration(
color: index == 0
? Colors.transparent
: colors[(index - 1) % colors.length],
),
width: double.infinity,
height: ((Get.height * .1)),
),
),
ClipPath(
clipper: FolderClipper(),
child: Material(
child: GestureDetector(
onTap: () {
if (_selectedIndex.value == index) {
_selectedIndex.value = -1;
} else {
_selectedIndex.value = index;
}
},
child: Obx(
() => Stack(
children: [
Container(
decoration: BoxDecoration(
color: colors[index % colors.length],
),
height: _selectedIndex.value != index
? (Get.height * .1)
: null,
width: double.infinity,
child: Obx(() =>
_selectedIndex.value == index
? gridView(index)
: const SizedBox.shrink()),
),
Container(
margin: EdgeInsets.only(
top: Get.height * .025,
left: Get.width * .125),
child: Text(
"Option $index",
style: const TextStyle(
fontWeight: FontWeight.bold),
),
),
],
),
),
),
),
),
],
);
},
childCount: optionsCount,
),
),
SliverFillRemaining(
child: Stack(
children: [
Transform.translate(
offset: Offset(0, -8),
child: Container(
color: colors[(optionsCount - 1) % colors.length],
width: double.infinity,
),
),
ClipPath(
clipper: FolderClipper2(),
child: Material(
elevation: 10,
child: Container(
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(
color: Colors.black,
spreadRadius: 5,
blurRadius: 5,
offset:
Offset(0, -8), // changes position of shadow
),
],
color: colors[(optionsCount - 1) % colors.length],
border: const Border(
top: BorderSide(width: 1.0, color: Colors.black),
),
),
),
),
),
],
),
)
],
),
],
),
);
}
gridView(index) {
return GridView.count(
shrinkWrap: true,
primary: true,
padding: EdgeInsets.only(
left: Get.width * .15,
right: Get.width * .15,
top: Get.width * .15,
bottom: 16),
crossAxisSpacing: Get.width * .075, //24,
mainAxisSpacing: Get.width * .075, //16,
crossAxisCount: 2,
children: <Widget>[
for (int i = 0; i < (gridOptionCount * index); i++)
Card(
elevation: 10,
child: Container(
//width: Get.width * .05,
//height: Get.width * .05,
color: Colors.white,
child: Center(
child: Container(
child: Text(
"option $i",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
)),
),
],
);
}
}
and here is what it looks like, it's kind of working, but doesn't look right mainly due to the CustomClipper being distorted due to the height of the container and the shadows not working as expected:
collapsed state
expanded state
Approach 2
To put this shape into an expandable list, I used the expandable package on pub.dev in a CustomScrollView.
I set a header and expanded widget, which will basically be the same custom clipped container that I created, except the expanded one will have the grid of cards. I set a height when the container is in the header only state because otherwise it will be so short if it has no children (and if I add a child that takes a large space, the collapsed item will take up a large height, which is not what I want). Setting that height however distorts the shape and so it doesn't look right (it only looks right when it has a relatively large height). I put the custom shape in a stack and added a container with a color that matches the preceding container's color under it so that it looks as if it's stacked. I did not use the collapsed widget (set it to SizedBox.shrink).
Here's the code:
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'folder_clipper.dart';
import 'folder_clipper2.dart';
class MyAttempt2 extends StatelessWidget {
MyAttempt2({Key? key}) : super(key: key);
RxInt _selectedIndex = (-1).obs;
final List<Color> colors = [
Color(0xFFBDBDBD),
Colors.grey,
Color(0xFF757575)
];
final int optionsCount = 5;
final int gridOptionCount = 2;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Stack(
children: [
ExpandableNotifier(
child: Stack(
children: [
Transform.translate(
offset: Offset(0, -8),
child: Container(
decoration: BoxDecoration(
color: index == 0
? Colors.transparent
: colors[(index - 1) % colors.length],
),
width: double.infinity,
height: ((Get.height * .1)),
),
),
ClipPath(
clipper: FolderClipper(),
child: Container(
//height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
color: colors[index % colors.length],
),
child: ScrollOnExpand(
scrollOnExpand: true,
scrollOnCollapse: false,
child: ExpandablePanel(
theme: const ExpandableThemeData(
headerAlignment:
ExpandablePanelHeaderAlignment.center,
tapBodyToCollapse: true,
),
header: Container(
height: ((Get.height * .1)),
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
"ExpandablePanel",
style: Theme.of(context)
.textTheme
.bodyText2,
)),
),
collapsed: SizedBox.shrink(),
// Text(
// loremIpsum,
// softWrap: true,
// maxLines: 2,
// overflow: TextOverflow.ellipsis,
// ),
expanded: SingleChildScrollView(
child: gridView(index)),
builder: (_, collapsed, expanded) {
return Padding(
padding: EdgeInsets.only(
left: 0, right: 0, bottom: 0),
child: Expandable(
collapsed: collapsed,
expanded: expanded,
theme: const ExpandableThemeData(
crossFadePoint: 0),
),
);
},
),
),
),
//margin: EdgeInsets.zero,
//clipBehavior: Clip.antiAlias,
),
],
),
),
],
);
},
childCount: optionsCount,
),
),
SliverFillRemaining(
child: Stack(
children: [
Container(
color: colors[(optionsCount - 1) % colors.length],
width: double.infinity,
),
ClipPath(
clipper: FolderClipper2(),
child: Material(
elevation: 10,
child: Container(
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(
color: Colors.black,
spreadRadius: 5,
blurRadius: 5,
offset: Offset(0, -8), // changes position of shadow
),
],
color: colors[(optionsCount - 1) % colors.length],
border: const Border(
top: BorderSide(width: 1.0, color: Colors.black),
),
),
),
),
),
],
),
)
],
),
);
}
gridView(index) {
return GridView.count(
shrinkWrap: true,
primary: true,
padding: EdgeInsets.only(
left: Get.width * .15,
right: Get.width * .15,
top: Get.width * .15,
bottom: 16),
crossAxisSpacing: Get.width * .075, //24,
mainAxisSpacing: Get.width * .075, //16,
crossAxisCount: 2,
children: <Widget>[
for (int i = 0; i < (gridOptionCount * index); i++)
Card(
elevation: 10,
child: Container(
//width: Get.width * .05,
//height: Get.width * .05,
color: Colors.white,
child: Center(
child: Container(
child: Text(
"option $i",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
)),
),
],
);
}
}
and this is what it looks like:
collapsed (header only) state
expanded state
Similar issues as approach 1.
UPDATE
I managed to add a height factor to the CustomClipper so that all offsets are relative to the height I based it off of and that seemed to get the shape to be consistent! Will probably do the same for the width.
Here's the new code:
class FolderClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
print("${size.width} - ${size.height}");
double heightFactor = size.height / 667;
double widthFactor = size.width / 375;
Path path = Path();
Offset c1 = Offset(size.width * .0825, size.height * .048);
Offset c2 = Offset(size.width * .021, size.height * .0075);
Offset end = Offset(size.width * .12, size.height * 0);
// if (size.height < 100)
// path.moveTo(0, size.height * .5);
// else if (size.height < 300)
// path.moveTo(0, size.height * .15);
// else
path.moveTo(0, size.height * 0.0775 / heightFactor);
path.cubicTo(c1.dx, c1.dy / heightFactor, c2.dx, c2.dy / heightFactor,
end.dx, end.dy / heightFactor);
path.lineTo(size.width * .71, end.dy / heightFactor);
Offset c1_2 = Offset(size.width * .829, size.height * .0002);
Offset c2_2 = Offset(size.width * .7497, size.height * .06495);
Offset end_2;
// if (size.height < 100)
// end_2 = Offset(size.width * .8602, size.height * .45);
// else if (size.height < 300)
// end_2 = Offset(size.width * .8602, size.height * .15);
// else
end_2 = Offset(size.width * .8602, size.height * .0652);
path.cubicTo(c1_2.dx, c1_2.dy / heightFactor, c2_2.dx,
c2_2.dy / heightFactor, end_2.dx, end_2.dy / heightFactor);
path.lineTo(size.width, end_2.dy / heightFactor);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
return path;
}
#override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return false;
}
This is what it looks like now:
expanded state - issue with SliverFillRemaining line on top and shadows
So I believe the main issue remaining here is with the shadow and the space between the last item and the SliverFillRemainingSection (and its shadow as well)
I was wondering how can I achieve something like this, this kind of shape?
Here:
class Draw extends StatelessWidget {
const Draw({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
width: 300,
height: 170,
child: Stack(
clipBehavior: Clip.none,
children: [
Center(
child: ClipPath(
clipper: MyClipper(),
child: Container(
width: 300,
height: 170,
color: Colors.black,
),
),
),
Positioned(
left: -20,
child: SizedBox(
height: 170,
child: Center(
child: Material(
elevation: 3,
color: Colors.black,
shadowColor: Colors.white,
borderRadius: BorderRadius.circular(10),
child: const SizedBox(width: 80, height: 80),
),
),
),
)
],
),
),
),
);
}
}
class MyClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
final p = Path();
double facotr = 30;
p.moveTo(10, 0);
p.lineTo(10, size.height - 20);
p.quadraticBezierTo(10, size.height, 20, size.height);
p.lineTo(size.width - facotr, size.height - facotr);
p.lineTo(size.width, size.height / 2);
p.lineTo(size.width - facotr, facotr);
p.lineTo(20, 0);
p.quadraticBezierTo(10, 0, 10, 10);
return p;
}
#override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => true;
}
There is the Paint widget, but also someone made a tool for that wich makes it convienent.
Shape Maker
I need your help and assistance to how to implement the curved tab as shown in below picture .
This can easily be achieved by custompainter or customclipper. here is an example. I believe you can improve it to better.
Widget for bottom navbar:
Positioned(
bottom: 0,
child: Container(
height: 100,
width: MediaQuery.of(context).size.width,
color: Colors.amber,
child: Stack(children: [
AnimatedPositioned(
curve: Curves.bounceOut,
duration: const Duration(milliseconds: 300),
top: 10,
left: (MediaQuery.of(context).size.width / 3) * tab,
child: SizedBox(
width: MediaQuery.of(context).size.width / 3,
height: 50,
child: CustomPaint(
size: Size(MediaQuery.of(context).size.width / 3, 50),
painter: TabPainter(),
),
),
),
Positioned(
top: 59,
child: Container(
height: 2,
width: MediaQuery.of(context).size.width,
color: Colors.white,
),
),
Positioned(
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: 80,
child: Row(
children: [
Expanded(
child: IconButton(
icon: const Icon(Icons.abc),
onPressed: () {
setState(() {
tab = 0;
});
},
),
),
Expanded(
child: IconButton(
icon: const Icon(Icons.add_box),
onPressed: () {
setState(() {
tab = 1;
});
},
),
),
Expanded(
child: IconButton(
icon: const Icon(Icons.air),
onPressed: () {
setState(() {
tab = 2;
});
},
),
)
],
),
),
),
]),
),
)
Painter code:
class TabPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint()
..color = Colors.white
..style = PaintingStyle.fill;
Path path = Path()
..moveTo(0, size.height)
..quadraticBezierTo(
size.width * 0.2, size.height, size.width * 0.15, size.height * 0.5)
..quadraticBezierTo(size.width * 0.1, 0, size.width * 0.3, 0)
..lineTo(size.width * 0.7, 0)
..quadraticBezierTo(
size.width * 0.9, 0, size.width * 0.85, size.height * 0.5)
..quadraticBezierTo(
size.width * 0.8, size.height, size.width * 1.1, size.height)
..close();
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
tab is just a integer variable
Output
Can someone help me with this ClipPath in Flutter?
I can't make the bottom border.
Also, how can I make it visible as a Row child?
I noticed that it works in Container, but it doesn't in when I drag the Container(or Flexible) in Row or without the widgets.
UPDATE
This is the return when the ClipPath is not visible
Scaffold(
body: SafeArea(
child: Row(
children: [
Flexible(
child: ClipPath(
clipper: AuthClipPath(),
child: Container(
height: size.height * .75,
decoration: BoxDecoration(gradient: gradient),
child: authForm(),
),
),
)
],
),
),
)
And this is the return when the ClipPath is visible
Scaffold(
body: SafeArea(
child: Container(
child: ClipPath(
clipper: AuthClipPath(),
child: Container(
height: size.height * .75,
decoration: BoxDecoration(gradient: gradient),
child: authForm(),
),
),
)
)
This is clipper class from ClipPath
class AuthClipPath extends CustomClipper<Path> {
#override
Path getClip(Size size) {
Path path = Path();
path.lineTo(0.0, size.height - 30);
Offset firstControlPoint = Offset(size.width / 4, size.height);
Offset firstPoint = Offset(size.width / 2, size.height);
path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy,
firstPoint.dx, firstPoint.dy);
Offset secondControlPoint = Offset(size.width / 4 * 3, size.height);
Offset secondPoint = Offset(size.width, size.height - 30);
path.quadraticBezierTo(secondControlPoint.dx, secondControlPoint.dy,
secondPoint.dx, secondPoint.dy);
path.lineTo(size.width, 0.0);
path.close();
return path;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
The Flexible Widget is not relevant for the visibility.
Thank you!
Use your AuthClipPath() like this inside Row()
home: Scaffold(
body: SafeArea(
child: Row(
children: [
Flexible(
child: ClipPath(
clipper: AuthClipPath(),
child: Container(
height: 300,
color: Colors.amber.shade200,
),
),
),
],
),
),
),
You can achieve the wave effect like this. And you can always tweak according to your needs.
Path path = Path();
path.moveTo(size.width, 0);
path.lineTo(0, 0);
path.quadraticBezierTo(
0, size.height * 0.4500000, 0, size.height * 0.6000000);
path.cubicTo(
size.width * 0.3000000,
size.height * 0.9000000,
size.width * 0.7000000,
size.height * 0.3000000,
size.width,
size.height * 0.6000000);
path.quadraticBezierTo(
size.width, size.height * 0.4500000, size.width, 0);
path.close();
return path;
I want to create a drop down menu like the image below, Which is opened by touching and dragging and closing by touching the outside.
before dragging
after dragging
Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false
),
body: Stack(
children: <Widget>[
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(20))
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Align(
alignment: Alignment.bottomCenter,
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Divider(
color: Colors.blueGrey[500],
height: 10,
indent: 5,
),
),
Icon(FontAwesomeIcons.angleDoubleDown,size: 15,color: Colors.blueGrey[500],)
],
),
)
],
),
),
Center(child: Text('List View'),)
],
)
)
I want to change the height, but I encounter overflow error!
What is the best way to make this widget?
Can I do this within the AppBar?
You can do this in some ways, but one that came up in mind immediately was to use a CustomPaint widget with your own CustomPainter at the top of a Stack so you can actually keep your other widgets below the scrolled bar.
I've tried to replicate what you've shown on the images but feel free to tweak it to your needs.
const kMinScrollBarHeight = 20.0;
class MyScreen extends StatefulWidget {
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
double _scrollBarOffset = 0.0;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color.fromRGBO(13, 23, 35, 1.0),
appBar: AppBar(
backgroundColor: const Color.fromRGBO(255, 72, 18, 1.0),
),
body: Stack(children: <Widget>[
GestureDetector(
onVerticalDragUpdate: (tapDetails) => setState(() => _scrollBarOffset = tapDetails.globalPosition.dy),
child: Stack(
children: <Widget>[
Center(
child: Text(
'My screen widgets',
style: TextStyle(color: Colors.white),
),
),
Stack(
children: <Widget>[
Positioned(
bottom: MediaQuery.of(context).size.height -
max(_scrollBarOffset,
MediaQuery.of(context).padding.top + kToolbarHeight + kMinScrollBarHeight),
child: CustomPaint(
painter: MyDraggable(),
child: Container(
height: MediaQuery.of(context).size.height,
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FlutterLogo(
size: 100.0,
),
Text('Flutter is awesome'),
],
),
),
),
),
],
),
],
),
),
]),
);
}
}
class MyDraggable extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()..color = Colors.white;
final Radius cornerRadius = Radius.circular(20.0);
final double lineMargin = 30.0;
// Draw slider
canvas.drawRRect(
RRect.fromLTRBAndCorners(0.0, 0.0, size.width, size.height,
bottomLeft: cornerRadius, bottomRight: cornerRadius),
paint);
paint.color = Colors.black.withAlpha(64);
paint.strokeWidth = 1.5;
// Draw triangle
canvas.drawPoints(
PointMode.polygon,
[
Offset((size.width / 2) - 5.0, size.height - 10.0),
Offset((size.width / 2) + 5.0, size.height - 10.0),
Offset((size.width / 2), size.height - 5.0),
Offset((size.width / 2) - 5.0, size.height - 10.0),
],
paint);
// Draw line
canvas.drawLine(Offset(lineMargin, size.height - kMinScrollBarHeight),
Offset(size.width - lineMargin, size.height - kMinScrollBarHeight), paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}