Permanent Persistent Bottom sheet flutter - flutter

I want my bottom sheet to stay on the screen till I close it from a code. Normally the bottom sheet can be closed by pressing back button(device or appbar) or even just by a downward gesture. How can I disable that?
_scaffoldKey.currentState
.showBottomSheet<Null>((BuildContext context) {
final ThemeData themeData = Theme.of(context);
return new ControlBottom(
songName: songName,
url: url,
play: play,
pause: pause,
state: test,
themeData: themeData,
);
}).closed.whenComplete((){
});
Control botton is a different widget.

Scaffold now has a bottom sheet argument and this bottom sheet cannot be dismissed by swiping down the screen.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(....),
bottomSheet: Container(
child: Text('Hello World'),
),
);
}

Also you can use WillPopScope Widget to control pressing back button:
Widget _buildBottomSheet() {
return WillPopScope(
onWillPop: () {
**here you can handle back button pressing. Just leave it empty to disable back button**
},
child: **your bottom sheet**
)),
);
}

You can remove the appbar back button by providing an empty container in leading property.
AppBar(
leading: Container(),
);
But we don't have any control over device back button & bottomsheet will disappear on back pressed.
One of the many alternative approach could be using a stack with positioned & opacity widget
Example :
Stack(
children: <Widget>[
// your code here
Positioned(
left: 0.0,
right: 0.0,
bottom: 0.0,
child: Opacity(
opacity: _opacityLevel,
child: Card(
child: //Your Code Here,
),
),
),
// your code here
],
);
You can change _opacityLevel from 0.0 to 1.0 when a song is selected.
From what I can make out from your code is that you will be having a listView on top & music controls on the bottom. Make sure to add some Padding at the end of listView so that your last list item does not stay hidden behind your music controller card when you have scrolled all the way down.
If you want to further customize the look & feel of your music controller. You could use animationController or sizeAnimation to slide it from the bottom like a bottomSheet.
I hope this helps.

Add this parameters to showmodalbottomsheet,
isDismissible: false, enableDrag: false,

I wanted a bottomsheet that is draggable up and down, but does not close. So, first of all I created a function for my modalBottomSheet.
Future modalBottomSheetShow(BuildContext context) {
return showModalBottomSheet(
backgroundColor: Colors.transparent,
context: context,
builder: (context) => buildSheet(),
isDismissible: false,
elevation: 0,
);
}
Next, I used .whenComplete() method of showModalBottomSheet() to recursively call the modalBottomSheetShow() function.
Future modalBottomSheetShow(BuildContext context) {
return showModalBottomSheet(
backgroundColor: Colors.transparent,
context: context,
builder: (context) => buildSheet(),
isDismissible: false,
elevation: 0,
).whenComplete(() => modalBottomSheetShow(context));
}
Next, I simply call the modalBottomSheetShow() whenever I wanted a bottomsheet. It cannot be closed, until the recursion ends. Here is the entire code for reference:
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
static const idScreen = "HomePage";
#override
State<HomePage> createState() => _HomePageState();
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
modalBottomSheetShow(context);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 0,
elevation: 0,
backgroundColor: Colors.black,
),
);
}
Widget buildSheet() {
return DraggableScrollableSheet(
initialChildSize: 0.6,
builder: (BuildContext context, ScrollController scrollController) {
return Container(
decoration: BoxDecoration(color: Colors.white, boxShadow: [
BoxShadow(
color: Color(0x6C000000),
spreadRadius: 5,
blurRadius: 20,
offset: Offset(0, 0),
)
]),
padding: EdgeInsets.all(16),
);
},
);
}
Future modalBottomSheetShow(BuildContext context) {
return showModalBottomSheet(
backgroundColor: Colors.transparent,
context: context,
builder: (context) => buildSheet(),
isDismissible: false,
elevation: 0,
).whenComplete(() => modalBottomSheetShow(context));
}
}

Related

How to update a single item of a SliverList in Flutter?

Do you guys know how can I update a single item of a sliver list without having to invoke a setState() ?
In my case I have a SliverList and i want to click into an item and change it's color, the problem using setState() is that it rebuilds the whole UI in a not smooth way and also mess up with Custom Scroll position.
The funny thing is that this SliverList behaviour does not occur when using normal ListView, when use setState() on a ListView the load is smooth and it doesn't break the scroll state. Looks like the ListView can implicitely handle state better than SliverList.
But since I have a Custom Scroll I can't use ListVew it has to be SliverList
Any options ? Providers ?Notifiers ? Stream ? Bloc ?
Ok, after all, I could solve my need using a simple ChangeNotifier combined with an AnimatedBuilder inside each item of the SliverList, I'll post some high-level code of the solution - it works just fine for my need!
class ChangeColorSliverListItemNotifier extends ChangeNotifier {
int index;
Color current_label_color;
ChangeColorSliverListItemNotifier()
{
this.current_label_color = Colors.white;
}
void onTap(int selected_index)
{
index = selected_index;
this.current_label_color = Colors.yellow;
notifyListeners();
}
}
// code block inside SliverList items binding- changing only the color of the selected Widget
SliverList(
delegate: SliverChildBuilderDelegate((context, index)
{
return GestureDetector(
onTap:() {
changeColorSliverListItemNotifier.onTap(index);
},
child:AnimatedBuilder(
animation: changeColorSliverListItemNotifier,
builder: (_, __) =>
Row(children:[
Container(
margin: EdgeInsets.only(right:8),
child: Icon(Icons.edit,color:changeColorSliverListItemNotifier.index==index?changeColorSliverListItemNotifier.current_label_color:default_color,size: 20,)
),
])
)
)
....
Just in case you want to use the Riverpod state management library, I made the following.
class SliverScreen extends StatelessWidget {
const SliverScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
_appBar(),
_list(),
],
),
);
}
SliverFixedExtentList _list() {
return SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListItem(index: index),
),
itemExtent: 100,
);
}
SliverAppBar _appBar() {
return SliverAppBar(
title: Text("Slivering..."),
backgroundColor: Colors.teal[900],
expandedHeight: 200,
pinned: true,
stretch: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.teal[100]!,
Colors.teal[600]!,
],
),
),
),
),
);
}
}
final colorStateProvider = StateProvider.family<Color, int>((ref, key) {
return Colors.blue[100]!;
});
class ListItem extends HookConsumerWidget {
final int index;
const ListItem({Key? key, required this.index}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final colorState = ref.watch(colorStateProvider(index));
return InkWell(
onTap: () => colorState.state = Colors.blue,
child: Container(
color: colorState.state,
child: Center(
child: Text("Item $index"),
),
),
);
}
}
If you click an item, it updates the color without rebuilding the whole list, only the item itself.

How to create offset slide-out dialog overlay

I'm trying to create an animated effect where a dialog box appears, and upon receipt of information, expands down to show it as opposed to up and down. (I'll explain below)
This is what I want. Notice the top stays fixed, and the bottom slides out. I'm not sure how to approach it due to the fixed top part of the dialog box. (Is there a way to offset a dialog?
But the closest I can get is this:
So, this is my code for the popup:
void showSites(BuildContext context) {
AlertDialog alert = AlertDialog(
contentPadding: EdgeInsets.all(0.0),
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(25.0))),
elevation: 0.0,
content: SitePopUp());
showDialog<void>(
barrierDismissible: true,
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
and the general structure of the pop-up build method inside SitePopUp() (without Padding, etc):
Widget build(BuildContext context) {
Site selectedSite;
return Container(
width: 700,
height: (selectedSite == null)?200:700, // <-- TODO: Animate this on callback
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text("Agency Directory",
LookAhead(
lookAheadCallback: (List<String> val) {
selectedSite = siteList.getSiteFromAgencyNumber(
agencyNumber: selectedAgencyNum);
}),])); }

Adding DraggableScrollableSheet to the bottom of a Sliver page

I’m working on the concept that you can see on the screenshot below:
design concept
Note: the arrows are not the part of the UI, but were added to demonstrate the draggable functionality.
The screen has a SliverAppBar that displays location title, Sliver body that contains location description, and has a DraggableScrollableSheet (or a similar alternative).
When the location description is scrolled up, the title collapses.
When the DraggableScrollableSheet is scrolled up it expands to the full height of the screen.
I tried many times to put it together, but something is always off.
My last attempt was to add DraggableScrollableSheet as a ‘bottom sheet:’ in Scaffold. Since I have a BottomAppBar, it breaks the UI, and looks the following way:
current UI behavior
Scaffold
#override
Widget build(BuildContext context) {
return Scaffold(
body: body,
extendBody: true,
appBar: appBar,
bottomSheet: hasBottomSheet
? DraggableScrollableSheet(
builder:
(BuildContext context, ScrollController scrollController) {
return Container(
color: Colors.blue[100],
child: ListView.builder(
controller: scrollController,
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
);
},
)
: null,
backgroundColor: Colors.white,
floatingActionButtonLocation: fab_position,
floatingActionButton: hasActionButton ? ScannerFAB() : null,
bottomNavigationBar: AppBarsNav(hasNavButtons: hasNavButtons));
}
Scaffold body
class LocationPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ScaffoldWithNav(
hasBottomSheet: true,
body: CustomScrollView(
slivers: <Widget>[
SliverBar(
title: "Location",
hasBackground: true,
backgroundImagePath: 'assets/testImage.jpg'),
SliverToBoxAdapter(
child: Text("very long text "),
),
SliverPadding(
padding: EdgeInsets.only(bottom: 70),
),
],
),
);
}
}
BottomAppBar FAB
class ScannerFAB extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
child: WebsafeSvg.asset('assets/qr-code.svg',
color: Colors.white, height: 24, width: 24),
);
}
}
The FAB jumps, the content is hidden.
When I set a fixed-sized container, the content comes back, but the FAB is still living its own life:)
current UI behavior2
If anyone has any idea how to solve this issue/those issues please share, I’ll be very grateful!
You can try to add another Scaffold on current body and put the DraggableScrollableSheet inside it. Then the DraggableScrollableSheet won't affect the FAB outside.
#override
Widget build(BuildContext context) {
return Scaffold(
body: Scaffold(
body: body,
bottomSheet: ... // move DraggableScrollableSheet to here
),
...
floatingActionButton: ... // keep FAB here
...
)
You can use Stack into Body, for example:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children. [
SingleChildScrollView(),
DraggableScrollableSheet(),
]
),
...
floatingActionButton: ... // keep FAB here
...
)

modalBottomSheet is overlapped up by the keyboard

It's written here That
/// The scaffold will expand to fill the available space. That usually
/// means that it will occupy its entire window or device screen. When
/// the device's keyboard appears the Scaffold's ancestor [MediaQuery]
/// widget's [MediaQueryData.viewInsets] changes and the Scaffold will
/// be rebuilt. By default the scaffold's [body] is resized to make
/// room for the keyboard.
According to this if there is a TextField at the bottom, the Scaffold will resize itself and it does happen. But when I put a TextField inside a modalBottomSheet it doesn't get pushed up by the keyboard. The Keyboard overlaps the modalBottomSheet (with the TextField). If the Scaffold itself gets resized how modalBottomSheet stays at its place? And resizeToAvoidBottomInsethas no effect on modalBottomSheet.
Here is the sample code.
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showModalBottomSheet(
context: context, builder: (context) => ShowSheet());
},
),
);
}
}
class ShowSheet extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
height: 200,
child: TextField(
autofocus: true,
),
);
}
}
I apologize if this question is dumb but I didn't understand this.
I still don't know the reason may be because modalBottomSheet is using PopupRoute so it's a different route not sure. Anyway, here I found the solution I just needed to put some bottom viewInsets padding.
Widget build(BuildContext context) {
return Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
color: Colors.blue,
height: 200,
child: TextField(
autofocus: true,
),
),
);
}
Also I needed to set isScrollControlled: true, of showModalBottomSheet()

Refresh widget or page in Flutter without ListView et al

I want refresh my page without having a scrollable content, i.e. without having a ListView et al.
When I want use RefreshIndicator, the documentation says it needs a scrollable widget like ListView.
But if I want to refresh and want to use the refresh animation of RefreshIndicator without using a ListView, GridView or any other scorllable widget, how can i do that?
You can simply wrap your content in a SingleChildScrollView, which will allow you to use a RefreshIndicator. In order to make the pull down to refresh interaction work, you will have to use AlwaysScrollableScrollPhysics as your content will most likely not cover more space than available without a scroll view:
RefreshIndicator(
onRefresh: () async {
// Handle refresh.
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: /* your content */,
),
);
You can just use GestureDetector, I have created a sample for you, but it's not perfect, you can customize it to your own needs, it just detects when you swipe from the top.
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
var refresh=false;
void refreshData(){
if(!refresh){
refresh=true;
print("Refreshing");
Future.delayed(Duration(seconds: 4),(){
refresh =false;
print("Refreshed");
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Test"),
centerTitle: true,
),
body: GestureDetector(
child: Container(
color: Colors.yellow,
height: double.infinity,
width: double.infinity,
child: Center(child: Text('TURN LIGHTS ON')),
),
onVerticalDragUpdate: (DragUpdateDetails details){
print("direction ${details.globalPosition.direction}");
print("distance ${details.globalPosition.distance}");
print("dy ${details.globalPosition.dy}");
if(details.globalPosition.direction < 1 && (details.globalPosition.dy >200 && details.globalPosition.dy < 250)){
refreshData();
}
},
));
}
}