Keyboard not showing properly when text field is placed in sliver - dart

I'm trying to create searchbar using Cupertino widgets and slivers.
Currently I have following structure:
CupertinoApp
CupertinoTabScaffold
CupertinoPageScaffold
CustomScrollView
SliverNavigationBar
SliverPersistentHeader
_SliverSearchBarDelegate
CupertinoTextField
SliverPersistentHeader has delegate, which is implemented in the following way:
class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate {
_SliverSearchBarDelegate({
#required this.child,
this.minHeight = 56.0,
this.maxHeight = 56.0,
});
final Widget child;
final double minHeight;
final double maxHeight;
#override
double get minExtent => minHeight;
#override
double get maxExtent => maxHeight;
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(child: child);
}
#override
bool shouldRebuild(_SliverSearchBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
And the screen widget looks like this:
class CategoriesScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar( /* ... */ ),
SliverPersistentHeader(
delegate: _SliverSearchBarDelegate(
child: Container(
/* ... */
child: CupertinoTextField( /* ... */ ),
),
),
)
],
),
);
}
}
The problem is that when I focus in text field, it looks like keyboard is trying to show but then immediately hides. I was thinking that this behavior appears because of scrollview events, but adding ScrollController to CustomScrollView doesn't gave me any results (there was no scroll events while focusing text field).
I was also thinking that the problem appears only in simulator but on real device the behavior is same.
Here's the video demonstration of problem:
UPDATE: Thanks to Raja Jain I figured out that the problem is not in slivers or CategoriesScreen widget itself but in CupertinoTabScaffold in which this widget is wrapped. If I remove CupertinoTabScaffold and set CupertinoApp's home widget to CategoriesScreen widget directly, the problem goes away. Here's my main.dart here, hope it will help, butI don't know how because there's nothing special in it:
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CupertinoApp(
/* ... */
// home: CategoriesScreen(),
home: CupertinoTabScaffold(
tabBar: CupertinoTabBar(
/* ... */
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.all, size: 20.0),
title: Text('Items'),
),
BottomNavigationBarItem(
icon: Icon(Icons.categories, size: 20.0),
title: Text('Categories'),
)
],
),
tabBuilder: (BuildContext tabBuilderContext, int index) {
return CategoriesScreen();
},
),
);
}
}

I copied your code and tried to run.Code is running fine with expected behaviour,Maybe you are rebuilding your widget somewhere while clicking on text field. I'm attaching the code which i tried and working fine.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
#override
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test'),
),
body: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text("Demo"),
),
SliverPersistentHeader(
delegate: _SliverSearchBarDelegate(
child: Container(
height: 20.0,
width: 20.0,
child: CupertinoTextField(/* ... */),
),
),
)
],
),
),
);
}
}
class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate {
_SliverSearchBarDelegate({
#required this.child,
this.minHeight = 56.0,
this.maxHeight = 56.0,
});
final Widget child;
final double minHeight;
final double maxHeight;
#override
double get minExtent => minHeight;
#override
double get maxExtent => maxHeight;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(child: child);
}
#override
bool shouldRebuild(_SliverSearchBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}

I just had a similar problem and what solved my issue was to use CupertinoTabView in the CupertinoTabScaffold's tabBuilder, like this:
tabBuilder: (BuildContext tabBuilderContext, int index) {
return CupertinoTabView(
builder: (context) => CategoriesScreen(),
);
},

Related

Getting an image-related exception when using a ListWheelScrollView in Flutter

I'm getting a peculiar bug when using a ListWheelScrollView to display image widgets on iOS. It is contained in one page of a PageView, and it works fine until I minimize the app. If the app is resumed after entering the background and then I switch to the page that contains the scrollview (either by switching away and switching back after resuming or by switching away before minimizing and then switching back after resuming), the visible images fail to display and the output reads as follows:
════════ Exception caught by image resource service
════════════════════════════ The method 'toDouble' was called on null.
Receiver: null Tried calling: toDouble()
════════════════════════════════════════════════════════════════════════════════
Below is a simple example that demonstrates the problem:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ListWheel Issue',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'ListWheelScrollview Bug'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _currentIndex = 0;
PageController _pageController;
List<Widget> get _tabs => [
ScrollScreen(),
Container(),
Container(),
];
#override
void initState() {
_pageController = PageController();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: PageView(
controller: _pageController,
children: _tabs,
)),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.refresh), title: Text('tab1')),
BottomNavigationBarItem(icon: Icon(Icons.movie), title: Text('tab2')),
BottomNavigationBarItem(
icon: Icon(Icons.search), title: Text('tab3')),
],
currentIndex: _currentIndex,
onTap: (i) {
setState(() {
_currentIndex = i;
_pageController.animateToPage(i,
curve: Curves.easeOut, duration: Duration(milliseconds: 200));
});
},
),
);
}
}
class ScrollScreen extends StatefulWidget {
#override
_ScrollScreenState createState() => _ScrollScreenState();
}
class _ScrollScreenState extends State<ScrollScreen> {
var _exImage = AssetImage('assets/images/no_image.png');
#override
Widget build(BuildContext context) {
List<AssetImage> _images = [
_exImage,
_exImage,
_exImage,
];
return Center(
child: ListWheelScrollView.useDelegate(
itemExtent: 300,
childDelegate: ListWheelChildLoopingListDelegate(
children: _images
.map((e) => Center(
child: Image(
image: e,
)))
.toList())),
);
}
}
Any help with this would be greatly appreciated.
I found out a solution.
#override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(AssetImage('assets/icons/settings_icon.png'), context);
}
after precaching the images used in the ListWheelScrollView, the error no longer appears
Idk if flutter team will fix it in the future or not.
I figured out a bit more about the issue. It turns out null values were being passed as "minScrollExtent" and "maxScrollExtent" into the _getItemFromOffset function in the list_wheel_scroll_view.dart file, which were, in turn being passed into the _clipOffsetToScrollableRange function.
As a workaround, I ended up changing the return value of the _clipOffsetScrollableRange to the following:
return (_clipOffsetToScrollableRange(
offset,
minScrollExtent ?? -double.infinity,
maxScrollExtent ?? double.infinity) /
itemExtent)
.round();
This likely does not address the underlying issue, but has resulted in my app working properly.

How to change title of main.dart AppBar in it's child programmatically?

I have an AppBar in main.dart and I want to defined it as primary on it's child, But I want to change the title of AppBar itself when I'm on child's page, how can i do that properly?
void main() => runApp(MyApp());
class MyApp extends StatelessWidget{
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter App",
theme: ThemeData(
primaryColor: Colors.cyan,
brightness: Brightness.dark
),
home: Scaffold(
appBar: AppBar(
title: Text("Main Dart"),
),
body: HomeScreen(),
),
routes: <String, WidgetBuilder>{
'/homeScreen': (buildContext)=>HomeScreen(),
'/second': (buildContext)=>Second()
},
);
}
}
//HomeScreen or Second Widget on different dart file
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
//here I want to change the title of Main Dart to HomeScreen
return Container(
child: Center(
child: FlatButton(
child: new Text("Home screen"),
onPressed: (){
Route route = MaterialPageRoute(builder: (context) => Second());
Navigator.push(context, route);
},
),
),
);
}
}
or I need to put Scaffold(appBar:AppBar(...), ...) in every screen? it is the best approach?
Have a BLoC for app properties in app_properties_bloc.dart
final appBloc = AppPropertiesBloc();
class AppPropertiesBloc{
StreamController<String> _title = StreamController<String>();
Stream<String> get titleStream => _title.stream;
updateTitle(String newTitle){
_title.sink.add(newTitle);
}
dispose() {
_title.close();
}
}
Use stream builder in AppBar like this:
AppBar(
title: StreamBuilder<Object>(
stream: appBloc.titleStream,
initialData: "Main Dart",
builder: (context, snapshot) {
return Text(snapshot.data);
}
),
),
Use this to update title on button's onPressed()
onPressed: () {
appBloc.updateTitle('new title');
},
Just in case you are changing only the title of Scaffold then this will work.
I am creating a DefaultScaffold with the title each screen provides. Here the code will show the MainPage and two other pages which have the same AppBar with changed titles.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(initialRoute: 'home', routes: <String, WidgetBuilder>{
'home': (context) => SOMain(),
'/secondPage': (context) => DefaultScaffold("Second Screen", SOSecond()),
'/thirdPage': (context) => DefaultScaffold("Third Screen", SOThird()),
});
}
}
class DefaultScaffold extends StatelessWidget {
String title;
Widget body;
DefaultScaffold(this.title, this.body);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: body,
);
}
}
class SOMain extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DefaultScaffold(
"Main Screen",
Center(
child: RaisedButton(
child: Text("Go to second screen"),
onPressed: () {
Navigator.pushNamed(context, '/secondPage');
}),
),
);
}
}
class SOSecond extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text("Go the 3rd screen"),
onPressed: () => Navigator.pushNamed(context, "/thirdPage"),
),
);
}
}
class SOThird extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(child: Text("You are on last screen"));
}
}
Note: This is a simple workaround and may not be the best way to do this.
You can accomplish updating the state of the parent from a child by using a callback function.
Parent Class:
import 'package:flutter/material.dart';
class Parent extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return ParentState();
}
}
class ParentState extends State<Parent> {
String title = "Old Title";
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: DaysFragmentView(onTitleSelect: (String value) {
setTitle(value);
}
),
);
}
void setTitle(String value) {
setState(() {
title = value;
});
}
}
Child Class
typedef TitleCallback = void Function(Title color);
class DaysFragmentView extends StatelessWidget {
const DaysFragmentView({this.onTitleSelect});
final TitleCallback onTitleSelect;
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
RaisedButton(
child: Text('One'),
onPressed: () {
onTitleSelect("TITLE ONE");
},
),
RaisedButton(
child: Text('Two'),
onPressed: () {
onTitleSelect("TITLE TWO");
},
)
],
);
}
}
Reference:
call-method-in-one-stateful-widget-from-another-stateful-widget-flutter
working-with-callback-in-flutter
Using ValueListenableBuilder is an option.
Use an instance variable
String appTitle;
Then set the app bar as in the following block:
appBar: AppBar(
ValueListenableBuilder<String>(
valueListenable: appTitle,
builder: (context, value, child) {
return Text(appTitle.value);
},
),
After that you can simply set appTitle.value in the other class. The title will be changed too because it listens to that value.
appTitle.value = "Home Screen";
Some answer here are too complicated. Here is a full working example using app bar update from child with scafold widget.
You can run the example in dart pad
import 'package:flutter/material.dart';
void main() {
runApp(const MyHomePage(title: 'init title'));
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final ValueNotifier<String?> _appBarTitleNotifier = ValueNotifier<String?>(null);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: ValueListenableBuilder<String?>(
builder: (BuildContext context, String? value, Widget? child) {
return Text(value ?? widget.title);
},
valueListenable: _appBarTitleNotifier,
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ChildDemoTitleBar(titleNotifier: _appBarTitleNotifier)
],
),
),
),
);
}
}
class ChildDemoTitleBar extends StatefulWidget {
final ValueNotifier<String?> titleNotifier;
const ChildDemoTitleBar({Key? key, required this.titleNotifier})
: super(key: key);
#override
State<ChildDemoTitleBar> createState() => _ChildDemoTitleBarState();
}
class _ChildDemoTitleBarState extends State<ChildDemoTitleBar> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
child: InkWell(
onTap: () {
_counter++;
widget.titleNotifier.value = "title updated $_counter";
},
child: const Text("tap to update title")));
}
}

How to make 'stacked card list view' in flutter?

I want to build ui similar to this link in flutter.
https://github.com/loopeer/CardStackView/blob/master/screenshot/screenshot1.gif
Key ideal features are followings.
Behave like list view, but cards should be stacked at the top of screen.
List can have infinite items. So old cards should be recycled to save memory.
I also want to set different size to each card.
First, I found some 'tinder' like ui like following and tried them.
https://blog.geekyants.com/tinder-swipe-in-flutter-7e4fc56021bc
However, users need to swipe each single card, that required user to swipe many times to browse list items.
And then I could somehow make a list view whose items are overlapped with next ones.
import 'package:flutter/material.dart';
class StackedList extends StatelessWidget {
List<ItemCard> cards = [];
StackedList() {
for (int i = 0; i < 20; i++) {
cards.add(ItemCard(i));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('title')),
body: Container(
child: ListView.builder(
itemBuilder: (context, index) {
return Align(
alignment: Alignment.topCenter,
heightFactor: 0.8,
child: cards[index],
);
},
itemCount: cards.length,
),
),
);
}
}
class ItemCard extends StatelessWidget {
int index;
ItemCard(this.index);
#override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(color: Colors.black, blurRadius: 20.0),
],
),
child: SizedBox.fromSize(
size: const Size(300, 400),
child: Card(
elevation: 5.0,
color: index % 2 == 0 ? Colors.blue : Colors.red,
child: Center(
child: Text(index.toString()),
),
),
),
);
}
}
However items don't stop at the top of screen, which is not exactly what I want.
I guess I can achieve this effect by customizing ScrollController or ScrollPhysics but I'm not sure where I should change.
You can achieve a similar behaviour with SliverPersistentHeader and a CustomScrollView, and you can wrap your cards in GestureDetector to modify their height by changing the value of SliverPersistentHeaderDelegate's maxExtent parameter. Here is a small app I wrote that achieves something that might look like what you are looking for:
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Stacked list example',
home: Scaffold(
appBar: AppBar(
title: Text("Stacked list example"),
backgroundColor: Colors.black,
),
body: StackedList()),
);
}
}
class StackedList extends StatelessWidget {
final List<Color> _colors = Colors.primaries;
static const _minHeight = 16.0;
static const _maxHeight = 120.0;
#override
Widget build(BuildContext context) => CustomScrollView(
slivers: _colors
.map(
(color) => StackedListChild(
minHeight: _minHeight,
maxHeight: _colors.indexOf(color) == _colors.length - 1
? MediaQuery.of(context).size.height
: _maxHeight,
pinned: true,
child: Container(
color: _colors.indexOf(color) == 0
? Colors.black
: _colors[_colors.indexOf(color) - 1],
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular(_minHeight)),
color: color,
),
),
),
),
)
.toList(),
);
}
class StackedListChild extends StatelessWidget {
final double minHeight;
final double maxHeight;
final bool pinned;
final bool floating;
final Widget child;
SliverPersistentHeaderDelegate get _delegate => _StackedListDelegate(
minHeight: minHeight, maxHeight: maxHeight, child: child);
const StackedListChild({
Key key,
#required this.minHeight,
#required this.maxHeight,
#required this.child,
this.pinned = false,
this.floating = false,
}) : assert(child != null),
assert(minHeight != null),
assert(maxHeight != null),
assert(pinned != null),
assert(floating != null),
super(key: key);
#override
Widget build(BuildContext context) => SliverPersistentHeader(
key: key, pinned: pinned, floating: floating, delegate: _delegate);
}
class _StackedListDelegate extends SliverPersistentHeaderDelegate {
final double minHeight;
final double maxHeight;
final Widget child;
_StackedListDelegate({
#required this.minHeight,
#required this.maxHeight,
#required this.child,
});
#override
double get minExtent => minHeight;
#override
double get maxExtent => math.max(maxHeight, minHeight);
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new SizedBox.expand(child: child);
}
#override
bool shouldRebuild(_StackedListDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
Here is how it looks like in action:
Stacked list example .gif
And here is a really good article about Flutter's slivers that might help you in this regard:
Slivers, demystified
Hope this helps you get in the right direction.

How to detect the drawer is closed on flutter?

As title. It since that we can detect the drawer is opened, but is this possible to check it is closed or not? Thanks.
I have added this feature in Flutter 2.0.0. Make sure you are using Flutter SDK version >= 2.0.0 to use this.
Simply use a callback in Scaffold
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: NavDrawer(),
onDrawerChanged: (isOpen) {
// write your callback implementation here
print('drawer callback isOpen=$isOpen');
},
endDrawer: NavDrawerEnd(),
onEndDrawerChanged: (isOpen) {
// write your callback implementation here
print('end drawer callback isOpen=$isOpen');
},
body:
...
Pull request merged in 2.0.0: https://github.com/flutter/flutter/pull/67249
Happy coding!
Declare a GlobalKey to reference your drawer:
GlobalKey _drawerKey = GlobalKey();
Put the key in your Drawer:
drawer: Drawer(
key: _drawerKey,
Check if your drawer is visible:
final RenderBox box = _drawerKey.currentContext?.findRenderObject();
if (box != null){
//is visible
} else {
//not visible
}
You can copy paste run full code below
You can wrap Drawer with a StatefulWidget and put callback in initState() and dispose()
initState() will call widget.callback(true); means open
dispose() will call widget.callback(false); means close
Slide also work in this case
code snippet
drawer: CustomDrawer(
callback: (isOpen) {
print("isOpen ${isOpen}");
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_isDrawerOpen = isOpen;
});
});
},
...
class CustomDrawer extends StatefulWidget {
CustomDrawer({
Key key,
this.elevation = 16.0,
this.child,
this.semanticLabel,
this.callback,
}) : assert(elevation != null && elevation >= 0.0),
super(key: key);
final double elevation;
final Widget child;
final String semanticLabel;
final DrawerCallback callback;
#override
_CustomDrawerState createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State<CustomDrawer> {
#override
void initState() {
if (widget.callback != null) {
widget.callback(true);
}
super.initState();
}
#override
void dispose() {
if (widget.callback != null) {
widget.callback(false);
}
super.dispose();
}
#override
Widget build(BuildContext context) {
return Drawer(
key: widget.key,
elevation: widget.elevation,
semanticLabel: widget.semanticLabel,
child: widget.child);
}
}
working demo
full code
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isDrawerOpen = false;
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: CustomDrawer(
callback: (isOpen) {
print("isOpen ${isOpen}");
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_isDrawerOpen = isOpen;
});
});
},
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('Drawer Header'),
decoration: BoxDecoration(
color: Colors.blue,
),
),
ListTile(
title: Text('Item 1'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
ListTile(
title: Text('Item 2'),
onTap: () {
// Update the state of the app.
// ...
},
),
],
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Align(
alignment: Alignment.centerRight,
child: Text(
_isDrawerOpen.toString(),
),
),
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class CustomDrawer extends StatefulWidget {
CustomDrawer({
Key key,
this.elevation = 16.0,
this.child,
this.semanticLabel,
this.callback,
}) : assert(elevation != null && elevation >= 0.0),
super(key: key);
final double elevation;
final Widget child;
final String semanticLabel;
final DrawerCallback callback;
#override
_CustomDrawerState createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State<CustomDrawer> {
#override
void initState() {
if (widget.callback != null) {
widget.callback(true);
}
super.initState();
}
#override
void dispose() {
if (widget.callback != null) {
widget.callback(false);
}
super.dispose();
}
#override
Widget build(BuildContext context) {
return Drawer(
key: widget.key,
elevation: widget.elevation,
semanticLabel: widget.semanticLabel,
child: widget.child);
}
}
class SecondRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("route test"),
),
body: Text("SecondRoute"));
}
}
You can simply use onDrawerChanged for detecting if the drawer is opened or closed in the Scaffold widget.
Property :
{void Function(bool)? onDrawerChanged}
Type: void Function(bool)?
Optional callback that is called when the Scaffold.drawer is opened or closed.
Example :
#override Widget build(BuildContext context) {
return Scaffold(
onDrawerChanged:(val){
if(val){
setState(() {
//foo bar;
});
}else{
setState(() {
//foo bar;
});
}
},
drawer: Drawer(
child: Container(
)
));
}
When you click a Drawer Item where you will navigate to a new screen, there in the Navigator.push(..) call, you can add a .then(..) clause, and then know when the Drawer item Screen has been popped.
Here is the ListTile for a Drawer item which makes the Navigator.push(..) call when clicked , and the the associated .then(..) callback block:
ListTile(
title: Text('About App'),
onTap: () {
Navigator.push(
_ctxt,
MaterialPageRoute(builder: (context) => AboutScreen()),
).then(
(value) {
print('Drawer callback for About selection');
if (_onReadyCallback != null) {
_onReadyCallback();
}
},
);
}),
_onReadyCallback() represents a Function param you can pass in.
I found this is approach - of leveraging the .then() callback from a .push() call - to be a very useful concept to understand with Flutter in general.
Big thanks to the main 2 answers here:
Force Flutter navigator to reload state when popping
Here's the complete Drawer code:
Drawer drawer = Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
decoration: BoxDecoration(
color: Color(0xFF7FAD5F),
),
child: Text(App.NAME_MENU),
),
ListTile(
title: Text('About App'),
onTap: () {
Navigator.push(
_ctxt,
MaterialPageRoute(builder: (context) => AboutScreen()),
).then(
(value) {
print('Drawer callback for About selection');
if (_onReadyCallback != null) {
_onReadyCallback();
}
},
);
}),
],
),
);
I would recommend that you use this package : https://pub.dev/packages/visibility_detector.
Afterwards you should assign a GlobalKey, like _drawerKey for instance, to the Drawer widget, after which you would be able to detect when the drawer is closed like this:
VisibilityDetector(
key: _drawerKey,
child: Container(),
onVisibilityChanged: (info) {
if (info.visibleFraction == 0.0) {
// drawer not visible.
}
},
)

How to achieve scrollable canvas in Flutter?

I'm a experienced iOS developer, but completely new to the Flutter. Right now I'm facing a problem with ScrollView in Flutter.
What I want to achieve is building a large scrollable canvas. I did it on iOS before, you can see the screenshot here.
The canvas is a big UIScrollView, and each subview on the canvas is draggable, so I can place them at will. Even if the text is very long, I can scroll the canvas to see the full content. Now I need to do the same thing using Flutter.
Currently, I can only drag the text widgets in Flutter. But the parent widget is not scrollable. I know I need to use a scrollable widget in Flutter to achieve the same result, but I just can't make it work. Here's the code I currently have.
void main() {
//debugPaintLayerBordersEnabled = true;
//debugPaintSizeEnabled = true;
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.indigo,
),
home: new MyHomePage(title: 'Flutter Demo Drag Box'),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: DragBox(Offset(0.0, 0.0)));
}
}
class DragBox extends StatefulWidget {
final Offset position; // widget's position
DragBox(this.position);
#override
_DragBoxState createState() => new _DragBoxState();
}
class _DragBoxState extends State<DragBox> {
Offset _previousOffset;
Offset _offset;
Offset _position;
#override
void initState() {
_offset = Offset.zero;
_previousOffset = Offset.zero;
_position = widget.position;
super.initState();
}
#override
Widget build(BuildContext context) {
return new Container(
constraints: BoxConstraints.expand(),
color: Colors.white24,
child: Stack(
children: <Widget>[
buildDraggableBox(1, Colors.red, _offset)
],
)
);
}
Widget buildDraggableBox(int boxNumber, Color color, Offset offset) {
print('buildDraggableBox $boxNumber !');
return new Stack(
children: <Widget>[
new Positioned(
left: _position.dx,
top: _position.dy,
child: Draggable(
child: _buildBox(color, offset),
feedback: _buildBox(color, offset),
//childWhenDragging: _buildBox(color, offset, onlyBorder: true),
onDragStarted: () {
print('Drag started !');
setState(() {
_previousOffset = _offset;
});
print('Start position: $_position}');
},
onDragCompleted: () {
print('Drag complete !');
},
onDraggableCanceled: (Velocity velocity, Offset offset) {
// update position here
setState(() {
Offset _offset = Offset(offset.dx, offset.dy - 80);
_position = _offset;
print('Drag canceled position: $_position');
});
},
),
)
],
);
}
Widget _buildBox(Color color, Offset offset, {bool onlyBorder: false}) {
return new Container(
child: new Text('Flutter widget',
textAlign: TextAlign.center,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 25.0)),
);
}
}
Any suggestions or code samples would be really helpful to me.
PS: Please forget about the rulers on the screenshot, it's not the most important thing to me right now. I just need a big scrollable canvas now.
The below Code may help to resolve your problem it scroll the custom canvas in horizontal direction as you have shown in example image.
import 'package:flutter/material.dart';
class MyScroll extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Canvas Scroller'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: new CustomPaint(
painter: new MyCanvasView(),
size: new Size(width*2, height/2),
),
),
),
);
}
}
class MyCanvasView extends CustomPainter{
#override
void paint(Canvas canvas, Size size) {
var paint = new Paint();
paint..shader = new LinearGradient(colors: [Colors.yellow[700], Colors.redAccent],
begin: Alignment.centerRight, end: Alignment.centerLeft).createShader(new Offset(0.0, 0.0)&size);
canvas.drawRect(new Offset(0.0, 0.0)&size, paint);
var path = new Path();
path.moveTo(0.0, size.height);
path.lineTo(1*size.width/4, 0*size.height/4);
path.lineTo(2*size.width/4, 2*size.height/4);
path.lineTo(3*size.width/4, 0*size.height/4);
path.lineTo(4*size.width/4, 4*size.height/4);
canvas.drawPath(path, new Paint()..color = Colors.yellow ..strokeWidth = 4.0 .. style = PaintingStyle.stroke);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

Resources