How to add highlight overlay around the widget upon click? - flutter

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,
),
),
);
}

Related

Is there any alternative for Stack widget that allow gesture in overflow?

I want to implement this. I could easily achieve it in CSS using position: absolute, a bit of if-else statement in JavaScript, done.
I'm trying to achieve the same thing in Flutter (see code snippet below), I use the Stack widget, it does give the same visual result. But, I cannot click anything inside the options box.
I've done searching for a solution and found that according to the answer of this question, this behaviour is intentional, and I should refactor my code not to use ClipBehavior (Overflow is deprecated). With that being said, I could do something like just using Column instead of Stack but I need the Stack's behaviour where the Options should not push another widgets when it is being shown, similar to position: absolute in CSS.
I am wondering if there is any other widget that do the same thing as Stack but allow me to interact with the Positioned elements/widgets outside of its bound. If there is any, please let me know!
main.dart
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(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Scaffold(
backgroundColor: Colors.blueGrey,
body: Center(
child: CustomDropdown(),
),
);
}
}
class CustomDropdown extends StatefulWidget {
const CustomDropdown({Key? key}) : super(key: key);
#override
State<CustomDropdown> createState() => _CustomDropdownState();
}
class _CustomDropdownState extends State<CustomDropdown> {
bool showOptions = false;
#override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
_buildPrimaryButton(),
if (showOptions) _buildOptions(),
],
);
}
Widget _buildPrimaryButton() {
return Ink(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade200),
borderRadius: BorderRadius.circular(10),
),
child: InkWell(
onTap: () {
setState(() {
showOptions = !showOptions;
});
},
child: const Padding(
padding: EdgeInsets.all(10),
child: Text('Primary Button'),
),
),
);
}
Widget _buildOptions() {
return Positioned(
right: 0,
bottom: -145,
child: Ink(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade200),
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () => print('TAPPED Option 1'),
child: const Padding(
padding: EdgeInsets.fromLTRB(20, 10, 150, 10),
child: Text('Option 1'),
),
),
InkWell(
onTap: () => print('TAPPED Option 2'),
child: const Padding(
padding: EdgeInsets.fromLTRB(20, 10, 150, 10),
child: Text('Option 2'),
),
),
InkWell(
onTap: () => print('TAPPED Option 3'),
child: const Padding(
padding: EdgeInsets.fromLTRB(20, 10, 150, 10),
child: Text('Option 3'),
),
),
],
),
),
);
}
}

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.

Using Dynamically-Sizing Tab View Nested in Scrollview in 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.

Flutter - How do I scroll down a Card

My app currently looks like this:
And the here's the code behind it:
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.lightBlue),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(children: [
Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24))),
elevation: 30,
child: Padding(
padding: EdgeInsets.only(top: 120, right: 15, bottom: 20, left: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TextField(
controller: TextEditingController(text: '12121+1212'),
keyboardType: TextInputType.none,
textAlign: TextAlign.end,
style: TextStyle().copyWith(fontSize: 70),
decoration: InputDecoration(border: InputBorder.none),
),
Align(
alignment: Alignment.centerRight,
child: Text(
'Result',
style: TextStyle().copyWith(fontSize: 36),
),
),
Container(
height: 6,
width: 25,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.all(Radius.circular(14))),
)
],
)),
),
Expanded(child: Container())
]))));
}
}
Now, I want to scroll down the this card to reveal a list above it,something like this:
https://youtu.be/qZtGjd_-KwI
I think it could be done with CustomScrollView, but as a novices CustomScrollView and Slivers are a bit complex for me to understand. A little help regrading how to go about this would be appreciated.
I think you want a CustomScrollView. This will get you close (but won't collapse the main body as in your video example). Note if you provide a hasScrollBody to true then you're body will shrink in response but it'll then overflow. Even a ConstrainedBox with minimum height didn't work for me so that shrinking part is really the hardest part.
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key}) : super(key: key);
final centerKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
center: centerKey,
slivers: [
const SliverAppBar(title: Text('The real title')),
SliverList(
delegate: SliverChildListDelegate.fixed(
[
Container(
color: Colors.pink,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
width: double.infinity,
height: 200.0,
color: Colors.pink,
),
Container(
width: double.infinity,
height: 200.0,
color: Colors.blue,
),
Container(
width: double.infinity,
height: 200.0,
color: Colors.red,
),
],
),
),
],
),
),
SliverAppBar(
key: centerKey,
expandedHeight: 100.0,
backgroundColor: Colors.transparent,
flexibleSpace: const Card(
margin: EdgeInsets.zero,
elevation: 14.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(32.0),
),
),
child: SizedBox.expand(),
),
),
SliverFillRemaining(
hasScrollBody: false,
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisSize: MainAxisSize.max,
children: List.generate(5, (i) => i).map((i) {
return Expanded(
child: Row(
children: List.generate(4, (i) => i).map((i) {
return const Expanded(
child: Placeholder(),
);
}).toList(),
),
);
}).toList(),
),
),
),
),
],
),
);
}
}
The main part you should concern yourself with is the centerKey. That tells the CustomScrollView where it should start from. You pass that to both the CustomScrollView and the SliverAppBar.
The SliverFillRemaining will fill what's left of the viewport, which if you're starting at the SliverAppBar will be about 80%. FillRemaining will take you into the box model (as opposed to the Sliver model) which allows you to just use the space as you regularly would with columns and what not.

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),
)),
);
}
}