Using Dynamically-Sizing Tab View Nested in Scrollview in Flutter - flutter

I'm trying to nest a tabview in a Scrollview, and can't find a good way to accomplish the task.
A diagram is included below:
The desired functionality is to have a normal scrollable page, where one of the slivers is a tab view with different sized (and dynamically resizing) tabs.
Unfortunately, despite looking at several resources and the flutter docs, I haven't come across any good solutions.
Here is what I have tried:
SingleChildScrollView with a column child, with the TabBarView wrapped in an IntrinsicHeight widget (Unbound constraints)
CustomScrollView variations, with the TabBarView wrapped in a SliverFillRemaining and the header and footer each wrapped with a SliverToBoxAdapter. In all cases, the content is forced to expand to the full size of the viewport (as if using a SliverFillViewport Sliver with a viewport fraction of 1.0) if smaller, or a nested scroll/overflow is created within the space if larger (see below)
If the children of the TabBarView are scrollable widgets, the sliver with the tab bar is given a height equal to the ViewPort (1.0) and any leftover space is empty.
If the children are not scrollable, they are force-expanded to fit if smaller, or give an overflow error if larger.
NestedScrollView comes closest but still suffers the ill effects of the previous implementation (see below for code example)
Various other unorthodox approaches (such as removing the TabBarView and trying to use an AnimatedSwitcher in conjunction with a listener on the TabBar to animate between the "tabs" but this wasn't swipable and the animation janked and the switched widgets overlapped)
The thus-far "best" implementation's code is given below, but it is not ideal.
Does anyone know of any way(s) to accomplish this?
Thank you in advance.
// best (more "Least-bad") solution code
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
routes: {
'root': (context) => const Scaffold(
body: ExamplePage(),
),
},
initialRoute: 'root',
);
}
}
class ExamplePage extends StatefulWidget {
const ExamplePage({
Key? key,
}) : super(key: key);
#override
State<ExamplePage> createState() => _ExamplePageState();
}
class _ExamplePageState extends State<ExamplePage>
with TickerProviderStateMixin {
late TabController tabController;
#override
void initState() {
super.initState();
tabController = TabController(length: 2, vsync: this);
tabController.addListener(() {
setState(() {});
});
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.grey[100],
appBar: AppBar(),
body: NestedScrollView(
floatHeaderSlivers: false,
physics: const AlwaysScrollableScrollPhysics(),
headerSliverBuilder: (BuildContext context, bool value) => [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
bottom: 24.0,
top: 32.0,
),
child: Column(
children: [
// TODO: Add scan tab thing
Container(
height: 94.0,
width: double.infinity,
color: Colors.blueGrey,
alignment: Alignment.center,
child: Text('A widget with information'),
),
const SizedBox(height: 24.0),
GenaricTabBar(
controller: tabController,
tabStrings: const [
'Tab 1',
'Tab 2',
],
),
],
),
),
),
],
body: CustomScrollView(
slivers: [
SliverFillRemaining(
child: TabBarView(
physics: const AlwaysScrollableScrollPhysics(),
controller: tabController,
children: [
// Packaging Parts
SingleChildScrollView(
child: Container(
height: 200,
color: Colors.black,
),
),
// Symbols
SingleChildScrollView(
child: Column(
children: [
Container(
color: Colors.red,
height: 200.0,
),
Container(
color: Colors.orange,
height: 200.0,
),
Container(
color: Colors.amber,
height: 200.0,
),
Container(
color: Colors.green,
height: 200.0,
),
Container(
color: Colors.blue,
height: 200.0,
),
Container(
color: Colors.purple,
height: 200.0,
),
],
),
),
],
),
),
SliverToBoxAdapter(
child: ElevatedButton(
child: Text('Button'),
onPressed: () => print('pressed'),
),
),
],
),
),
);
}
class GenaricTabBar extends StatelessWidget {
final TabController? controller;
final List<String> tabStrings;
const GenaricTabBar({
Key? key,
this.controller,
required this.tabStrings,
}) : super(key: key);
#override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(8.0),
),
padding: const EdgeInsets.all(4.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// if want tab-bar, uncomment
TabBar(
controller: controller,
indicator: ShapeDecoration.fromBoxDecoration(
BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: Colors.white,
),
),
tabs: tabStrings
.map((String s) => _GenaricTab(tabString: s))
.toList(),
),
],
),
);
}
class _GenaricTab extends StatelessWidget {
final String tabString;
const _GenaricTab({
Key? key,
required this.tabString,
}) : super(key: key);
#override
Widget build(BuildContext context) => Container(
child: Text(
tabString,
style: const TextStyle(
color: Colors.black,
),
),
height: 32.0,
alignment: Alignment.center,
);
}
The above works in Dartpad (dartpad.dev) and doesn't require any external libraries

Ideally, there is a better answer out there somewhere. BUT, until it arrives, this is how I got around the issue:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
// darkTheme: Themes.darkTheme,
// Language support
// Routes will keep track of all of the possible places to go.
routes: {
'root': (context) => const Scaffold(
body: ExamplePage(),
),
},
initialRoute: 'root', // See below.
);
}
}
class ExamplePage extends StatefulWidget {
const ExamplePage({
Key? key,
}) : super(key: key);
#override
State<ExamplePage> createState() => _ExamplePageState();
}
class _ExamplePageState extends State<ExamplePage>
with TickerProviderStateMixin {
late TabController tabController;
late PageController scrollController;
late int _pageIndex;
#override
void initState() {
super.initState();
_pageIndex = 0;
tabController = TabController(length: 2, vsync: this);
scrollController = PageController();
tabController.addListener(() {
if (_pageIndex != tabController.index) {
animateToPage(tabController.index);
}
});
}
void animateToPage([int? target]) {
if (target == null || target == _pageIndex) return;
scrollController.animateToPage(
target,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
setState(() {
_pageIndex = target;
});
}
void animateTabSelector([int? target]) {
if (target == null || target == tabController.index) return;
tabController.animateTo(
target,
duration: const Duration(
milliseconds: 100,
),
);
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.grey[100],
appBar: AppBar(),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
bottom: 24.0,
top: 32.0,
),
child: Column(
children: [
// TODO: Add scan tab thing
Container(
height: 94.0,
width: double.infinity,
color: Colors.blueGrey,
alignment: Alignment.center,
child: Text('A widget with information'),
),
const SizedBox(height: 24.0),
GenaricTabBar(
controller: tabController,
tabStrings: const [
'Tab 1',
'Tab 2',
],
),
],
),
),
),
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.black,
),
),
SliverToBoxAdapter(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
// if page more than 50% to other page, animate tab controller
double diff = notification.metrics.extentBefore -
notification.metrics.extentAfter;
if (diff.abs() < 50 && !tabController.indexIsChanging) {
animateTabSelector(diff >= 0 ? 1 : 0);
}
if (notification.metrics.atEdge) {
if (notification.metrics.extentBefore == 0.0) {
// Page 0 (1)
if (_pageIndex != 0) {
setState(() {
_pageIndex = 0;
});
animateTabSelector(_pageIndex);
}
} else if (notification.metrics.extentAfter == 0.0) {
// Page 1 (2)
if (_pageIndex != 1) {
setState(() {
_pageIndex = 1;
});
animateTabSelector(_pageIndex);
}
}
}
return false;
},
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
physics: const PageScrollPhysics(),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Parts
SizedBox(
width: MediaQuery.of(context).size.width,
child: Container(
color: Colors.teal,
height: 50,
),
),
// 2. Symbols
SizedBox(
width: MediaQuery.of(context).size.width,
child: Container(
color: Colors.orange,
height: 10000,
),
),
],
),
),
),
),
SliverToBoxAdapter(
child: Column(
children: [
Container(
color: Colors.red,
height: 200.0,
),
Container(
color: Colors.orange,
height: 200.0,
),
Container(
color: Colors.amber,
height: 200.0,
),
Container(
color: Colors.green,
height: 200.0,
),
Container(
color: Colors.blue,
height: 200.0,
),
Container(
color: Colors.purple,
height: 200.0,
),
],
),
),
],
),
);
}
class GenaricTabBar extends StatelessWidget {
final TabController? controller;
final List<String> tabStrings;
const GenaricTabBar({
Key? key,
this.controller,
required this.tabStrings,
}) : super(key: key);
#override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(8.0),
),
padding: const EdgeInsets.all(4.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// if want tab-bar, uncomment
TabBar(
controller: controller,
indicator: ShapeDecoration.fromBoxDecoration(
BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: Colors.white,
),
),
tabs: tabStrings
.map((String s) => _GenaricTab(tabString: s))
.toList(),
),
],
),
);
}
class _GenaricTab extends StatelessWidget {
final String tabString;
const _GenaricTab({
Key? key,
required this.tabString,
}) : super(key: key);
#override
Widget build(BuildContext context) => Container(
child: Text(
tabString,
style: const TextStyle(
color: Colors.black,
),
),
height: 32.0,
alignment: Alignment.center,
);
}
(Dartpad ready)
The basic idea is to not use a Tabview at all and instead use a horizontal scroll view nested in our scrollable area.
By using page physics for the horizontal scroll and using a PageController instead of a normal ScrollController, we can achieve a a scroll effect between the two widgets in the horizontal area that snap to whichever page is correct.
By using a notification listener, we can listen for changes in the scrollview and update the tab view accordingly.
LIMITATIONS:
The above code assumes only two tabs, so would require more thought to optimize for more tabs, particularly in the NotificationListener function.
This also may not be performant for large tabs since both tabs are being built, even if one is out of view.
Finally, the vertical height of each tab is the same; so a tab that is much larger will cause the other tab to have a lot of empty vertical space.
Hope this helps anyone in a similar boat, and am open to suggestions to improve.

Related

Can i wrap tab bar using widget chip

I want to do when I click on each chip, it will change the content of my body but I am using tab controller which I wrap with chip widget.
I also want to decorate my chip when it is selected and unselected. I try to declare my text for each widget using list array but I am stuck. Can someone help me. This is what I have been done so far
class YearTab extends StatefulWidget {
const YearTab({
Key? key,
}) : super(key: key);
#override
State<YearTab> createState() => _YearTabState();
}
class _YearTabState extends State<YearTab>
with SingleTickerProviderStateMixin {
late TabController _controller;
bool _selectedTab = false;
List<Widget> list = const [
Chip(label: Text('This year')),
Chip(label: Text('2021')),
Chip(label: Text('2020')),
Chip(label: Text('2019')),
Chip(label: Text('2018')),
];
#override
void initState() {
super.initState();
_controller = TabController(length: list.length, vsync: this);
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Container(
color: AppColor.white,
width: MediaQuery.of(context).size.width,
child: TabBar(
// unselectedLabelColor: Colors.yellow,
// labelColor: Colors.red,
physics: const BouncingScrollPhysics(),
indicator: BoxDecoration(
border: Border.all(color: Colors.red),
borderRadius: BorderRadius.circular(10),
color: const Color.fromARGB(255, 176, 208, 255)
// color: !widget.selected
// ? Color.fromARGB(255, 176, 208, 255)
// : Colors.transparent
),
controller: _controller,
onTap: (index) {},
isScrollable: true,
tabs: list,
)),
SizedBox(
height: MediaQuery.of(context).size.height * 2.2,
child: TabBarView(
controller: _controller,
children: const [
//Content for Demografi Pengguna
Content1(),
Content2(),
Content3(),
Content4(),
Content5(),
],
),
)
],
);
}
}
Instead of Tabview ,you can use this :
dependencies:
toggle_switch: ^2.0.1
Example:
ToggleSwitch(
minWidth: 90.0,
initialLabelIndex: 1,
cornerRadius: 20.0,
activeFgColor: Colors.white,
inactiveBgColor: Colors.grey,
inactiveFgColor: Colors.white,
totalSwitches: 2,
labels: ['Tab1', 'Tab2'],
icons: [FontAwesomeIcons.mars, FontAwesomeIcons.venus],
activeBgColors: [[Colors.blue],[Colors.pink]],
onToggle: (index) {
print('switched to: $index');
//change the view as per index
},
),
When you request a chip widget, it is called in initState() the first time you call the widget. You can initialize the array from there and get the data. Alternatively, you can call the widget by observing the array using the Stream structure using getX or Provider.
i changed a few things
class YearTab extends StatefulWidget {
const YearTab({
Key? key,
}) : super(key: key);
#override
State<YearTab> createState() => _YearTabState();
}
class _YearTabState extends State<YearTab> with SingleTickerProviderStateMixin {
List<Widget> tabs = const [
Chip(label: Text('This year')),
Chip(label: Text('2021')),
Chip(label: Text('2020')),
Chip(label: Text('2019')),
Chip(label: Text('2018')),
];
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: tabs.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
indicatorColor: Theme.of(context).colorScheme.secondary,
tabs: tabs,
),
),
body: TabBarView(
children: [
//Content for Demografi Pengguna
Container(
color: Colors.blueAccent,
),
Container(
color: Colors.red,
),
Container(
color: Colors.green,
),
Container(
color: Colors.yellow,
),
Container(
color: Colors.grey,
),
],
),
),
);
}
}

Flutter Tab bar can't take all space

I want to make a custom tab widget and set a list of this widgets to a tab bar. But tab bar can't take all space and some space will remain. I try to wrap it with PreferredSize but it doesn't work .
The tab bar (ScrollTabBar) :
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(0.0, 0.75),
),
],
borderRadius: BorderRadius.circular(widget.borderRadiusT),
color: Colors.red,
),
height: widget.tabHeight,
child: PreferredSize(
preferredSize: Size.fromHeight(widget.tabHeight),
child: TabBar(
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
color: widget.indicatorColor,
width: widget.indicatorWheight,
),
insets: EdgeInsets.symmetric(
horizontal: widget.horizontalPadding,
),
),
indicatorWeight: widget.indicatorWheight,
indicatorColor: widget.indicatorColor,
labelPadding: EdgeInsets.only(
bottom: 0,
right: widget.horizontalPadding,
left: widget.horizontalPadding,
),
labelColor: widget.activeTextColor,
unselectedLabelColor: widget.diactiveTextColor,
controller: widget.tabController,
tabs: widget.tabList,
isScrollable: true,
),
),
),
Expanded(
child: TabBarView(
controller: widget.tabController,
children: [
for (var builder in widget.screenList) builder.call(context)
],
),
),
],
),
);
}
tabList is list of FTabComp :
FTabComp(
String title,
Key key,
ScrollTabBar parent, {
bool haveDivider = true,
}) {
return Tab(
key: key,
child: Container(
color: Colors.blue,
width: parent.tabLength,
child: Stack(
clipBehavior: Clip.none,
children: [
Align(
alignment: Alignment.center,
child: Text(title),
),
haveDivider
? Positioned.fill(
left: parent.tabLength * - 1.5,
child: SizedBox(
height: double.maxFinite,
child: VerticalDivider(
color: parent.outerBackgroundColor,
thickness: parent.dviderWidth,
),
),
)
: Center()
,
],
),
),
);
}
Container are red . Tabs are blue , if you solve this , I will say thank you.
Image
To make the TabBar use the maximum width of the screen you should remove the isScrollable property or set it to false. This way the TabBar is going to fill the entire width of the screen and resize each tab accordingly. From the docs, each tab gets an equal share of the available space). Also, the tabLength should be removed as it doesn't make sense anymore.
Now, the position of VerticalDivider should be calculated. It should take into account the screen width, the width of the tab, and also the padding between tabs. And it should be places in a Stack with the TabBar to make it the same height as the TabBar itself.
The algorithm to calculate the tab width can be something like this:
double width = MediaQuery.of(context).size.width;
double tabLength = (width -
horizontalPadding * (tabCount + 1) -
horizontalPadding * (tabCount - 1)) /
tabCount;
Below are the screenshots of the final result. Take a look also on the live demo on DartPad (Wait a little to run):
Small
Medium
Large
Take a look at the changed code below:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late TabController tabController;
late TabController tabController2;
#override
void initState() {
// TODO: implement initState
super.initState();
tabController = TabController(length: 4, vsync: this);
tabController2 = TabController(length: 3, vsync: this);
}
#override
Widget build(BuildContext context) {
double horizontalPadding = 8;
return Scaffold(
body: ScrollTabBar(
tabController: tabController,
horizontalPadding: horizontalPadding,
tabList: const [
FTabComp(
title: 'secKey',
key: ValueKey('tab1'),
),
FTabComp(
title: 'firstKey',
key: ValueKey('tab2'),
),
FTabComp(
title: 'otherKey',
key: ValueKey('tab3'),
),
FTabComp(
title: 'anotherKey',
key: ValueKey('tab4'),
),
],
screenList: [
(context) => const Text('Tab 1'),
(context) => const Text('Tab 2'),
(context) => const Text('Tab 3'),
(context) => const Text('Tab 4'),
],
),
);
}
}
class ScrollTabBar extends StatefulWidget {
final double borderRadiusT;
final double tabHeight;
final Color indicatorColor;
final Color activeTextColor;
final Color diactiveTextColor;
final double indicatorWheight;
final double horizontalPadding;
final Color outerBackgroundColor;
final double dviderWidth;
final TabController? tabController;
final List<Widget> tabList;
final List<Widget Function(BuildContext)> screenList;
final bool haveDivider;
const ScrollTabBar({
Key? key,
this.borderRadiusT = 4,
this.tabHeight = 48,
this.indicatorColor = Colors.blue,
this.activeTextColor = Colors.black,
this.diactiveTextColor = Colors.white38,
this.indicatorWheight = 8,
this.horizontalPadding = 8,
required this.tabController,
required this.tabList,
required this.screenList,
this.outerBackgroundColor = Colors.black,
this.dviderWidth = 4,
this.haveDivider = true,
}) : super(key: key);
#override
State<ScrollTabBar> createState() => _ScrollTabBarState();
}
class _ScrollTabBarState extends State<ScrollTabBar> {
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double tabLength = (width -
widget.horizontalPadding * (widget.tabList.length + 1) -
widget.horizontalPadding * (widget.tabList.length - 1)) /
widget.tabList.length;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(0.0, 0.75),
),
],
borderRadius: BorderRadius.circular(widget.borderRadiusT),
color: Colors.red,
),
height: widget.tabHeight,
child: Stack(
children: [
TabBar(
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
color: widget.indicatorColor,
width: widget.indicatorWheight,
),
insets: EdgeInsets.symmetric(
horizontal: widget.horizontalPadding,
),
),
indicatorWeight: widget.indicatorWheight,
indicatorColor: widget.indicatorColor,
labelPadding: EdgeInsets.only(
bottom: 0,
right: widget.horizontalPadding,
left: widget.horizontalPadding,
),
labelColor: widget.activeTextColor,
unselectedLabelColor: widget.diactiveTextColor,
controller: widget.tabController,
tabs: widget.tabList,
),
for (int i = 1; i < widget.tabList.length; i++)
Positioned.fill(
left: (widget.horizontalPadding + tabLength + widget.horizontalPadding) * i - (widget.dviderWidth / 2),
right: width,
child: SizedBox(
height: widget.tabHeight,
child: VerticalDivider(
color: widget.outerBackgroundColor,
thickness: widget.dviderWidth,
),
),
),
],
),
),
Expanded(
child: TabBarView(
controller: widget.tabController,
children: [
for (var builder in widget.screenList) builder.call(context)
],
),
),
],
),
);
}
}
class FTabComp extends StatelessWidget {
final String title;
const FTabComp({
Key? key,
required this.title,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Tab(
key: key,
child: Container(
color: Colors.blue,
child: Stack(
clipBehavior: Clip.none,
children: [
Align(
alignment: Alignment.center,
child: Text(title),
),
],
),
),
);
}
}
Inside the Stack, you use Positioned.fill. try removing it

How to expand list items without affecting other items. (flutter)

I want to create the Netflix home page in flutter, and I'm stuck while creating this hover state.
I have created two base widgets. One for the number plus the thumbnail and the other one for the expanded view when the widget is hovered. Then I put them in a stack with an Inkwell where the onHover changes the state to show the expanded widget.
When I hover on the widget, it does switch between the normal state an expanded state, the problem comes when I try to put a list of these widgets together.
When using row (or ListView) to put them together, after hovering, the expanded widget makes the other widgets move. (which is not the wanted behaviour, I want them to overlap)
When I use it with stack, the widgets do overlap but now it isn't scrollable anymore.
I have added the link to the repo for anyone that wants to clone it and try running it themselves, I'm running it on flutter web.
https://github.com/Advait1306/netflix-flutter
Widget with thumbnail and number:
class TopListItem extends StatelessWidget {
final int index;
const TopListItem({Key? key, required this.index}) : super(key: key);
#override
Widget build(BuildContext context) {
const double height = 250;
return SizedBox(
height: height,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset("assets/numbers/$index.svg",
fit: BoxFit.fitHeight, height: height),
Transform.translate(
offset: const Offset(-30, 0),
child: Image.asset("assets/thumbnails/thumb1.jpg"))
],
),
);
}
}
Expanded view widget:
import 'package:flutter/material.dart';
class HoverMovieTrailer extends StatelessWidget {
const HoverMovieTrailer({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
const textTheme = TextStyle(color: Colors.white);
return SizedBox(
width: 400,
height: 400,
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: const Color(0xFF242424)),
child: Column(
children: [
Image.asset("assets/backgrounds/background1.jpg"),
const SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: const [
RoundIconButton(icon: Icons.play_arrow_outlined),
SizedBox(width: 5),
RoundIconButton(icon: Icons.add_outlined),
SizedBox(width: 5),
RoundIconButton(icon: Icons.thumb_up_alt_outlined),
SizedBox(width: 5),
],
),
Row(
children: const [
RoundIconButton(icon: Icons.keyboard_arrow_down_outlined),
],
),
],
),
),
const SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Text(
"98% Match",
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold
),
),
const SizedBox(width: 5),
Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 1)
),
child: const Text(
"18+",
style: textTheme,
),
),
const SizedBox(width: 5),
const Text(
"4 Seasons",
style: textTheme,
),
const SizedBox(width: 5),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 1)
),
child: const Text(
"HD",
style: textTheme,
),
)
],
),
),
const SizedBox(
height: 5,
),
Padding(
padding: const EdgeInsets.all(18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Text(
"Captivating",
style: textTheme,
),
const SizedBox(width: 5),
Container(
width: 5,
height: 5,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white54
),
),
const SizedBox(width: 5),
const Text(
"Exciting",
style: textTheme,
),
const SizedBox(width: 5),
Container(
width: 5,
height: 5,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white54
),
),
const SizedBox(width: 5),
const Text(
"Docuseries",
style: textTheme,
),
],
),
),
],
),
),
);
}
}
class RoundIconButton extends StatelessWidget {
final IconData icon;
const RoundIconButton({Key? key, required this.icon}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
border: Border.all(width: 2, color: Colors.white)),
margin: const EdgeInsets.all(1),
child: IconButton(
onPressed: () {},
icon: Icon(icon),
color: Colors.white,
),
);
}
}
Combining the widgets in the single widget:
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:netflix_flutter/widgets/hover_movie_trailer.dart';
import 'package:netflix_flutter/widgets/top_list_item.dart';
class TopListItemWithHover extends StatefulWidget {
const TopListItemWithHover({Key? key}) : super(key: key);
#override
State<TopListItemWithHover> createState() => _TopListItemWithHoverState();
}
class _TopListItemWithHoverState extends State<TopListItemWithHover> {
bool hover = false;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: (){},
onHover: (value){
log("Hover value: $value");
setState(() {
hover = value;
});
},
child: Stack(
clipBehavior: Clip.none,
children: [
TopListItem(index: 1),
if(hover) HoverMovieTrailer(),
],
),
);
}
}
Lists:
import 'package:flutter/material.dart';
import 'package:netflix_flutter/widgets/hover_movie_trailer.dart';
import 'package:netflix_flutter/widgets/top_list_item.dart';
import 'package:netflix_flutter/widgets/top_list_item_with_hover.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
height: 400,
child: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
clipBehavior: Clip.none,
itemCount: 8,
itemBuilder: (context, index) {
return TopListItemWithHover();
},
),
),
),
const SizedBox(height: 50),
SingleChildScrollView(
child: SizedBox(
height: 400,
child: Stack(
fit: StackFit.passthrough,
children: [
for (var i = 10; i >= 0; i--)
Positioned(
left: (i) * 300,
child: TopListItemWithHover(),
)
],
),
),
)
],
),
);
}
}
So finally found the solution to this problem, the way to move forward is to use a stack in SingleChildScrollView.
A mistake that I made is, I did not set the SingleChildScrollView's direction to horizontal. So I added that.
And then one more addition that's needed is -- A empty sized box which has the width of sum of all the items in the stack.
Stack(
clipBehavior: Clip.none,
children: [
const SizedBox(
width: 300*10,
),
for (var i = 10; i >= 0; i--)
Positioned(
left: (i) * 300,
child: TopListItemWithHover(),
)
],
)
This finally expanded the stack to the required width and then made is scrollable also.

How to add highlight overlay around the widget upon click?

I'm working on simple design system using Flutter. I want to highlight a widget upon selection (click), as you can see in the below images button get highlighted upon click. It gets handle and border.
Challenging part: I don't want layout getting changed as additional space taken by handle and border upon click. I want widget, handle and border are overlaid, so that it wouldn't shift the position of other neighbouring widgets.
And after selection
You could also use a Stack with the overlay bleeding out of the Stack thanks to a clipBehavior of Clip.none.
Full code
Just copy paste it in a DartPad to see it in action.
import 'package:flutter/material.dart';
const kcPrimary = Color(0xFF001989);
const kcSecondary = Color(0xFF239689);
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
debugShowCheckedModeBanner: false,
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Text('Blablablablabla'),
Text('Blablablablabla'),
Text('Blablablablabla'),
PlayButton(),
Text('Blablablablabla'),
Text('Blablablablabla'),
Text('Blablablablabla'),
],
),
),
),
);
}
}
class PlayButton extends StatefulWidget {
const PlayButton({Key? key}) : super(key: key);
#override
State<PlayButton> createState() => _PlayButtonState();
}
class _PlayButtonState extends State<PlayButton> {
bool clicked = false;
#override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
InkWell(
onTap: () => setState(() => clicked = !clicked),
child: _mainButton,
),
if (clicked) ...[
Positioned.fill(
child: IgnorePointer(
child: _overlayBorder,
),
),
Positioned(
top: -20.0,
left: 0,
child: _overlayTitle,
),
Positioned(top: 0, right: 0, child: _corner),
Positioned(bottom: 0, right: 0, child: _corner),
Positioned(bottom: 0, left: 0, child: _corner),
Positioned(top: 0, left: 0, child: _corner),
],
],
);
}
Widget get _mainButton => Container(
width: 80.0,
height: 40.0,
decoration: BoxDecoration(
border: Border.all(
color: kcPrimary,
width: 3.0,
),
borderRadius: const BorderRadius.all(Radius.circular(12))),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.play_arrow),
Text('Play'),
],
),
),
);
Widget get _overlayBorder => Container(
decoration: BoxDecoration(
border: Border.all(
color: kcSecondary,
width: 3.0,
),
),
);
Widget get _corner => Container(width: 10, height: 10, color: kcSecondary);
Widget get _overlayTitle => Container(
height: 20.0,
width: 48.0,
color: kcSecondary,
alignment: Alignment.center,
child: const Text(
'Button',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
);
}

How to create this type of Bottom Navigation?

I am trying to make a Bottom Navigation Bar that looks exactly like this. Since I'm just a beginner to learn flutter, I am having a lot of problems one of which is not able to find the icons so I decided to use other similarly available icons. Now I just confused with my own code.
This is what I want:
this is how my Bottom Navigation Bar looks:
This is my code:
Scaffold(bottomNavigationBar:
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
height: 50,
width: MediaQuery.of(context).size.width / 5,
decoration: BoxDecoration(color:Color(0xfffed307)),
child: Column(
children: [
Icon(Icons.store_mall_directory_outlined),
Text('My Page')
],
),
),
Container(
height: 50,
width: MediaQuery.of(context).size.width / 5,
decoration: BoxDecoration(color: Color(0xfffed307)),
child: Column(
children: [Icon(Icons.apps), Text('Election')],
),
),
Container(
height: 50,
width: MediaQuery.of(context).size.width / 5,
decoration: BoxDecoration(color: Color(0xfffed307)),
child: Image.asset('images/scan_icon.png'),
),
Container(
height: 50,
width: MediaQuery.of(context).size.width / 5,
decoration: BoxDecoration(color: Color(0xfffed307)),
child: Column(
children: [Icon(Icons.apps), Text('Add Election')],
),
),
Expanded(
child: Container(
height: 50,
width: MediaQuery.of(context).size.width / 5,
decoration: BoxDecoration(color: Color(0xfffed307)),
child: Column(
children: [Icon(Icons.groups_outlined), Text('Customer')],
),
),
),
],
)
,);
You can use floatingActionButton for the scan icon and use floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
With Flutter, to make this kind of UI is easy peasy)) Use Positioned Widget inside Stack Widget if you really wanna make this UI using bottomNavigationBar property of Scaffold Widget.
Result UI
Copy and paste the code below to see the effect:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyScreen(),
);
}
}
class MyScreen extends StatefulWidget {
const MyScreen({Key? key}) : super(key: key);
#override
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
late List<Widget> _screens;
int currentIndex = 0;
#override
void initState() {
super.initState();
_screens = [
TestScreen(title: '1st Screen'),
TestScreen(title: '2nd Screen'),
TestScreen(title: '3rd Screen'),
TestScreen(title: '4th Screen'),
TestScreen(title: '5th Screen'),
];
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: currentIndex,
children: _screens,
),
bottomNavigationBar: BottomBar(
selectedIndex: currentIndex,
children: [
BottomBarItem(icon: Icons.home),
BottomBarItem(icon: Icons.search),
BottomBarItem(icon: Icons.favorite),
BottomBarItem(icon: Icons.person),
],
onMainPressed: () {
setState(() {
currentIndex = 4;
});
},
onPressed: (index) {
setState(() {
currentIndex = index;
});
},
),
);
}
}
class BottomBarItem {
BottomBarItem({required this.icon});
IconData icon;
}
class BottomBar extends StatelessWidget {
final List<BottomBarItem> children;
final Function? onPressed;
final Function? onMainPressed;
final selectedIndex;
BottomBar({
this.children = const [],
this.onPressed,
this.onMainPressed,
this.selectedIndex,
});
#override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Container(
color: Colors.grey[100],
child: SafeArea(
bottom: true,
child: Container(
height: 60,
decoration: BoxDecoration(
color: Colors.grey[100],
boxShadow: [
BoxShadow(
color: Colors.grey,
blurRadius: 8.0,
offset: Offset(
0.0, // horizontal, move right 10
-6.0, // vertical, move down 10
),
),
],
),
child: Stack(
clipBehavior: Clip.none,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: children.map<Widget>(
(item) {
int index = children.indexOf(item);
bool isSelected = selectedIndex == index;
return Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
onPressed!(index);
},
child: Padding(
padding: EdgeInsets.zero,
child: Icon(
item.icon,
size: isSelected ? 35 : 30,
color: isSelected ? Colors.blue : Colors.grey,
),
),
),
),
);
},
).toList()
..insert(2, SizedBox(width: 80)),
),
Positioned(
top: -14,
width: size.width,
child: Center(
child: Container(
height: 60,
width: 60,
child: ClipOval(
child: Material(
color: selectedIndex == 4 ? Colors.blue : Colors.grey,
child: InkWell(
onTap: () {
onMainPressed!();
},
child: Center(
child: Icon(
Icons.adb,
size: 27,
color: Colors.white,
),
),
),
),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey,
blurRadius: 6.0,
),
],
),
),
),
),
],
),
),
),
);
}
}
class TestScreen extends StatelessWidget {
final String title;
const TestScreen({required this.title, Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Text(title),
)),
);
}
}