How to make custom Bottom Navigation Bar in Flutter - flutter

I have a design of Bottom Navigation Bar like this:
I try my best to create custom of Bottom Navigation Bar with this code:
class FABBottomAppBarItem {
FABBottomAppBarItem({this.iconData, this.text});
IconData iconData;
String text;
}
class FABBottomAppBar extends StatefulWidget {
FABBottomAppBar({
this.items,
this.centerItemText,
this.height: 60.0,
this.iconSize: 24.0,
this.backgroundColor,
this.color,
this.selectedColor,
this.notchedShape,
this.onTabSelected,
}) {
assert(this.items.length == 2 || this.items.length == 4);
}
final List<FABBottomAppBarItem> items;
final String centerItemText;
final double height;
final double iconSize;
final Color backgroundColor;
final Color color;
final Color selectedColor;
final NotchedShape notchedShape;
final ValueChanged<int> onTabSelected;
#override
State<StatefulWidget> createState() => FABBottomAppBarState();
}
class FABBottomAppBarState extends State<FABBottomAppBar> {
int _selectedIndex = 0;
_updateIndex(int index) {
widget.onTabSelected(index);
setState(() {
_selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
List<Widget> items = List.generate(widget.items.length, (int index) {
return _buildTabItem(
item: widget.items[index],
index: index,
onPressed: _updateIndex,
);
});
items.insert(items.length >> 1, _buildMiddleTabItem());
return BottomAppBar(
shape: widget.notchedShape,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items,
),
color: widget.backgroundColor,
);
}
Widget _buildMiddleTabItem() {
return Expanded(
child: SizedBox(
height: widget.height,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(height: widget.iconSize),
Text(
widget.centerItemText ?? '',
style: TextStyle(color: widget.color),
),
],
),
),
);
}
Widget _buildTabItem({
FABBottomAppBarItem item,
int index,
ValueChanged<int> onPressed,
}) {
Color color = _selectedIndex == index ? widget.selectedColor : widget.color;
return Expanded(
child: SizedBox(
height: widget.height,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () => onPressed(index),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(item.iconData, color: color, size: widget.iconSize),
Text(
item.text,
style: TextStyle(color: color),
)
],
),
),
),
),
);
}
}
And I implemented that custom like this:
bottomNavigationBar: FABBottomAppBar(
centerItemText: 'เสา',
color: Colors.grey,
backgroundColor: Colors.white,
selectedColor: Colors.red,
notchedShape: CircularNotchedRectangle(),
onTabSelected: _onTapped,
items: [
FABBottomAppBarItem(iconData: Icons.home, text: 'หน้าแรก'),
FABBottomAppBarItem(iconData: Icons.search, text: 'ค้นหา'),
FABBottomAppBarItem(iconData: Icons.account_circle, text: 'โปรไฟล์'),
FABBottomAppBarItem(iconData: Icons.more_horiz, text: 'อื่นๆ'),
],
),
body: _list[_page],
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: FloatingActionButton(
onPressed: () {
},
child: Icon(Icons.add),
elevation: 2.0,
),
),
And the result of that code is:
How to make my Custom Nav Bar like the design? Because in the design, the FAB/Asset/Icon inside the Bottom Navigation Bar and have curved upwards as I marked with arrows in the design.

I edited CircularNotchedRectangle.
Use CircularOuterNotchedRectangle instead of CircularNotchedRectangle.
PS. I added extraOffset param for extra thick. but it's not working exactly correct. But I just wanted to show you how to approach.
class CircularOuterNotchedRectangle extends NotchedShape {
/// Creates a [CircularOuterNotchedRectangle].
///
/// The same object can be used to create multiple shapes.
const CircularOuterNotchedRectangle({this.extraOffset = 10.0});
final double extraOffset;
/// Creates a [Path] that describes a rectangle with a smooth circular notch.
///
/// `host` is the bounding box for the returned shape. Conceptually this is
/// the rectangle to which the notch will be applied.
///
/// `guest` is the bounding box of a circle that the notch accommodates. All
/// points in the circle bounded by `guest` will be outside of the returned
/// path.
///
/// The notch is curve that smoothly connects the host's top edge and
/// the guest circle.
// TODO(amirh): add an example diagram here.
#override
Path getOuterPath(Rect host, Rect guest) {
if (guest == null || !host.overlaps(guest)) return Path()..addRect(host);
// The guest's shape is a circle bounded by the guest rectangle.
// So the guest's radius is half the guest width.
final double notchRadius = guest.width / 2.0;
// We build a path for the notch from 3 segments:
// Segment A - a Bezier curve from the host's top edge to segment B.
// Segment B - an arc with radius notchRadius.
// Segment C - a Bezier curve from segment B back to the host's top edge.
//
// A detailed explanation and the derivation of the formulas below is
const double s1 = 15.0;
const double s2 = 1.0;
final double r = notchRadius + extraOffset/2;
final double a = -1.0 * r - s2;
final double b = host.top + guest.center.dy;
final double n2 = math.sqrt(b * b * r * r * (a * a + b * b - r * r));
final double p2xA = ((a * r * r) - n2) / (a * a + b * b);
final double p2xB = ((a * r * r) + n2) / (a * a + b * b);
final double p2yA = math.sqrt(r * r - p2xA * p2xA) - extraOffset/2;
final double p2yB = math.sqrt(r * r - p2xB * p2xB) - extraOffset/2;
final List<Offset> p = List<Offset>(6);
// p0, p1, and p2 are the control points for segment A.
p[0] = Offset(a - s1, b);
p[1] = Offset(a, b);
p[2] = p2yA > p2yB ? Offset(p2xA, -p2yA) : Offset(p2xB, p2yB);
// p3, p4, and p5 are the control points for segment B, which is a mirror
// of segment A around the y axis.
p[3] = Offset(-1.0 * p[2].dx, -p[2].dy);
p[4] = Offset(-1.0 * p[1].dx, p[1].dy);
p[5] = Offset(-1.0 * p[0].dx, p[0].dy);
// translate all points back to the absolute coordinate system.
for (int i = 0; i < p.length; i += 1) p[i] += guest.center;
return Path()
..moveTo(host.left, -host.top)
..lineTo(p[0].dx, p[0].dy)
..quadraticBezierTo(p[1].dx, p[1].dy, p[2].dx, -p[2].dy)
..arcToPoint(
p[3],
radius: Radius.circular(notchRadius),
clockwise: true,
)
..quadraticBezierTo(p[4].dx, p[4].dy, p[5].dx, p[5].dy)
..lineTo(host.right, host.top)
..lineTo(host.right, host.bottom)
..lineTo(host.left, host.bottom)
..close();
}
}

Please try this one, its may helps you
Scaffold(
backgroundColor: Colors.blueAccent,
floatingActionButton: Padding(
padding: EdgeInsets.only(top: 20),
child: SizedBox(
height: 70,
width: 70,
child: FloatingActionButton(
backgroundColor: Colors.transparent,
elevation: 0,
onPressed: () {},
child: Container(
height: 75,
width: 75,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 4),
shape: BoxShape.circle,
color: Colors.red
),
child: Icon(Icons.add, size: 40),
),
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: new Container(
height: 80.0,
color: Colors.white,
padding: new EdgeInsets.only(top: 20.0),
child: new Theme(
data: Theme.of(context).copyWith(
// sets the background color of the `BottomNavigationBar`
canvasColor: Colors.white,
// sets the active color of the `BottomNavigationBar` if `Brightness` is light
primaryColor: Colors.red,
bottomAppBarColor: Colors.green,
textTheme: Theme
.of(context)
.textTheme
.copyWith(caption: new TextStyle(color: Colors.grey))), // sets the inactive color of the `BottomNavigationBar`
child:
new BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex:0 ,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.home),
title: new Text('Home'),
backgroundColor: Colors.black
),
BottomNavigationBarItem(
icon: new Icon(Icons.search),
title: new Text('Search'),
),
BottomNavigationBarItem(
icon: Icon(Icons.bookmark_border,color: Colors.transparent,),
title: Text('Center')
),
BottomNavigationBarItem(
icon: Icon(Icons.perm_identity),
title: Text('Person')
),
BottomNavigationBarItem(
icon: Icon(Icons.more_horiz),
title: Text('More')
),
]),
),
),
)

I would probably wrap your FloatingActionButton in a ClipRRect and Container like
floatingActionButton: ClipRRect(
borderRadius: BorderRadius.circular(80),
child: Container(
color: Colors.black,
padding: EdgeInsets.all(8),
child: FloatingActionButton(
onPressed: () {},
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
Giving as color same of the BottomNavigationBoar. It should work

Related

How to change selected letter when scrolling alphabet scroll view?

I have implemented https://pub.dev/packages/alphabet_scroll_view in my project, I edited it for my own customization but when I scroll list with items scroll with letters is not changing.
https://imgur.com/a/ResAVrE here is a video to better understand what I want to achieve. That changes on selected letter is just my tap on them.
Here is other link if above is not working https://drive.google.com/file/d/1Oy6XWalXwXM-yqk7IZU0Av2_jEL3ZNk1/view?usp=sharing. I hope this will working.
I want to change selected letter dynamically when I am scrolling items.
Here is my code:
import 'package:alphabet_scroll_view/src/meta.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart';
enum LetterAlignment { left, right }
class AlphabetScrollView extends StatefulWidget {
AlphabetScrollView(
{Key? key,
required this.list,
this.alignment = LetterAlignment.right,
this.isAlphabetsFiltered = true,
this.overlayWidget,
required this.selectedTextStyle,
required this.unselectedTextStyle,
this.itemExtent = 40,
required this.itemBuilder})
: super(key: key);
/// List of Items should be non Empty
/// and you must map your
/// ```
/// List<T> to List<AlphaModel>
/// e.g
/// List<UserModel> _list;
/// _list.map((user)=>AlphaModel(user.name)).toList();
/// ```
/// where each item of this ```list``` will be mapped to
/// each widget returned by ItemBuilder to uniquely identify
/// that widget.
final List<AlphaModel> list;
/// ```itemExtent``` specifies the max height of the widget returned by
/// itemBuilder if not specified defaults to 40.0
final double itemExtent;
/// Alignment for the Alphabet List
/// can be aligned on either left/right side
/// of the screen
final LetterAlignment alignment;
/// defaults to ```true```
/// if specified as ```false```
/// all alphabets will be shown regardless of
/// whether the item in the [list] exists starting with
/// that alphabet.
final bool isAlphabetsFiltered;
/// Widget to show beside the selected alphabet
/// if not specified it will be hidden.
/// ```
/// overlayWidget:(value)=>
/// Container(
/// height: 50,
/// width: 50,
/// alignment: Alignment.center,
/// color: Theme.of(context).primaryColor,
/// child: Text(
/// '$value'.toUpperCase(),
/// style: TextStyle(fontSize: 20, color: Colors.white),
/// ),
/// )
/// ```
final Widget Function(String)? overlayWidget;
/// Text styling for the selected alphabet by which
/// we can customize the font color, weight, size etc.
/// ```
/// selectedTextStyle:
/// TextStyle(
/// fontWeight: FontWeight.bold,
/// color: Colors.black,
/// fontSize: 20
/// )
/// ```
final TextStyle selectedTextStyle;
/// Text styling for the unselected alphabet by which
/// we can customize the font color, weight, size etc.
/// ```
/// unselectedTextStyle:
/// TextStyle(
/// fontWeight: FontWeight.normal,
/// color: Colors.grey,
/// fontSize: 18
/// )
/// ```
final TextStyle unselectedTextStyle;
/// The itemBuilder must return a non-null widget and the third paramter id specifies
/// the string mapped to this widget from the ```[list]``` passed.
Widget Function(BuildContext, int, String) itemBuilder;
#override
_AlphabetScrollViewState createState() => _AlphabetScrollViewState();
}
class _AlphabetScrollViewState extends State<AlphabetScrollView> {
void init() {
widget.list
.sort((x, y) => x.key.toLowerCase().compareTo(y.key.toLowerCase()));
_list = widget.list;
setState(() {});
/// filter Out AlphabetList
if (widget.isAlphabetsFiltered) {
List<String> temp = [];
alphabets.forEach((letter) {
AlphaModel? firstAlphabetElement = _list.firstWhereOrNull(
(item) => item.key.toLowerCase().startsWith(letter.toLowerCase()));
if (firstAlphabetElement != null) {
temp.add(letter);
}
});
_filteredAlphabets = temp;
} else {
_filteredAlphabets = alphabets;
}
calculateFirstIndex();
setState(() {});
}
#override
void initState() {
init();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
listController.addListener(() {
print('scrolling ${listController.position.pixels}');
if (listController.position.pixels >=
listController.position.maxScrollExtent) {
print('achieved end');
} else if (listController.position.pixels <=
listController.position.minScrollExtent) {
print('achieved start');
}
});
});
if (listController.hasClients) {
maxScroll = listController.position.maxScrollExtent;
}
super.initState();
}
ScrollController listController = ScrollController();
final _selectedIndexNotifier = ValueNotifier<int>(0);
final positionNotifer = ValueNotifier<Offset>(Offset(0, 0));
final Map<String, int> firstIndexPosition = {};
List<String> _filteredAlphabets = [];
final letterKey = GlobalKey();
List<AlphaModel> _list = [];
bool isLoading = false;
bool isFocused = false;
final key = GlobalKey();
#override
void didUpdateWidget(covariant AlphabetScrollView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.list != widget.list ||
oldWidget.isAlphabetsFiltered != widget.isAlphabetsFiltered) {
_list.clear();
firstIndexPosition.clear();
init();
}
}
int getCurrentIndex(double vPosition) {
double kAlphabetHeight = letterKey.currentContext!.size!.height;
return (vPosition ~/ kAlphabetHeight);
}
/// calculates and Maintains a map of
/// [letter:index] of the position of the first Item in list
/// starting with that letter.
/// This helps to avoid recomputing the position to scroll to
/// on each Scroll.
void calculateFirstIndex() {
_filteredAlphabets.forEach((letter) {
AlphaModel? firstElement = _list.firstWhereOrNull(
(item) => item.key.toLowerCase().startsWith(letter));
if (firstElement != null) {
int index = _list.indexOf(firstElement);
firstIndexPosition[letter] = index;
}
});
}
void scrolltoIndex(int x, Offset offset) {
int index = firstIndexPosition[_filteredAlphabets[x].toLowerCase()]!;
final scrollToPostion = widget.itemExtent * index;
if (index != null) {
listController.animateTo((scrollToPostion),
duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
}
positionNotifer.value = offset;
}
void onVerticalDrag(Offset offset) {
int index = getCurrentIndex(offset.dy);
if (index < 0 || index >= _filteredAlphabets.length) return;
_selectedIndexNotifier.value = index;
setState(() {
isFocused = true;
});
scrolltoIndex(index, offset);
}
double? maxScroll;
#override
Widget build(BuildContext context) {
return Stack(
children: [
ListView.builder(
controller: listController,
scrollDirection: Axis.vertical,
itemCount: _list.length,
physics: ClampingScrollPhysics(),
itemBuilder: (_, x) {
return ConstrainedBox(
constraints: BoxConstraints(maxHeight: widget.itemExtent),
child: widget.itemBuilder(_, x, _list[x].key));
}),
Align(
alignment: widget.alignment == LetterAlignment.left
? Alignment.centerLeft
: Alignment.centerRight,
child: Container(
key: key,
padding: const EdgeInsets.symmetric(horizontal: 2),
child: SingleChildScrollView(
child: GestureDetector(
onVerticalDragStart: (z) => onVerticalDrag(z.localPosition),
onVerticalDragUpdate: (z) => onVerticalDrag(z.localPosition),
onVerticalDragEnd: (z) {
setState(() {
isFocused = false;
});
},
child: ValueListenableBuilder<int>(
valueListenable: _selectedIndexNotifier,
builder: (context, int selected, Widget? child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
_filteredAlphabets.length,
(x) => GestureDetector(
key: x == selected ? letterKey : null,
onTap: () {
_selectedIndexNotifier.value = x;
scrolltoIndex(x, positionNotifer.value);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: widget.alignment == LetterAlignment.left ? Radius.circular(20) : Radius.circular(0),
bottomRight: widget.alignment == LetterAlignment.left ? Radius.circular(20) : Radius.circular(0),
topLeft: widget.alignment == LetterAlignment.right ? Radius.circular(20) : Radius.circular(0),
bottomLeft: widget.alignment == LetterAlignment.right ? Radius.circular(20) : Radius.circular(0)
),
color: selected == x ? Color(0xFFFA3B71) : Colors.transparent
),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 2),
child: Text(
_filteredAlphabets[x].toUpperCase(),
style: selected == x
? widget.selectedTextStyle
: widget.unselectedTextStyle,
// style: TextStyle(
// fontSize: 12,
// fontWeight: selected == x
// ? FontWeight.bold
// : FontWeight.normal),
),
),
),
));
}),
),
),
),
),
!isFocused
? Container()
: ValueListenableBuilder<Offset>(
valueListenable: positionNotifer,
builder:
(BuildContext context, Offset position, Widget? child) {
return Positioned(
right:
widget.alignment == LetterAlignment.right ? 40 : null,
left:
widget.alignment == LetterAlignment.left ? 40 : null,
top: position.dy,
child: widget.overlayWidget == null
? Container()
: widget.overlayWidget!(_filteredAlphabets[
_selectedIndexNotifier.value]));
})
],
);
}
}
class AlphaModel {
final String key;
final String? secondaryKey;
AlphaModel(this.key, {this.secondaryKey});
}
If you want to test my code you can install package linked above, I just changed customization for background of letter scrollview.
You can do Like this :
Check this Example:
Expanded(
child: AlphabetScrollView(
list:
_filterList.map((e) => AlphaModel(e.employeeName)).toList(),
// isAlphabetsFiltered: false,
alignment: LetterAlignment.right,
itemExtent: 90,
unselectedTextStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.normal,
color: _filterList.length > 5
? Colors.black
: Colors.transparent),
selectedTextStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: _filterList.length > 5
? Colors.red
: Colors.transparent),
overlayWidget: (value) => Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.star,
size: 50,
color: Colors.red,
),
Container(
height: 50,
width: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
// color: Theme.of(context).primaryColor,
),
alignment: Alignment.center,
child: Text(
'$value'.toUpperCase(),
style: TextStyle(fontSize: 18, color: Colors.white),
),
),
],
),
itemBuilder: (_, index, id) {
return InkWell(
onTap: () => Get.toNamed(EmployeeInfo.route,
arguments: _filterList[index]),
child: Container(
margin: EdgeInsets.only(right: 20),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
children: [
SizedBox(
width: 10,
),
Stack(
children: [
CircularProfileAvatar(
SystemConfiguration.baseUrl +
SystemConfiguration.getEmployePhoto +
_filterList[index]
.id, //sets image path, it should be a URL string. default value is empty string, if path is empty it will display only initials
radius: 30, // sets radius, default 50.0
backgroundColor: Colors
.transparent, // sets background color, default Colors.white
borderWidth:
2, // sets border, default 0.0
borderColor: Colors
.blue, // sets border color, default Colors.white
cacheImage:
true, // allow widget to cache image against provided url
imageFit: BoxFit.cover,
// sets on tap
showInitialTextAbovePicture: true,
errorWidget: (BuildContext context,
String data, dynamic v) {
return Icon(
FeatherIcons.user,
size: 40,
color: Colors.white,
);
}, // setting it true will show initials text above profile picture, default false
),
Positioned(
bottom: 5,
right: 0,
child: Padding(
padding:
const EdgeInsets.only(left: 3.0),
child: Container(
width: 17,
height: 17,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFFe0f2f1)),
child: Icon(
Icons.circle,
size: 16.0,
// ignore: unnecessary_null_comparison
color: (_filterList[index]
.attendanceStatus
.length >
0 &&
_filterList[index]
.InOffice
.contains("IN"))
? Colors.green
: Colors.red[500],
),
),
))
],
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
_filterList[index].employeeName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.roboto(
fontSize: 13.0,
fontWeight: FontWeight.w500),
),
],
),
),
),
Expanded(
flex: 2,
child: Stack(
children: [
Positioned(
left: 44,
child: ElevatedButton(
onPressed: () => _launchURL(
_filterList[index].mobile1),
child: Icon(
Icons.call,
size: 20.0,
color: Colors.white,
),
style: ElevatedButton.styleFrom(
shape: CircleBorder(),
padding: EdgeInsets.all(2),
primary:
Colors.blue, // <-- Button color
onPrimary:
Colors.red, // <-- Splash color
),
),
),
ElevatedButton(
onPressed: () {
// _textMe(_filterList[index].mobile1); mobile text
Get.toNamed(ChatViwer.route,
arguments: {
"employe_name":
_filterList[index]
.employeeName,
"employe_id":
_filterList[index].id
});
},
child: Icon(
Icons.message,
size: 20.0,
color: Colors.white,
),
style: ElevatedButton.styleFrom(
shape: CircleBorder(),
padding: EdgeInsets.all(2),
primary:
Colors.blue, // <-- Button color
onPrimary:
Colors.red, // <-- Splash color
),
),
],
),
)

How to create this type of bottom navigation bar with curved background? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
Improve this question
I have tried some packages and stack with image but it is not working for me. Any help would be great.
Background image with curve is not working.
Does this work for you?
You should be able to do that with a custom NotchedShape:
class CustomNotchedShape extends NotchedShape {
const CustomNotchedShape();
#override
Path getOuterPath(Rect host, Rect guest) {
final x = math.min(host.width / 20, host.height / 3);
final guestMargin = guest == null ? 0 : 1.0;
if (guest == null || !host.overlaps(guest)) {
guest = Rect.fromCenter(
center: Offset(host.width / 2, host.top), width: 0, height: 0);
}
num degToRad(num deg) => deg * (math.pi / 180.0);
return Path()
..moveTo(host.left, host.bottom)
..quadraticBezierTo(
host.left + x, host.bottom, host.left + x, host.bottom - x)
..lineTo(host.left + x, host.top + x)
..quadraticBezierTo(host.left + x, host.top, host.left + 2 * x, host.top)
..lineTo(guest.left, host.top)
..arcTo(
Rect.fromCenter(
center: guest.center,
width: guest.width + 2 * guestMargin,
height: guest.width + 2 * guestMargin),
degToRad(180),
degToRad(-180),
false)
..lineTo(host.right - 2 * x, host.top)
..quadraticBezierTo(
host.right - x, host.top, host.right - x, host.top + x)
..lineTo(host.right - x, host.bottom - x)
..quadraticBezierTo(host.right - x, host.bottom, host.right, host.bottom)
..close();
}
}
Then, you use this CustomNotchedShape as the shape of your BottomAppBar:
class MyBottomNavigationBar extends HookWidget {
#override
Widget build(BuildContext context) {
final _currentIndex = useState(1);
void navigateTo(int index) => _currentIndex.value = index;
bool active(int index) => _currentIndex.value == index;
return BottomAppBar(
color: Theme.of(context).primaryColor,
shape: CustomNotchedShape(),
child: Container(
height: 50,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
onPressed: () => navigateTo(0),
icon: Icon(Icons.home_outlined),
color: active(0) ? Colors.white : Colors.black,
),
IconButton(
onPressed: () => navigateTo(1),
icon: Icon(Icons.horizontal_split_outlined),
color: active(1) ? Colors.white : Colors.black,
),
Padding(
padding: EdgeInsets.only(top: 24.0),
child: Text('New task'),
),
IconButton(
onPressed: () => navigateTo(2),
icon: Icon(Icons.access_time_outlined),
color: active(2) ? Colors.white : Colors.black,
),
IconButton(
onPressed: () => navigateTo(3),
icon: Icon(Icons.settings_outlined),
color: active(3) ? Colors.white : Colors.black,
),
],
),
),
),
),
);
}
}
Full source code
For easy copy-paste.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
scaffoldBackgroundColor: Color(0xffd3ccca),
primaryColor: Color(0xff86736c),
accentColor: Color(0xff76504e),
),
title: 'Flutter Demo',
home: Scaffold(
body: MyContent(),
bottomNavigationBar: MyBottomNavigationBar(),
floatingActionButton: FloatingActionButton(
mini: true,
onPressed: () => print('Adding new task...'),
child: Icon(Icons.add),
),
floatingActionButtonLocation:
FloatingActionButtonLocation.miniCenterDocked,
),
),
);
}
class MyContent extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(child: Text('Does this work for you?'));
}
}
class MyBottomNavigationBar extends HookWidget {
#override
Widget build(BuildContext context) {
final _currentIndex = useState(1);
void navigateTo(int index) => _currentIndex.value = index;
bool active(int index) => _currentIndex.value == index;
return BottomAppBar(
color: Theme.of(context).primaryColor,
shape: CustomNotchedShape(),
child: Container(
height: 50,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
onPressed: () => navigateTo(0),
icon: Icon(Icons.home_outlined),
color: active(0) ? Colors.white : Colors.black,
),
IconButton(
onPressed: () => navigateTo(1),
icon: Icon(Icons.horizontal_split_outlined),
color: active(1) ? Colors.white : Colors.black,
),
Padding(
padding: EdgeInsets.only(top: 24.0),
child: Text('New task'),
),
IconButton(
onPressed: () => navigateTo(2),
icon: Icon(Icons.access_time_outlined),
color: active(2) ? Colors.white : Colors.black,
),
IconButton(
onPressed: () => navigateTo(3),
icon: Icon(Icons.settings_outlined),
color: active(3) ? Colors.white : Colors.black,
),
],
),
),
),
),
);
}
}
class CustomNotchedShape extends NotchedShape {
const CustomNotchedShape();
#override
Path getOuterPath(Rect host, Rect guest) {
final x = math.min(host.width / 20, host.height / 3);
final guestMargin = guest == null ? 0 : 1.0;
if (guest == null || !host.overlaps(guest)) {
guest = Rect.fromCenter(
center: Offset(host.width / 2, host.top), width: 0, height: 0);
}
num degToRad(num deg) => deg * (math.pi / 180.0);
return Path()
..moveTo(host.left, host.bottom)
..quadraticBezierTo(
host.left + x, host.bottom, host.left + x, host.bottom - x)
..lineTo(host.left + x, host.top + x)
..quadraticBezierTo(host.left + x, host.top, host.left + 2 * x, host.top)
..lineTo(guest.left, host.top)
..arcTo(
Rect.fromCenter(
center: guest.center,
width: guest.width + 2 * guestMargin,
height: guest.width + 2 * guestMargin),
degToRad(180),
degToRad(-180),
false)
..lineTo(host.right - 2 * x, host.top)
..quadraticBezierTo(
host.right - x, host.top, host.right - x, host.top + x)
..lineTo(host.right - x, host.bottom - x)
..quadraticBezierTo(host.right - x, host.bottom, host.right, host.bottom)
..close();
}
}

How can I align little circles equally into a big circle?

I've made one big rounded container and now I want to align different tiny circle containers equally into it. How can I do that without adjusting the position all the time? Is there a better way so that even when I change the size of the tiny circles, the position remain equally?
this is a piece of code:
return Material(
color: Colors.black,
child: Center(
child: Stack(
textDirection: TextDirection.ltr,
children: [
DialBottomCircle,
Positioned(
//maybe as buttons
child: CircleNumbers(
textNumber: '0',
),
bottom: 0,
top: 260.0,
left: 245.0,
),
Positioned(
child: CircleNumbers(
textNumber: '9',
),
bottom: -70.0,
top: 225.0,
left: 175.0),
use CustomMultiChildLayout, something like this:
#override
Widget build(BuildContext context) {
var numbers = List.generate(10, (index) => '$index');
return Material(
shape: CircleBorder(),
color: Colors.red,
child: CustomMultiChildLayout(
delegate: FooDelegate(numbers.length),
children: [
for (var i = 0; i < numbers.length; i++)
LayoutId(
id: i,
child: Material(
elevation: 4,
color: Colors.white,
shape: CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
splashColor: Colors.orange,
onTap: () => print('${numbers[i]} pressed'),
child: FittedBox(
child: Text(numbers[i]),
),
),
),
),
],
),
);
);
class FooDelegate extends MultiChildLayoutDelegate {
final int numChildren;
FooDelegate(this.numChildren);
#override
void performLayout(Size size) {
final s = Size.square(size.shortestSide / 6.5);
final radius = (size.shortestSide - s.shortestSide) * 0.45;
final childConstraints = BoxConstraints.tight(s);
final delta = ((size - s) as Offset) / 2;
for (var i = 0; i < numChildren; i++) {
layoutChild(i, childConstraints);
var angle = i * math.pi / 6;
var offset = Offset(math.cos(angle), math.sin(angle)) * radius;
positionChild(i, offset + delta);
}
}
#override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => true;
}

How to set Flutter showMenu starting point

I would like to know how to change the origin point of the popUpMenu, start the popup right above the bottom app bar, no matter the count of items. Aligned to the right end of the screen. Something that is like (for example)
Positioned(right: 0, bottom: bottomAppBarHeight)
Here is a screenshot of the design placement of popUpMenu I want to achieve:
Here is a screenshot of the current placement of the popUpMenu (Please ignore other design differences as they are irrelevant):
The code used is as follows :
onPressed: () {
final RelativeRect position =
buttonMenuPosition(context);
showMenu(context: context, position: position, items: [
PopupMenuItem<int>(
value: 0,
child: Text('Working a lot harder'),
),
PopupMenuItem<int>(
value: 1,
child: Text('Working a lot less'),
),
PopupMenuItem<int>(
value: 1,
child: Text('Working a lot smarter'),
),
]);
},
The buttonMenuPosition function code:
RelativeRect buttonMenuPosition(BuildContext context) {
final bool isEnglish =
LocalizedApp.of(context).delegate.currentLocale.languageCode == 'en';
final RenderBox bar = context.findRenderObject() as RenderBox;
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
const Offset offset = Offset.zero;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints(
bar.localToGlobal(
isEnglish
? bar.size.centerRight(offset)
: bar.size.centerLeft(offset),
ancestor: overlay),
bar.localToGlobal(
isEnglish
? bar.size.centerRight(offset)
: bar.size.centerLeft(offset),
ancestor: overlay),
),
offset & overlay.size,
);
return position;
}
Changing the offset didn't work.
Well, I couldn't achieve it with the showMenu function, but it was achievable by using a PopUpMenuButton and setting its offset to the height of the bottom app bar.
Here is an example code:
PopupMenuButton<int>(
offset: const Offset(0, -380),
itemBuilder: (context) => [
PopupMenuItem<int>(
value: 0,
child: PopUpMenuTile(
isActive: true,
icon: Icons.fiber_manual_record,
title:'Stop recording',
)),
PopupMenuItem<int>(
value: 1,
child: PopUpMenuTile(
isActive: true,
icon: Icons.pause,
title: 'Pause recording',
)),
PopupMenuItem<int>(
value: 2,
child: PopUpMenuTile(
icon: Icons.group,
title: 'Members',
)),
PopupMenuItem<int>(
value: 3,
child: PopUpMenuTile(
icon: Icons.person_add,
title: 'Invite members',
)),
],
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.more_vert,
color: Colors.white60),
Text(translate('more'),
style: Theme.of(context)
.textTheme
.caption)
],
),
)
The code to the custom pop up menu tile is as follows even though it is not relevant to the question:
class PopUpMenuTile extends StatelessWidget {
const PopUpMenuTile(
{Key key,
#required this.icon,
#required this.title,
this.isActive = false})
: super(key: key);
final IconData icon;
final String title;
final bool isActive;
#override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Icon(icon,
color: isActive
? Theme.of(context).accentColor
: Theme.of(context).primaryColor),
const SizedBox(
width: 8,
),
Text(
title,
style: Theme.of(context).textTheme.headline4.copyWith(
color: isActive
? Theme.of(context).accentColor
: Theme.of(context).primaryColor),
),
],
);
}
}
I was able to get similar behavior as follows:
class _SettingsPopupMenuState extends State<SettingsPopupMenu> {
static const Map<String, IconData> _options = {
'Settings' : Icons.favorite_border,
'Share' : Icons.bookmark_border,
'Logout' : Icons.share,
};
void _showPopup(BuildContext context) async {
//*get the render box from the context
final RenderBox renderBox = context.findRenderObject() as RenderBox;
//*get the global position, from the widget local position
final offset = renderBox.localToGlobal(Offset.zero);
//*calculate the start point in this case, below the button
final left = offset.dx;
final top = offset.dy + renderBox.size.height;
//*The right does not indicates the width
final right = left + renderBox.size.width;
//*show the menu
final value = await showMenu<String>(
// color: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0)
),
context: context,
position: RelativeRect.fromLTRB(left, top, right, 0.0),
items: _options.entries.map<PopupMenuEntry<String>>((entry) {
return PopupMenuItem(
value: entry.key,
child: SizedBox(
// width: 200, //*width of popup
child: Row(
children: [
Icon(entry.value, color: Colors.redAccent),
const SizedBox(width: 10.0),
Text(entry.key)
],
),
),
);
}).toList()
);
print(value);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Popup Settings'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//*encloses the widget from which the relative positions will be
//*taken in a builder, in this case a button
Builder(
builder: (context) {
return RawMaterialButton(
fillColor: Colors.indigo,
constraints: const BoxConstraints(minWidth: 200),
onPressed: () => _showPopup(context),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0)
),
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: const Text('Show Menu', style: TextStyle(color:
Colors.white)),
);
}
),
],
),
),
);
}
}
I know this is old but your code is one line from working and I couldn't see an answer that covered it.
You just need to change the following in the buttonMenuPosition function:
return position;
to
return position.shift(offset);
The one and only working code is here
Offset offs = Offset(0,0);
final RenderBox button = context.findRenderObject()! as RenderBox;
final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(offs, ancestor: overlay),
button.localToGlobal(button.size.bottomRight(Offset.zero) + offs, ancestor: overlay),
),
Offset.zero & overlay.size,
);
This is taken from

How to make the Bottom Navigation Bar overlay the main page in Flutter?

I'm building an app that has a Bottom Navigation Bar and I want to provide to this bar the ability to overlay the main page when I change its height in order to prevent the contents in the main page to be reduced to fit the available area. How can I do that?
I think that explaining it by images could be better, so there we go:
This is how the main page looks like together with the Bottom Navigation Bar:
Normal App:
When I click in the Yellow Circle Button (middle button), the BottomAppBar has to increase its height, but when I do this, the main content reduces its size to fit the available space.
Weird App:
I want to prevent it. How can I do it?
This is my code:
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double _appBarHeight = 60.0;
void _openBottomAppBar() {
setState(() {
if (_appBarHeight > 60) {
_appBarHeight = 60.0;
} else {
_appBarHeight = 300.0;
}
});
}
#override
Widget build(BuildContext context) {
final Shader textGradient = LinearGradient(
colors: <Color>[Theme.of(context).primaryColorDark, Theme.of(context).primaryColorLight],
).createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0));
return Scaffold(
body: Container(
child: Column(
children: <Widget>[
Expanded(
flex: 3,
child: ClipShadowPath(
clipper: ApplicationBar(),
shadow: Shadow(blurRadius: 10),
child: LayoutBuilder(builder: (context, constraints) {
return Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage(
'images/tiny-squares.png',
),
repeat: ImageRepeat.repeat,
),
),
),
Container(
decoration: BoxDecoration(
backgroundBlendMode: BlendMode.multiply,
gradient: LinearGradient(
begin: const Alignment(-1.0, 0.0),
end: const Alignment(0.6, 0.0),
colors: <Color>[Theme.of(context).primaryColorDark, Theme.of(context).primaryColorLight],
),
),
),
Positioned(
right: constraints.biggest.width * 0.1,
top: constraints.biggest.height * 0.35,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
),
child: Material(
elevation: 4.0,
clipBehavior: Clip.antiAliasWithSaveLayer,
type: MaterialType.circle,
color: Colors.transparent,
)),
Padding(
padding: EdgeInsets.only(top: 8),
),
],
))
],
);
})),
),
Expanded(
flex: 4,
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: Container(
padding: EdgeInsets.only(top: 16, left: 64),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 8),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
),
Padding(
padding: EdgeInsets.only(top: 8),
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
),
),
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
),
)
],
),
),
],
),
))
],
)),
],
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: FloatingActionButton(
onPressed: _openBottomAppBar,
tooltip: 'Increment',
child: Text('A'),
elevation: 10.0,
),
bottomNavigationBar: NotchedBottomBar(
centerItemText: '',
height: _appBarHeight,
color: Colors.grey,
selectedColor: Theme.of(context).accentColor,
notchedShape: FollowerNotchedShape(inverted: true),
items: [
NotchedBottomBarItem(iconData: Icons.home, text: 'This'),
NotchedBottomBarItem(iconData: Icons.payment, text: 'is'),
NotchedBottomBarItem(iconData: Icons.input, text: 'Bottom'),
NotchedBottomBarItem(iconData: Icons.settings_applications, text: 'Bar'),
],
),
);
}
}
class ApplicationBar extends CustomClipper<Path> {
#override
Path getClip(Size size) {
final path = Path();
path.lineTo(size.width, 0);
path.lineTo(size.width, 0.84 * size.height);
path.lineTo(0.72 * size.width, 0.983 * size.height);
final firstEndPoint = new Offset(0.7 * size.width, (0.986 + 0.0065) * size.height);
final controlPoint = new Offset(0.675 * size.width, 0.989 * size.height);
path.quadraticBezierTo(firstEndPoint.dx, firstEndPoint.dy, controlPoint.dx, controlPoint.dy);
path.lineTo(0, 0.87 * size.height);
path.close();
return path;
}
#override
bool shouldReclip(YEXApplicationBar oldClipper) {
return oldClipper != this;
}
}
Code for the Bottom Bar
import 'package:flutter/material.dart';
import 'dart:math' as math;
class FollowerNotchedShape extends CircularNotchedRectangle {
int _inverterMultiplier;
#override
Path getOuterPath(Rect host, Rect guest) {
if (!host.overlaps(guest)) return Path()..addRect(host);
final double notchRadius = guest.width / 2.0;
const double s1 = 15.0;
const double s2 = 1.0;
final double r = notchRadius;
final double a = -1.0 * r - s2;
final double b = host.top - guest.center.dy;
final double n2 = math.sqrt(b * b * r * r * (a * a + b * b - r * r));
final double p2xA = ((a * r * r) - n2) / (a * a + b * b);
final double p2xB = ((a * r * r) + n2) / (a * a + b * b);
final double p2yA = math.sqrt(r * r - p2xA * p2xA);
final double p2yB = math.sqrt(r * r - p2xB * p2xB);
final List<Offset> p = List<Offset>(6);
// p0, p1, and p2 are the control points for segment A.
p[0] = Offset(a - s1, b);
p[1] = Offset(a, b);
final double cmp = b < 0 ? -1.0 : 1.0;
p[2] =
cmp * p2yA > cmp * p2yB ? Offset(p2xA, _inverterMultiplier * p2yA) : Offset(p2xB, _inverterMultiplier * p2yB);
// p3, p4, and p5 are the control points for segment B, which is a mirror
// of segment A around the y axis.
p[3] = Offset(-1.0 * p[2].dx, p[2].dy);
p[4] = Offset(-1.0 * p[1].dx, p[1].dy);
p[5] = Offset(-1.0 * p[0].dx, p[0].dy);
// translate all points back to the absolute coordinate system.
for (int i = 0; i < p.length; i += 1) p[i] += guest.center;
final Path path = Path()
..moveTo(host.left, -host.top)
..lineTo(p[0].dx, -p[0].dy)
..quadraticBezierTo(p[1].dx, p[1].dy, p[2].dx, p[2].dy);
if (guest.height == guest.width) {
// circle
path.arcToPoint(
p[3],
radius: Radius.circular(notchRadius),
clockwise: _inverterMultiplier == 1 ? false : true,
);
} else {
// stadium
path
..arcToPoint(
(_inverterMultiplier == 1 ? guest.bottomLeft : guest.topLeft) + Offset(guest.height / 2, 0), // here
radius: Radius.circular(guest.height / 2),
clockwise: _inverterMultiplier == 1 ? false : true,
)
..lineTo(guest.right - guest.height / 2, (_inverterMultiplier == 1 ? guest.bottom : guest.top)) // here
..arcToPoint(
p[3],
radius: Radius.circular(guest.height / 2),
clockwise: _inverterMultiplier == 1 ? false : true,
);
}
path
..quadraticBezierTo(p[4].dx, p[4].dy, p[5].dx, p[5].dy)
..lineTo(host.right, host.top)
..lineTo(host.right, host.bottom)
..lineTo(host.left, host.bottom)
..close();
return path;
}
FollowerNotchedShape({inverted: false}) {
if (inverted) {
_inverterMultiplier = -1;
} else
_inverterMultiplier = 1;
}
}
class NotchedBottomBarItem {
NotchedBottomBarItem({this.iconData, this.text});
IconData iconData;
String text;
}
class NotchedBottomBar extends StatefulWidget {
NotchedBottomBar({
this.items,
this.centerItemText,
this.height: 60.0,
this.iconSize: 24.0,
this.backgroundColor,
this.color,
this.selectedColor,
this.notchedShape,
this.onTabSelected,
}) {
assert(this.items.length == 2 || this.items.length == 4);
}
final List<NotchedBottomBarItem> items;
final String centerItemText;
double height;
final double iconSize;
final Color backgroundColor;
final Color color;
final Color selectedColor;
final NotchedShape notchedShape;
final ValueChanged<int> onTabSelected;
#override
State<StatefulWidget> createState() => NotchedBottomBarState();
}
class NotchedBottomBarState extends State<NotchedBottomBar> {
int _selectedIndex = 0;
_updateIndex(int index) {
widget.onTabSelected(index);
setState(() {
_selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
List<Widget> items = List.generate(widget.items.length, (int index) {
return _buildTabItem(
item: widget.items[index],
index: index,
onPressed: _updateIndex,
);
});
items.insert(items.length >> 1, _buildMiddleTabItem());
return BottomAppBar(
shape: widget.notchedShape,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items,
),
color: widget.backgroundColor,
);
}
Widget _buildMiddleTabItem() {
return Expanded(
child: SizedBox(
height: widget.height,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(height: widget.iconSize),
Text(
widget.centerItemText ?? '',
style: TextStyle(color: widget.color),
),
],
),
),
);
}
Widget _buildTabItem({
NotchedBottomBarItem item,
int index,
ValueChanged<int> onPressed,
}) {
Color color = _selectedIndex == index ? widget.selectedColor : widget.color;
return Expanded(
child: SizedBox(
height: widget.height,
child: Material(
type: MaterialType.transparency,
color: Colors.transparent,
child: InkWell(
onTap: () {
onPressed(index);
},
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 8),
),
Icon(item.iconData, color: color, size: widget.iconSize),
Padding(
padding: EdgeInsets.only(bottom: 4),
),
Text(
item.text,
style: TextStyle(color: color),
)
],
),
),
),
),
);
}
}
Any help?
use "extendBody: true" inside "Scaffold" class
Scaffold(
extendBody: true,
body: Container()