animated switcher children not clickable - flutter - flutter

i have a clickable content in my switcher pages. the buttons doesnt work.
i have three questions:
1- is this the right way to use animated switcher with list view builder to make it switched with fade animation?
2- how to make the content of animated switcher clickable
3- there is another way to use fade transition with list view builder ?
any suggestions will be helpful.
thanks for your help
the code below:
import 'package:flutter/material.dart';
class MicroExam extends StatefulWidget {
const MicroExam({Key? key}) : super(key: key);
#override
_MicroExamState createState() => _MicroExamState();
}
List<Widget> _pages = [
Center(
child: GestureDetector(
onTap: () {
print('red');
},
child: Container(
color: Colors.red,
width: 237.34,
height: 44.74,
)),
),
Center(
child: GestureDetector(
onTap: () {
print('blue');
},
child: Container(
color: Colors.blue,
width: 237.34,
height: 44.74,
)),
)
];
int selectedPage = 0;
class _MicroExamState extends State<MicroExam> with TickerProviderStateMixin {
PageController _controller = PageController();
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Stack(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Container(
key: ValueKey(_pages[selectedPage]),
child: _pages[selectedPage],
),
),
Center(
child: PageView.builder(
onPageChanged: (value) {
setState(() {
selectedPage = value;
});
},
controller: _controller,
physics: const BouncingScrollPhysics(),
itemCount: _pages.length,
itemBuilder: (context, index) {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
);
},
),
),
// )
],
),
),
);
}
}

You can get tap event using onTapDown with
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (_) {
print("on tapDown");
},
And place the AnimatedSwitcher on bottom on Stack children.
class MicroExam extends StatefulWidget {
const MicroExam({Key? key}) : super(key: key);
#override
_MicroExamState createState() => _MicroExamState();
}
List<Widget> _pages = [
Center(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
print('red');
},
onTapDown: (_) {
print("on tapDown");
},
child: Container(
color: Colors.red,
width: 237.34,
height: 44.74,
)),
),
Center(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (_) {
print("on tapDown");
},
onTap: () {
print('blue');
},
child: Container(
color: Colors.blue,
width: 237.34,
height: 44.74,
)),
)
];
int selectedPage = 0;
class _MicroExamState extends State<MicroExam> with TickerProviderStateMixin {
PageController _controller = PageController();
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Stack(
children: [
Center(
child: PageView.builder(
onPageChanged: (value) {
print("tapped");
setState(() {
selectedPage = value;
});
},
controller: _controller,
physics: const BouncingScrollPhysics(),
itemCount: _pages.length,
itemBuilder: (context, index) {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
);
},
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Container(
key: ValueKey(_pages[selectedPage]),
child: _pages[selectedPage],
),
),
],
),
),
);
}
}

Related

PageView.builder reload content after navigation to the new page

I have a list of videos urls/youtube IDs and a PageView.builder. I want to load next/previous video according to position in the list. For example:
List videos = ['vid_0', 'vid_1', 'vid_2', 'vid_3', 'vid_4']
if current Playing video is vid_2, it should reload the next video or previous video of the list based on button event.
Currently when I press next or previous button, the same video is loaded but the PageView makes pages equal to the length of list. I am using youtube_video_player package.
Here is my code:
import 'package:flutter/material.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
void main(){
runApp(WatchVideoLesson());
}
class WatchVideoLesson extends StatefulWidget {
const WatchVideoLesson({Key? key}) : super(key: key);
#override
State<WatchVideoLesson> createState() => _WatchVideoLessonState();
}
class _WatchVideoLessonState extends State<WatchVideoLesson> {
List ytIDs = ['mluJOYd17L8', 'd-RCKfVjFI4', 'xXPuaB7UpB0'];
var playingVideo;
final _pageController = PageController();
late YoutubePlayerController _controller;
bool _isPlayerReady = false;
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
void initState() {
_controller = YoutubePlayerController(
initialVideoId: ytIDs.first,
flags: const YoutubePlayerFlags(
autoPlay: false,
)
);
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text('Week'),
backgroundColor: Colors.red,
),
body:
Column(
children:[
Expanded(
child: PageView.builder(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
itemCount: ytIDs.length,
itemBuilder: (context, index) {
return Column(
children: [
Container(
decoration: const BoxDecoration(
color: Colors.grey,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(style: ElevatedButton.styleFrom(
backgroundColor: Colors.red),
onPressed: () {
_pageController.previousPage(duration: Duration(seconds: 1), curve: Curves.ease);
},
child: Icon(Icons.arrow_back_ios_new,)),
SizedBox(width: 10,),
Flexible(child: Text('Video Title', overflow: TextOverflow.ellipsis,)),
SizedBox(width: 10,),
ElevatedButton(style: ElevatedButton.styleFrom(
backgroundColor: Colors.green),
onPressed: () {
_pageController.nextPage(duration: Duration(seconds: 1), curve: Curves.easeInOut);
},
child: Icon(Icons.arrow_forward_ios,)),
],
),
),
SizedBox(height: 20,),
YoutubePlayer(
progressColors: const ProgressBarColors(
playedColor: Colors.red,
handleColor: Colors.green),
controller: _controller,
showVideoProgressIndicator: true,
onReady: (){
_isPlayerReady = true;
},
),
SizedBox(height: 10,),
],
);
}
),
),
]
),
),
);
}
}
Current snippet using the same controller on YoutubePlayer and my guess this is issue having same video. I separating the widget so that it can have different controller for PageView item.
class WatchVideoLesson extends StatefulWidget {
const WatchVideoLesson({Key? key}) : super(key: key);
#override
State<WatchVideoLesson> createState() => _WatchVideoLessonState();
}
class _WatchVideoLessonState extends State<WatchVideoLesson> {
List ytIDs = ['mluJOYd17L8', 'd-RCKfVjFI4', 'xXPuaB7UpB0'];
var playingVideo;
final _pageController = PageController();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(children: [
Expanded(
child: PageView.builder(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
itemCount: ytIDs.length,
itemBuilder: (context, index) {
return Column(
children: [
Container(
decoration: const BoxDecoration(
color: Colors.grey,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red),
onPressed: () {
_pageController.previousPage(
duration: Duration(seconds: 1),
curve: Curves.ease);
},
child: Icon(
Icons.arrow_back_ios_new,
)),
SizedBox(
width: 10,
),
Flexible(
child: Text(
'Video Title',
overflow: TextOverflow.ellipsis,
)),
SizedBox(
width: 10,
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green),
onPressed: () {
_pageController.nextPage(
duration: Duration(seconds: 1),
curve: Curves.easeInOut);
},
child: Icon(
Icons.arrow_forward_ios,
)),
],
),
),
YTPlayer(ytIDs: ytIDs[index]),
SizedBox(
height: 20,
),
SizedBox(
height: 10,
),
],
);
}),
),
]),
),
);
}
}
class YTPlayer extends StatefulWidget {
final String ytIDs;
const YTPlayer({super.key, required this.ytIDs});
#override
State<YTPlayer> createState() => _YTPlayerState();
}
class _YTPlayerState extends State<YTPlayer> {
late YoutubePlayerController _controller;
bool _isPlayerReady = false;
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
_controller = YoutubePlayerController(
initialVideoId: widget.ytIDs,
flags: const YoutubePlayerFlags(
autoPlay: false,
));
}
#override
Widget build(BuildContext context) {
return YoutubePlayer(
controller: _controller,
progressColors: const ProgressBarColors(
playedColor: Colors.red, handleColor: Colors.green),
showVideoProgressIndicator: true,
onReady: () {
_isPlayerReady = true;
},
);
}
}

Flutter: Custom navigation drawer does not reflect activeIndex on rebuild with AutoRoute

I am using AutoRoute with AutoTabsRouter for navigation in my web app. I have a custom Drawer used for navigation. I want my current tab icon and text to be the primary color when on the selected index.
For mobile and tablet view, the drawer pops with every selection and when I open it again the current tab has the primary color as it should. On desktop view this works if I open the app in desktop view and do not resize the screen at all. If I do resize the screen at all, even if I'm still in desktop view after resizing, the selected does not work as expected and the current tab does not have the primary color.
Here is my code for the Drawer :
class CustomDrawer extends StatefulWidget {
final double width;
const CustomDrawer({
required this.width,
Key? key,
}) : super(key: key);
#override
State<CustomDrawer> createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State<CustomDrawer> {
ScrollController scrollController = ScrollController();
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
#override
Widget build(context) {
final tabsRouter = context.tabsRouter;
return Drawer(
elevation: 0,
backgroundColor: lightThemeCardColor,
child: SingleChildScrollView(
controller: scrollController,
child: Column(
children: [
DrawerHeader(
child: Column(
children: const [
SizedBox(
height: defaultMargin,
),
Icon(
Icons.architecture,
color: lightPrimaryColor,
size: 50,
),
Text(
'Drawer Header',
style: TextStyle(
fontSize: mobileTitleTextSize,
fontWeight: FontWeight.bold,
),
),
],
),
),
ListTile(
leading: const Icon(Icons.home),
title: const Text('H O M E'),
selected: tabsRouter.activeIndex == 0,
selectedColor: lightPrimaryColor,
onTap: () {
// Push to screen
tabsRouter.setActiveIndex(0);
if (widget.width < 1100) {
Navigator.pop(context);
}
},
),
ListTile(
leading: const Icon(Icons.edit),
title: const Text('S E C O N D'),
selected: tabsRouter.activeIndex == 1,
selectedColor: lightPrimaryColor,
onTap: () {
// Push to screen
tabsRouter.setActiveIndex(1);
if (widget.width < 1100) {
Navigator.pop(context);
}
},
),
ListTile(
leading: const Icon(Icons.check),
title: const Text('T H I R D'),
selected: tabsRouter.activeIndex == 2,
selectedColor: lightPrimaryColor,
onTap: () {
// Push to screen
tabsRouter.setActiveIndex(2);
if (widget.width < 1100) {
Navigator.pop(context);
}
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('F O U R T H'),
selected: tabsRouter.activeIndex == 3,
selectedColor: lightPrimaryColor,
onTap: () {
//Push to screen
tabsRouter.setActiveIndex(3);
if (widget.width < 1100) {
Navigator.pop(context);
}
},
),
],
),
),
);
}
}
Here is my code for the desktop home page where CustomDrawer is the first item in body row:
class DesktopHomePage extends StatelessWidget {
const DesktopHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: lightThemeScaffoldColor,
body: Row(
children: [
// Always open drawer
CustomDrawer(width: width),
// Body contents
Expanded(
child: Container(
margin: const EdgeInsets.all(largeMargin),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const MobileTabletTextHeadline(
text: 'Some widget',
),
SizedBox(
height: 300,
child: AspectRatio(
aspectRatio: 4,
child: SizedBox(
width: double.infinity,
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 4,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.all(defaultPadding),
child: Container(color: lightThemeCardColor),
);
}),
),
),
),
),
// Some other widget
const MobileTabletTextHeadline(
text: 'Other Widget',
),
SizedBox(
height: 300,
child: AspectRatio(
aspectRatio: 4,
child: SizedBox(
width: double.infinity,
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 4,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.all(defaultPadding),
child: Container(color: lightThemeCardColor),
);
}),
),
),
),
),
],
),
),
),
),
],
),
);
}
}
(All desktop views have the same layout)
And here is my code where I set up AutoTabsRouter:
class NavPage extends StatelessWidget {
const NavPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return AutoTabsRouter(
routes: const [
HomeRouter(),
SecondRouter(),
ThirdRouter(),
FourthRouter(),
],
builder: (context, child, animation) {
return Scaffold(
appBar:
MediaQuery.of(context).size.width < 1100 ? mobileAppBar : null,
backgroundColor: lightThemeScaffoldColor,
body: child,
drawer: CustomDrawer(width: width),
);
},
);
}
}
I've tried using using setState after setting the current index but that does not help. What can I change the Drawer in a way that reflects the changes in desktop view after window resize?

Transform FAB to modal bottom sheet (or similar)

I'm trying to make a FAB expand to become the bottom sheet similar to the "Within screen" example in material.io
So that when I tap on
this FAB
animates to become
this modal bottom sheet
Here is the code for this example:
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Example'),
),
body: Center(child: Text('Hello, World!')),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () async {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.transparent,
builder: (context) {
return AddEntryBottomSheet();
},
);
},
),
);
}
}
class AddEntryBottomSheet extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(32.0),
child: Text('Hello, world!'),
),
Padding(
padding: const EdgeInsets.all(32.0),
child: Text('More content'),
),
],
),
);
}
}
I managed to get the result I wanted with the example below. Just use ExpandingFab as a FAB in a Scaffold
import 'package:flutter/material.dart';
class ExpandingFab extends StatefulWidget {
#override
_ExpandingFabState createState() => _ExpandingFabState();
}
class _ExpandingFabState extends State<ExpandingFab> {
bool _cardIsOpen = false;
double get cardWidth => MediaQuery.of(context).size.width - 32;
double cardHeight = 200;
Widget _renderFab() {
return InkWell(
onTap: () {
setState(() {
_cardIsOpen = true;
});
},
child: Icon(Icons.add, color: Colors.white),
);
}
Widget _renderUpsertEntryCard() {
return Container(
width: cardWidth,
height: cardHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Hello, World!'),
ElevatedButton(
onPressed: () {
setState(() {
_cardIsOpen = false;
});
},
child: Text('Close'),
),
],
),
);
}
#override
Widget build(BuildContext context) {
double w = _cardIsOpen ? cardWidth : 56;
double h = _cardIsOpen ? cardHeight : 56;
return AnimatedContainer(
curve: Curves.ease,
constraints: BoxConstraints(
minWidth: w, maxWidth: w,
minHeight: h, maxHeight: h,
),
duration: Duration(milliseconds: 300),
decoration: BoxDecoration(
color: _cardIsOpen ? Colors.blueGrey[100] : Colors.blue,
boxShadow: kElevationToShadow[1],
borderRadius: _cardIsOpen
? BorderRadius.all(Radius.circular(0.0))
: BorderRadius.all(Radius.circular(50)),
),
child: AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (child, animation) => ScaleTransition(
scale: animation,
child: child,
),
child: !_cardIsOpen ? _renderFab() : _renderUpsertEntryCard(),
),
);
}
}

how to add flexible height to showGeneralDialog on Flutter?

I added padding for transparent outside. But fixed height. How to change it?
padding: EdgeInsets.fromLTRB(20, 50, 20, 50),
Is it possible to remove above this line and flexible(center)?
I am expected like this flexible height alert. click here
onPressed: () {
showGeneralDialog(
context: context,
barrierColor: Palette.black.withOpacity(.3),
barrierDismissible: true,
transitionDuration: Duration(milliseconds: 400),
pageBuilder: (_, __, ___) {
return ChangePropertyPage(
propertyModel: propertyModel);
},
);
},
change Property Page
class ChangePropertyPage extends StatelessWidget {
final List<PropertyModel> propertyModel;
const ChangePropertyPage({Key key, this.propertyModel}) : super(key: key);
#override
Widget build(BuildContext context) {
final double width = CustomMediaQuery.width(context);
return Padding(
padding: EdgeInsets.fromLTRB(20, 50, 20, 50),
child: Material(
borderRadius: BorderRadius.all(Radius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
PropertyListTileWidget(
mainTitle: 'USER\'S Name', subTitle: 'USER\'S Email'),
VerticalSpacing(height: 10),
CustomLine(
height: 1,
width: (width - 40) - 20,
color: Palette.black.withOpacity(.2),
),
Expanded(
child: ListView.builder(
itemCount: propertyModel.length,//now length is 1
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: ()async{
},
child: PropertyListTileWidget(
mainTitle: '${propertyModel[index].propertyName}',
subTitle: '${propertyModel[index].ownerUId}'),
);
}),
)
],
),
),
),
);
}
}
if you are expecting this:
then
full code:
import 'package:flutter/material.dart';
class CustomDialogBox extends StatefulWidget {
#override
_CustomDialogBoxState createState() => _CustomDialogBoxState();
}
class _CustomDialogBoxState extends State<CustomDialogBox> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Custom Dialog Box"),
centerTitle: true,
),
body:Center(
child:FlatButton(
color: Colors.blue,
onPressed: (){
showDialog(
context: (context),
child: ShowCustomDialogBox()
);
},
child: Text("Show Dialog")
)
) ,
);
}
}
class ShowCustomDialogBox extends StatefulWidget {
#override
State<StatefulWidget> createState() => ShowCustomDialogBoxState();
}
class ShowCustomDialogBoxState extends State<ShowCustomDialogBox>with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> scaleAnimation;
#override
void initState() {
super.initState();
controller = AnimationController(vsync: this, duration: Duration(milliseconds: 450));
scaleAnimation =CurvedAnimation(parent: controller, curve: Curves.decelerate);
controller.addListener(() {
setState(() {});
});
controller.forward();
}
#override
Widget build(BuildContext context) {
return Center(
child: Material(
color: Colors.transparent,
child: ScaleTransition(
scale: scaleAnimation,
child: Container(
margin: EdgeInsets.all(20.0),
padding: EdgeInsets.all(8.0),
height: MediaQuery.of(context).size.height/2.5, //Change height of dialog box.
width: MediaQuery.of(context).size.width,
decoration: ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0))),
child: Column(
children: <Widget>[
Expanded(
flex: 4,
child: ListView.builder(
itemCount: 10,
itemBuilder: (context, index){
return Column(
// mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text((index+1).toString(),style: TextStyle(color:Colors.blue,fontSize:40),),
Divider()
],
);
}
)
),
Padding(
padding: const EdgeInsets.only(
left: 20.0, right: 10.0, top: 0.0,),
child: ButtonTheme(
height: 35.0,
minWidth: MediaQuery.of(context).size.width/3.5,
child: RaisedButton(
color: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0)),
splashColor: Colors.white.withAlpha(40),
child: Text(
'Next',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13.0),
),
onPressed: () {
setState(() {
Navigator.pop(context);
});
},
)
)
),
],
)
),
),
),
);
}
}

Standard Bottom Sheet in Flutter

I'm having very hard time to implement "Standard Bottom Sheet" in my application - with that I mean bottom sheet where "header" is visible and dragable (ref: https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet). Even more: I can not find any example of it anywhere:S. the closes I came to wished result is by implementing DraggableScrollableSheet as bottomSheet: in Scaffold (only that widget has initialChildSize) but seams like there is no way to make a header "sticky" bc all the content is scrollable:/.
I also found this: https://flutterdoc.com/bottom-sheets-in-flutter-ec05c90453e7 - seams like there the part about "Persistent Bottom Sheet" is the one I'm looking for but artical is not detailed so I can not figure it out exacly the way to implement it plus the comments are preaty negative there so I guess it's not totally correct...
Does Anyone has any solution?:S
The standard bottom sheet behavior that you can see in the material spec can be achived using DraggableScrollableSheet.
Here I am going to explain it in detail.
Step 1:
Define your Scaffold.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Draggable sheet demo',
home: Scaffold(
///just for status bar color.
appBar: PreferredSize(
preferredSize: Size.fromHeight(0),
child: AppBar(
primary: true,
elevation: 0,
)),
body: Stack(
children: <Widget>[
Positioned(
left: 0.0,
top: 0.0,
right: 0.0,
child: PreferredSize(
preferredSize: Size.fromHeight(56.0),
child: AppBar(
title: Text("Standard bottom sheet demo"),
elevation: 2.0,
)),
),
DraggableSearchableListView(),
],
)),
);
}
}
Step 2:
Define DraggableSearchableListView
class DraggableSearchableListView extends StatefulWidget {
const DraggableSearchableListView({
Key key,
}) : super(key: key);
#override
_DraggableSearchableListViewState createState() =>
_DraggableSearchableListViewState();
}
class _DraggableSearchableListViewState
extends State<DraggableSearchableListView> {
final TextEditingController searchTextController = TextEditingController();
final ValueNotifier<bool> searchTextCloseButtonVisibility =
ValueNotifier<bool>(false);
final ValueNotifier<bool> searchFieldVisibility = ValueNotifier<bool>(false);
#override
void dispose() {
searchTextController.dispose();
searchTextCloseButtonVisibility.dispose();
searchFieldVisibility.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return NotificationListener<DraggableScrollableNotification>(
onNotification: (notification) {
if (notification.extent == 1.0) {
searchFieldVisibility.value = true;
} else {
searchFieldVisibility.value = false;
}
return true;
},
child: DraggableScrollableActuator(
child: Stack(
children: <Widget>[
DraggableScrollableSheet(
initialChildSize: 0.30,
minChildSize: 0.15,
maxChildSize: 1.0,
builder:
(BuildContext context, ScrollController scrollController) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
boxShadow: [
BoxShadow(
color: Colors.grey,
offset: Offset(1.0, -2.0),
blurRadius: 4.0,
spreadRadius: 2.0)
],
),
child: ListView.builder(
controller: scrollController,
///we have 25 rows plus one header row.
itemCount: 25 + 1,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return Container(
child: Column(
children: <Widget>[
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(
top: 16.0,
left: 24.0,
right: 24.0,
),
child: Text(
"Favorites",
style:
Theme.of(context).textTheme.headline6,
),
),
),
SizedBox(
height: 8.0,
),
Divider(color: Colors.grey),
],
),
);
}
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: ListTile(title: Text('Item $index')));
},
),
);
},
),
Positioned(
left: 0.0,
top: 0.0,
right: 0.0,
child: ValueListenableBuilder<bool>(
valueListenable: searchFieldVisibility,
builder: (context, value, child) {
return value
? PreferredSize(
preferredSize: Size.fromHeight(56.0),
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1.0,
color: Theme.of(context).dividerColor),
),
color: Theme.of(context).colorScheme.surface,
),
child: SearchBar(
closeButtonVisibility:
searchTextCloseButtonVisibility,
textEditingController: searchTextController,
onClose: () {
searchFieldVisibility.value = false;
DraggableScrollableActuator.reset(context);
},
onSearchSubmit: (String value) {
///submit search query to your business logic component
},
),
),
)
: Container();
}),
),
],
),
),
);
}
}
Step 3:
Define the custom sticky SearchBar
class SearchBar extends StatelessWidget {
final TextEditingController textEditingController;
final ValueNotifier<bool> closeButtonVisibility;
final ValueChanged<String> onSearchSubmit;
final VoidCallback onClose;
const SearchBar({
Key key,
#required this.textEditingController,
#required this.closeButtonVisibility,
#required this.onSearchSubmit,
#required this.onClose,
}) : super(key: key);
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Container(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 0),
child: Row(
children: <Widget>[
SizedBox(
height: 56.0,
width: 56.0,
child: Material(
type: MaterialType.transparency,
child: InkWell(
child: Icon(
Icons.arrow_back,
color: theme.textTheme.caption.color,
),
onTap: () {
FocusScope.of(context).unfocus();
textEditingController.clear();
closeButtonVisibility.value = false;
onClose();
},
),
),
),
SizedBox(
width: 16.0,
),
Expanded(
child: TextFormField(
onChanged: (value) {
if (value != null && value.length > 0) {
closeButtonVisibility.value = true;
} else {
closeButtonVisibility.value = false;
}
},
onFieldSubmitted: (value) {
FocusScope.of(context).unfocus();
onSearchSubmit(value);
},
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
textCapitalization: TextCapitalization.none,
textAlignVertical: TextAlignVertical.center,
textAlign: TextAlign.left,
maxLines: 1,
controller: textEditingController,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
hintText: "Search here",
),
),
),
ValueListenableBuilder<bool>(
valueListenable: closeButtonVisibility,
builder: (context, value, child) {
return value
? SizedBox(
width: 56.0,
height: 56.0,
child: Material(
type: MaterialType.transparency,
child: InkWell(
child: Icon(
Icons.close,
color: theme.textTheme.caption.color,
),
onTap: () {
closeButtonVisibility.value = false;
textEditingController.clear();
},
),
),
)
: Container();
})
],
),
),
);
}
}
See the screenshots of the final output.
state 1:
The bottom sheet is shown with it's initial size.
state 2:
User dragged up the bottom sheet.
state 3:
The bottom sheet reached the top edge of the screen and a sticky custom SearchBar interface is shown.
That's all.
See the live demo here.
As #Sergio named some good alternatives it still needs more coding to make it work as it should with that said, I found Sliding_up_panel so for anyone else looking for solution You can find it here .
Still, I find it really weird that built in bottomSheet widget in Flutter does not provide options for creating "standard bottom sheet" mentioned in material.io :S
If you are looking for Persistent Bottomsheet than please refer the source code from below link
Persistent Bottomsheet
You can refer the _showBottomSheet() for your requirement and some changes will fulfil your requirement
You can do it using a stack and an animation:
class HelloWorldPage extends StatefulWidget {
#override
_HelloWorldPageState createState() => _HelloWorldPageState();
}
class _HelloWorldPageState extends State<HelloWorldPage>
with SingleTickerProviderStateMixin {
final double minSize = 80;
final double maxSize = 350;
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
_animation =
Tween<double>(begin: minSize, end: maxSize).animate(_controller);
super.initState();
}
AnimationController _controller;
Animation<double> _animation;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: <Widget>[
Positioned(
bottom: 0,
height: _animation.value,
child: GestureDetector(
onDoubleTap: () => _onEvent(),
onVerticalDragEnd: (event) => _onEvent(),
child: Container(
color: Colors.red,
width: MediaQuery.of(context).size.width,
height: minSize,
),
),
),
],
),
);
}
_onEvent() {
if (_controller.isCompleted) {
_controller.reverse(from: maxSize);
} else {
_controller.forward();
}
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Can easily be achieved with showModalBottomSheet. Code:
void _presentBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => Wrap(
children: <Widget>[
SizedBox(height: 8),
_buildBottomSheetRow(context, Icons.share, 'Share'),
_buildBottomSheetRow(context, Icons.link, 'Get link'),
_buildBottomSheetRow(context, Icons.edit, 'Edit Name'),
_buildBottomSheetRow(context, Icons.delete, 'Delete collection'),
],
),
);
}
Widget _buildBottomSheetRow(
BuildContext context,
IconData icon,
String text,
) =>
InkWell(
onTap: () {},
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16),
child: Icon(
icon,
color: Colors.grey[700],
),
),
SizedBox(width: 8),
Text(text),
],
),
);