I'm using a DropDownButton with a custom style inside a ListView. My problem is: the PopupMenu opens about 200-300px below the Button so it looks like the Button below that has opened:
I wrapped the Dropdown in a custom style, but i already tried removing that and that did nothing. I've also tried to use just a normal Dropdownbutton, but that had the same effect.
the corresponding build:
#override
Widget build(BuildContext context) {
homeModel = Provider.of<HomeModel>(context);
model = Provider.of<TransferModel>(context);
navigator = Navigator.of(context);
var items = model.items.entries.toList();
return Container(
color: Colors.white,
child: ListView.builder(
physics: BouncingScrollPhysics(),
itemCount: model.items.entries.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.only(left: 30, right: 30, top: 10),
child: CustomDropDown(
errorText: "",
hint: items[index].value["label"],
items: items[index]
.value["items"]
.asMap()
.map((int i, str) => MapEntry(
i,
DropdownMenuItem(
value: i,
child: Text(str is Map
? str["displayName"].toString()
: str.toString()),
)))
.values
.toList()
.cast<DropdownMenuItem<int>>(),
value: items[index].value["selected"],
onChanged: (position) =>
model.selectItem(items[index].key, position),
),
);
},
),
);
}
CustomDropDown:
class CustomDropDown extends StatelessWidget {
final int value;
final String hint;
final String errorText;
final List<DropdownMenuItem> items;
final Function onChanged;
const CustomDropDown(
{Key key,
this.value,
this.hint,
this.items,
this.onChanged,
this.errorText})
: super(key: key);
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
decoration: BoxDecoration(
color: Colors.grey[100], borderRadius: BorderRadius.circular(30)),
child: Padding(
padding:
const EdgeInsets.only(left: 30, right: 30, top: 10, bottom: 5),
child: DropdownButton<int>(
value: value,
hint: Text(
hint,
style: TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
),
style: Theme.of(context).textTheme.title,
items: items,
onChanged: (item) {
onChanged(item);
},
isExpanded: true,
underline: Container(),
icon: Icon(Icons.keyboard_arrow_down),
),
),
),
if (errorText != null)
Padding(
padding: EdgeInsets.only(left: 30, top: 10),
child: Text(errorText, style: TextStyle(fontSize: 12, color: Colors.red[800]),),
)
],
);
}
}
edit: I've just noticed, that the popup always opens in the screen center. But I still have no idea why that is.
edit 2: thanks to #João Soares I've now narrowed down the issue: I surround the Widget with the ListView with an AnimatedContainer for opening and closing the menu. The padding of this container seems to be the culprit, but i have no idea how i can fix this, since i need that Container: (the Child is the ListView Widget)
class ContentSheet extends StatelessWidget {
final Widget child;
final bool isMenuVisible;
const ContentSheet({Key key, this.child, this.isMenuVisible}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: 50),
child: AnimatedContainer(
duration: Duration(milliseconds: 450),
curve: Curves.elasticOut,
padding: EdgeInsets.only(top: isMenuVisible ? 400 : 100),
child: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20), topRight: Radius.circular(20)),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20), topRight: Radius.circular(20)),
color: Colors.white,
),
child: child
),
),
),
);
}
}
I've tried your CustomDropDown widget with the code below and it works as expected, without the dropdown showing lower in the view. Something else in your code may be affecting its position.
class DropdownIssue extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _DropdownIssueState();
}
}
class _DropdownIssueState extends State<DropdownIssue> {
int currentValue = 0;
#override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.grey,
child: Container(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CustomDropDown(
hint: 'hint',
errorText: '',
value: currentValue,
items: [
DropdownMenuItem(
value: 0,
child: Text('test 0'),
),
DropdownMenuItem(
value: 1,
child: Text('test 1'),
),
DropdownMenuItem(
value: 2,
child: Text('test 2'),
),
].cast<DropdownMenuItem<int>>(),
onChanged: (value) {
setState(() {
currentValue = value;
});
print('changed to $value');
}
),
],
),
)
),
);
}
}
Related
I tried to acheive from NestedScrollView by creating a sample code. But its not exactly which I want. Please help me.
I want to achieve this ui. When scrolling appbar should dock at the top. Image top should start from status bar to the fixed height 300.
After Image there would be some dynamic content like description.
Then I have to add TabBar, which will dock just below the Appbar when scroll.
Body will scroll on top of Header Image, like a overlay.
Image Attachments
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class StickyTabbarExample extends StatefulWidget {
#override
_StickyTabbarExampleState createState() => _StickyTabbarExampleState();
}
class _StickyTabbarExampleState extends State<StickyTabbarExample>
with SingleTickerProviderStateMixin {
// TabController? _tabController;
final List<String> _tabs = <String>[
"Featured",
"Popular",
];
#override
void initState() {
super.initState();
// _tabController = TabController(vsync: this, length: _tabs.length);
}
#override
void dispose() {
// _tabController?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scaffold(
body: DefaultTabController(
length: _tabs.length,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) => <Widget>[
SliverAppBar(
pinned: true,
// floating: true,
toolbarHeight: 70,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
Icons.arrow_back_ios_new_sharp,
color: Colors.white,
size: 15,
),
Icon(
Icons.shop,
color: Colors.white,
size: 15,
)
]),
backgroundColor: Colors.green,
expandedHeight: 300,
flexibleSpace: FlexibleSpaceBar(
background: Image.network(
"https://images.unsplash.com/photo-1673942393203-fe61f45b4479?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80%20870w",
fit: BoxFit.cover,
width: double.maxFinite,
),
),
),
SliverToBoxAdapter(
child: Positioned.fill(
child: Transform.translate(
offset: Offset(0, -10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 30,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.only(
topRight: Radius.circular(100),
topLeft: Radius.circular(100),
),
),
),
Text("Challenge description"),
],
),
),
),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
width: 4,
color: Color(0xFF646464),
),
insets: EdgeInsets.only(left: 0, right: 8, bottom: 4)),
isScrollable: true,
labelPadding: EdgeInsets.only(left: 0, right: 0),
tabs: [
Tab(
child: Text(
"Tab 1",
style: TextStyle(color: Colors.black),
),
),
Tab(
child: Text(
"Tab 2",
style: TextStyle(color: Colors.black),
)),
],
),
),
pinned: true,
),
],
body: TabBarView(children: [TabA(), TabB()]),
),
),
);
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar);
final TabBar _tabBar;
#override
double get minExtent => _tabBar.preferredSize.height;
#override
double get maxExtent => _tabBar.preferredSize.height;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) =>
Container(
color: Colors.white,
child: _tabBar,
);
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) => false;
}
class TabA extends StatelessWidget {
const TabA({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => CustomScrollView(
// primary: false,
key: PageStorageKey<String>("Tab1"),
slivers: [
SliverPadding(
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: index % 2 == 0 ? Colors.green : Colors.greenAccent,
height: 80,
alignment: Alignment.center,
child: Text(
"Item $index",
style: const TextStyle(fontSize: 30),
),
),
),
// 40 list items
childCount: 40,
),
),
)
],
);
}
class TabB extends StatelessWidget {
const TabB({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => CustomScrollView(
key: PageStorageKey<String>("Tab2"),
slivers: [
SliverPadding(
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: index % 2 == 0 ? Colors.green : Colors.greenAccent,
height: 80,
alignment: Alignment.center,
child: Text(
"Item $index",
style: const TextStyle(fontSize: 30),
),
),
),
childCount: 40,
),
),
)
],
);
}
I am using flutter. I want to show different widgets when I tap on different options. On selecting option A, the option A widget is shown. On selecting option B, the option B widget is shown below the options bar and vice versa (like a tab bar). The code is attached below. I am glad if someone helps. ..
import 'package:flutter/material.dart';
class Cards extends StatefulWidget {
const Cards({Key? key}) : super(key: key);
#override
State<Cards> createState() => _CardsState();
}
class _CardsState extends State<Cards> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Cards"),
),
body: Padding(
padding: const EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
optionCards("A", "assets/icons/recycle.png", context, "1"),
optionCards("B", "assets/icons/tools.png", context, "2"),
optionCards("C", "assets/icons/file.png", context, "3"),
],
),
),
);
}
Widget optionCards(
String text, String assetImage, BuildContext context, String cardId) {
return Container(
width: 100,
height: 100,
decoration: ShapeDecoration(
color: Colors.grey,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
),
child: SingleChildScrollView(
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(top: 13),
child: IconButton(
onPressed: null,
icon: Icon(Icons.file_copy),
),
),
Text(
text,
style: const TextStyle(
fontSize: 14,
fontFamily: 'CeraPro',
color: Color.fromRGBO(0, 0, 0, 1),
),
),
],
),
),
);
}
Widget optiona() {
return Container();
}
Widget optionb() {
return Container();
}
Widget optionc() {
return Container();
}
}
class Cards extends StatefulWidget {
const Cards({Key? key}) : super(key: key);
#override
State<Cards> createState() => _CardsState();
}
class _CardsState extends State<Cards> {
Widget? selectedOption;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Cards"),
),
body: Padding(
padding: const EdgeInsets.only(top: 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: (){
setState(() {
selectedOption = optiona();
});
},
child: optionCards("A", "assets/icons/recycle.png", context, "1")
),
InkWell(
onTap: (){
setState(() {
selectedOption = optionb();
});
},
child: optionCards("B", "assets/icons/tools.png", context, "2")
),
InkWell(
onTap: (){
setState(() {
selectedOption = optionc();
});
},
child: optionCards("C", "assets/icons/file.png", context, "3")
),
],
),
// options
if(selectedOption != null) selectedOption!
],
),
),
);
}
Widget optionCards(
String text, String assetImage, BuildContext context, String cardId) {
return Container(
width: 100,
height: 100,
decoration: const ShapeDecoration(
color: Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
),
child: SingleChildScrollView(
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(top: 13),
child: IconButton(
onPressed: null,
icon: Icon(Icons.file_copy),
),
),
Text(
text,
style: const TextStyle(
fontSize: 14,
fontFamily: 'CeraPro',
color: Color.fromRGBO(0, 0, 0, 1),
),
),
],
),
),
);
}
Widget optiona() {
return Container();
}
Widget optionb() {
return Container();
}
Widget optionc() {
return Container();
}
}
You can use the Visibility widget to wrap the widgets which you want to hide or show and keep track of which one to show through a variable. Then you can set the visible property accordingly.
import 'package:flutter/material.dart';
class Cards extends StatefulWidget {
const Cards({Key? key}) : super(key: key);
#override
State<Cards> createState() => _CardsState();
}
class _CardsState extends State<Cards> {
var showOption = "";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Cards"),
),
body: Padding(
padding: const EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
optionCards("A", "assets/icons/recycle.png", context, "1"),
optionCards("B", "assets/icons/tools.png", context, "2"),
optionCards("C", "assets/icons/file.png", context, "3"),
],
),
),
);
}
Widget optionCards(
String text, String assetImage, BuildContext context, String cardId) {
return Container(
width: 100,
height: 100,
decoration: ShapeDecoration(
color: Colors.grey,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
),
child: SingleChildScrollView(
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(top: 13),
child: IconButton(
onPressed: null,
icon: Icon(Icons.file_copy),
),
),
Text(
text,
style: const TextStyle(
fontSize: 14,
fontFamily: 'CeraPro',
color: Color.fromRGBO(0, 0, 0, 1),
),
),
],
),
),
);
}
Widget optiona() {
return Visibility(visible: showOption == "A", child: Container());
}
Widget optionb() {
return Visibility(visible: showOption == "B", child: Container());
}
Widget optionc() {
return Visibility(visible: showOption == "C", child: Container());
}
Now you can change the showOption variable whenever you want to show another option.
I am working on a Flutter project, which has multiple dropdown buttons on different pages. I have created a custom dropdown button and it's working fine. But when I try to make it a separate widget (to use in other pages) in a file having some errors. below is the code I tried.
import 'package:flutter/material.dart';
class TestPage extends StatefulWidget {
#override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
List<Activities> _activities = [
Activities(id: 1, icon: Icons.place, title: 'Travel'),
Activities(id: 2, icon: Icons.food_bank, title: 'Eat'),
];
List<DropdownMenuItem<Activities>> activityListDropDownItems = [];
Activities selectedActivity;
int selectedActivityId = 0;
String selectedActivityTitle = '';
IconData selectedActivityIcon = Icons.addchart;
List<DropdownMenuItem<Activities>> buildActivityList(List activities) {
List<DropdownMenuItem<Activities>> items = [];
for (Activities activity in activities) {
items.add(
DropdownMenuItem(
value: activity,
child: Row(
children: [
Text(
'${activity.title}',
style: TextStyle(
color: Colors.red,
),
),
],
),
),
);
}
return items;
}
onChangeActivityListDropDownItem(Activities selected) {
setState(() {
selectedActivity = selected;
selectedActivityId = selected.id;
selectedActivityTitle = selected.title;
selectedActivityIcon = selected.icon;
});
}
#override
void initState() {
activityListDropDownItems = buildActivityList(_activities);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Custom DropDown'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$selectedActivityTitle'),
SizedBox(height: 10.0),
Container(
padding: EdgeInsets.symmetric(horizontal: 10.0),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.grey,
width: 0.3,
),
borderRadius: BorderRadius.all(
Radius.circular(
30.0,
),
),
),
child: Row(
children: [
SizedBox(width: 10.0),
Icon(
selectedActivityIcon,
color: Colors.red.withOpacity(0.7),
),
SizedBox(width: 10.0),
Expanded(
child: DropdownButton(
hint: Text(
'Activity',
style: TextStyle(),
),
isExpanded: true,
value: selectedActivity,
items: activityListDropDownItems,
onChanged: onChangeActivityListDropDownItem,
underline: Container(),
),
)
],
),
),
SizedBox(height: 20.0),
//MyDropDown(
// value: selectedActivity,
// items: activityListDropDownItems,
// onChanged: onChangeActivityListDropDownItem,
//),
],
),
),
);
}
}
class MyDropDown extends StatelessWidget {
final List<DropdownMenuItem> items;
final IconData icon;
final dynamic value;
final String hintText;
final ValueChanged onChanged;
const MyDropDown(
{Key key,
#required this.items,
this.icon,
this.value,
this.hintText,
this.onChanged})
: super(key: key);
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10.0),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.grey,
width: 0.3,
),
borderRadius: BorderRadius.all(
Radius.circular(
30.0,
),
),
),
child: Row(
children: [
SizedBox(width: 10.0),
Icon(
icon,
color: Colors.red.withOpacity(0.7),
),
SizedBox(width: 10.0),
Expanded(
child: DropdownButton(
hint: Text(
hintText,
style: TextStyle(),
),
isExpanded: true,
value: value,
items: items,
onChanged: onChanged,
underline: Container(),
),
)
],
),
);
}
}
class Activities {
final int id;
final IconData icon;
final String title;
Activities({#required this.id, #required this.icon, #required this.title});
}
when I use the MyDropDown shown an error. How to pass value and onChanged to DropdownButton?
MyDropDown(
value: selectedActivity,
items: activityListDropDownItems,
onChanged: onChangeActivityListDropDownItem,
),
Your value expects a datetype dynamic while your passing Activities
class MyDropDown<T> extends StatelessWidget {
final T value;
const MyDropDown({Key key, this.value}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container();
}
}
Declare Object type
MyDropDown<Activies>(
value: selectedActivity,
items: activityListDropDownItems,
onChanged: onChangeActivityListDropDownItem,
),
The AnimatedContainer class is not very clear for me. From the code below (that works) - all the children are created in all list views. I want to only populate certain list views - not all. How do I do this?
You can see the code works - no errors, but no matter how much I change this code, I cannot seem to get a list view or card to have uniquely its own children (or expandedContainers).
Its really causing me a lot of distress.
And when I press down the down button, it expands everything, not just the cell I need expanded.
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: Colors.grey,
appBar: new AppBar(
title: new Text("Expandable List"),
backgroundColor: Colors.redAccent,
),
body: new ListView.builder(
itemBuilder: (BuildContext context, int index) {
return new ExpandableListView(title: "Title $index");
},
itemCount: 5,
),
);
}
}
class ExpandableListView extends StatefulWidget {
final String title;
const ExpandableListView({Key key, this.title}) : super(key: key);
#override
_ExpandableListViewState createState() => new _ExpandableListViewState();
}
class _ExpandableListViewState extends State<ExpandableListView> {
bool expandFlag = false;
#override
Widget build(BuildContext context) {
return new Container(
margin: new EdgeInsets.symmetric(vertical: 1.0),
child: new Column(
children: <Widget>[
new Container(
color: Colors.blue,
padding: new EdgeInsets.symmetric(horizontal: 5.0),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new IconButton(
icon: new Container(
height: 50.0,
width: 50.0,
decoration: new BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
child: new Center(
child: new Icon(
expandFlag ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
color: Colors.white,
size: 30.0,
),
),
),
onPressed: () {
setState(() {
expandFlag = !expandFlag;
});
}),
new Text(
widget.title,
style: new TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
)
],
),
),
new ExpandableContainer(
expanded: expandFlag,
child: new ListView.builder(
itemBuilder: (BuildContext context, int index) {
return new Container(
decoration:
new BoxDecoration(border: new Border.all(width: 1.0, color: Colors.grey), color: Colors.black),
child: new ListTile(
title: new Text(
"Cool $index",
style: new TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
),
leading: new Icon(
Icons.local_pizza,
color: Colors.white,
),
),
);
},
itemCount: 15,
))
],
),
);
}
}
class ExpandableContainer extends StatelessWidget {
final bool expanded;
final double collapsedHeight;
final double expandedHeight;
final Widget child;
ExpandableContainer({
#required this.child,
this.collapsedHeight = 0.0,
this.expandedHeight = 300.0,
this.expanded = true,
});
#override
Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
return new AnimatedContainer(
duration: new Duration(milliseconds: 500),
curve: Curves.easeInOut,
width: screenWidth,
height: expanded ? expandedHeight : collapsedHeight,
child: new Container(
child: child,
decoration: new BoxDecoration(border: new Border.all(width: 1.0, color: Colors.blue)),
),
);
}
}
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),
],
),
);