How to manipulate the ScrollController in Flutter's sliding up panel plugin? - flutter

I'm using Flutter's sliding_up_panel plugin.
I want to scroll the panel to the top when a new item is selected from my app drawer. Presently selecting a new item closes the panel and refreshes the panel content. Then opens it to a 200px peak, but it doesn't reset the panel's Scroll location to the top.
I've been going around in circles trying the same solutions in slightly different ways and getting nowhere.
What I've tried:
I have global
PanelController slidingPanelController = new PanelController();
ScrollController slideUpPanelScrollController = new ScrollController();
I tried attaching my global slideUpPanelScrollController to my panel's listview, but when swiping up the panel's ListView it simultaneously starts closing the whole panel. If you were scrolling up to read the content you'd skimmed, well, you're not able to because it's disappearing.
Preventing this bug is easy, you do it the canonical way from the plugin's examples, pass the ScrollController through from SlidingPanel and therefore create a local ScrollController in the panel's Listview.
panelBuilder: (slideUpPanelScrollController) => _scrollingList(presentLocation, slideUpPanelScrollController)
The problem then is, you can't scroll the panel on new App drawer selections, because the controller is now local.
I tried putting a listener on the local listview ScrollController, _slideUpPanelScrollController and testing for panelController.close():
if(slidingPanelController.isPanelClosed) _slideUpPanelScrollController.jumpTo(0);
But the listener blocked the panel from swiping, swipe events fired but the panel didn't swipe, or was extremely reluctant too.
Having freshly selected content open in the panel displaying content halfway down the ListView is a glitchy user experience. I would love some ideas or better solutions.
I need it so when the panel is closed, I can
slideUpPanelScrollController.jumpTo(0);
I need the global controller to attach to the panel ListView's local controller, or I need a way to access the local controller to fire its Scroll from outwith my _scrollingList() function.
Here's the panel Widget:
SlidingUpPanel(
key: Key("slidingUpPanelKey"),
borderRadius: slidingPanelBorderRadius,
parallaxEnabled: false,
controller: slidingPanelController,
isDraggable: isDraggableBool,
onPanelOpened: () {
},
onPanelSlide: (value) {
if (value >= 0.98)
setState(() {
slidingPanelBorderRadius =
BorderRadius.vertical(top: Radius.circular(16));
});
},
onPanelClosed: () async {
setState(() {
listViewScrollingOff = true;
});
imageZoomController.value =
Matrix4.identity(); // so next Panel doesn't have zoomed in image
slidingPanelBorderRadius =
BorderRadius.vertical(top: Radius.circular(16));
},
minHeight: panelMinHeight,
maxHeight:
MediaQuery.of(context).size.height - AppBar().preferredSize.height,
panelBuilder: (slideUpPanelScrollController) => _scrollingList(presentLocation, slideUpPanelScrollController),
body: ...
Here's the _scrollingList Widget:
Widget _scrollingList(LocationDetails presentLocation, ScrollController _slideUpPanelScrollController ) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ListView(
controller: _slideUpPanelScrollController,
physics: listViewScrollingOff
? const NeverScrollableScrollPhysics()
: const AlwaysScrollableScrollPhysics(),
key: Key("scrollingPanelListView"),
children: [
This is my onTap from my Drawer ListView item:
onTap: () {
if(slidingPanelController.isPanelShown) {
//slideUpPanelScrollController.jumpTo(0);
slidingPanelController.close();
}
Love help! Below is I think a minimum viable problem. I wrote it in Dartpad, but sharing from Dartpad is nontrivial, so I've copied and pasted it here. Dartpad doesn't support the plugin anyway so it's not like you could tweak it there.
import 'package:flutter/material.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
PanelController slidingPanelController = new PanelController();
ScrollController slideUpPanelScrollController = new ScrollController();
final String title = "sliding panel";
String panelContent = "";
String stupidText = "";
String stupidText2 = ""
int panelMinHeight = 0;
int teaserPanelHeight = 77;
bool listViewScrollingOff = false;
initState() {
super.initState();
for(int i = 0; i < 500; i++) {
stupidText += "More stupid text. ";
}
for(int i = 0; i < 500; i++) {
stupidText2 += "More dumb, dumbest text. ";
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: Text(title)),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(
color: Colors.blue,
),
child: Text('Drawer Header'),
),
ListTile(
title: const Text('Item 1'),
onTap: () {
if(slidingPanelController.isPanelShown) {
print('attempting to scroll to top and close panel');
//slideUpPanelScrollController.jumpTo(0);
slidingPanelController.close();
}
Navigator.of(context).pop();
setState() {
panelContent = stupidText1;
panelMinHeight = teaserPanelHeight;
}
},
),
ListTile(
title: const Text('Item 2'),
onTap: () {
if(slidingPanelController.isPanelShown) {
//slideUpPanelScrollController.jumpTo(0);
slidingPanelController.close();
}
Navigator.of(context).pop();
setState() {
panelContent = stupidText2;
panelMinHeight = teaserPanelHeight;
}
}
),
],
),
),
body: SlidingUpPanel(
key: Key("slidingUpPanelKey"),
borderRadius: 8,
parallaxEnabled: false,
controller: slidingPanelController,
isDraggable: true,
onPanelOpened: () async {
setState(() {
listViewScrollingOff = false;
panelMinHeight = 0;
animatedMarkerMap;
//slideUpPanelScrollController.jumpTo(0);
});
},
onPanelSlide: (value) {
print("onPanelSlide: attempting to scroll panel");
},
onPanelClosed: () async {
setState(() {
//slideUpPanelScrollController.jumpTo(0);
listViewScrollingOff = true;
});
},
minHeight: panelMinHeight,
maxHeight:
MediaQuery.of(context).size.height - AppBar().preferredSize.height,
// TODO BUG
// SAM, IF I USE PANELBUILDER's ScrollController attached to the panel's ListView, then, when closing, the ListView will move to the top first, then the panel closes,
// however ListView's controller is set to a globalController, this causes a bug when closing the panel, but means you can open/peek the panel from the App drawer,
panelBuilder: (slideUpPanelScrollController) => _scrollingList(panelContent, slideUpPanelScrollController),
body: Center(
child: Text(
'Hello, World!',
style: Theme.of(context).textTheme.headline4,
),
),
),
),
);
}
Widget _scrollingList(String panelContent, ScrollController _slideUpPanelScrollController ) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ListView(
controller: _slideUpPanelScrollController,
physics: listViewScrollingOff
? const NeverScrollableScrollPhysics()
: const AlwaysScrollableScrollPhysics(),
key: Key("scrollingPanelListView"),
children: [Text(panelContent)])));
}
}
OK, so the problem became that when I closed the sliding panel 'naturally', by scrolling back up the panel to it top then sliding the panel down, well both things happened at once.
I've found how to solve this, I need to set SlidingUpPanel's isDraggable property to false, till the user has scrolled to the top of the panel.
Like so...
#override
void initState() {
super.initState();
slideUpPanelScrollController.addListener(() {
if(slideUpPanelScrollController.offset == 0) {
setState(() {
isDraggableBool = true;
});
}
});
}
The shortfall of this approach is the listener is running its test whenever the panel is Scrolled, could it jank the scroll? Is there a better/clearer/more performant way?
For completion I amended setScrollBehaviour to this:
void setScrollBehavior(bool _canScroll, {resetPos = false}) {
setState(() {
canScroll = _canScroll;
isDraggableBool = !_canScroll;
if (resetPos) {
slideUpPanelScrollController.jumpTo(0);
isDraggableBool = true;
}
});
}
So when the user can scroll they can't drag.
When the panel closes, resetPos == true therefore the panel scrolls to the top AND it can be dragged (slid) once more.

==== UPDATE ====
you can clone panel builder scrolling behavior and use the controller freely elsewhere. that way, you don't have to worry much about listview scroll physic or panel states.
did some cleanup, added comments, and fixed the tap/scrolling issues.
The only drawback of this method is that the user has to scroll back to the top to close the panel (you can always wrap your body with a gesture and call resetPanel on tap if needed)
import 'package:flutter/material.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final PanelController panelController = PanelController();
ScrollController? _scrollController;
/// reset the content position of your listview
void resetScrollBehavior() {
// We make sure that our scroll exist and it is attached before reset
if (_scrollController != null && _scrollController!.hasClients) {
_scrollController!.jumpTo(0);
}
}
/// close the panel and reset the scroll behavior
void resetPanel() {
// We make sure our panel is attached and open before executing
if (panelController.isAttached && panelController.isPanelOpen) {
panelController.close();
// Remove this line if you wish to not reset the scrollExtent on panel reset.
resetScrollBehavior();
}
}
void onDrawerItemTap() {
// Reset the panel and pop the screen
resetPanel();
Navigator.of(context).pop();
}
#override
void dispose() {
// we make sure to dispose our controller(s)
_scrollController?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
child: Text('Drawer Header'),
),
ListTile(
title: const Text('Item 1'),
onTap: () {
onDrawerItemTap();
},
),
ListTile(
title: const Text('Item 2'),
onTap: () {
onDrawerItemTap();
},
),
],
),
),
body: SlidingUpPanel(
controller: panelController,
borderRadius: BorderRadius.circular(8),
minHeight: 80,
maxHeight:
MediaQuery.of(context).size.height - AppBar().preferredSize.height,
panelBuilder: (ScrollController sc) {
// ! So we can use local scrollcontroller outside the panel builder
_scrollController ??= sc;
return ScrollingList(
scrollController: _scrollController!,
);
},
body: Center(
child: Text(
'Hello, World!',
style: Theme.of(context).textTheme.headline4,
),
),
),
);
}
}
class ScrollingList extends StatefulWidget {
const ScrollingList({
Key? key,
required this.scrollController,
}) : super(key: key);
final ScrollController scrollController;
#override
_ScrollingListState createState() => _ScrollingListState();
}
class _ScrollingListState extends State<ScrollingList> {
#override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ListView(
controller: widget.scrollController,
children: List.generate(200, (index) => Text('Text #$index')),
),
),
);
}
}
=== OLD ===
Running some test, it seems that panelController.close();
also triggers onPanelClosed();
so you can take advantage of that to handle your scrolling behaviors.
The following sample demonstrates how you can reset your panel and/or listview by tapping on a drawer item.
import 'package:flutter/material.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
PanelController panelController = PanelController();
ScrollController scrollController = ScrollController();
bool canScroll = false;
void onDrawerItemTap() {
if (panelController.isAttached && panelController.isPanelOpen) {
panelController.close();
}
Navigator.of(context).pop();
}
void setScrollBehavior(bool _canScroll, {resetPos = false}) {
setState(() {
canScroll = _canScroll;
if (resetPos) {
scrollController.jumpTo(0);
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
child: Text('Drawer Header'),
),
ListTile(
title: const Text('Item 1'),
onTap: () {
onDrawerItemTap();
},
),
ListTile(
title: const Text('Item 2'),
onTap: () {
onDrawerItemTap();
},
),
],
),
),
body: SlidingUpPanel(
controller: panelController,
borderRadius: BorderRadius.circular(8),
parallaxEnabled: false,
isDraggable: true,
onPanelOpened: () {
setScrollBehavior(true);
},
onPanelSlide: (_) {
// panel slide
},
onPanelClosed: () {
setScrollBehavior(false, resetPos: true);
},
minHeight: 80,
maxHeight:
MediaQuery.of(context).size.height - AppBar().preferredSize.height,
panelBuilder: (_) => ScrollingList(
canScroll: canScroll,
scrollController: scrollController,
),
body: Center(
child: Text(
'Hello, World!',
style: Theme.of(context).textTheme.headline4,
),
),
),
);
}
}
class ScrollingList extends StatefulWidget {
const ScrollingList({
Key? key,
this.canScroll = false,
required this.scrollController,
}) : super(key: key);
final bool canScroll;
final ScrollController scrollController;
#override
_ScrollingListState createState() => _ScrollingListState();
}
class _ScrollingListState extends State<ScrollingList> {
#override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ListView(
controller: widget.scrollController,
physics: !widget.canScroll
? const NeverScrollableScrollPhysics()
: const AlwaysScrollableScrollPhysics(),
children: List.generate(200, (index) => Text('Text #$index')),
),
),
);
}
}

ListView might be the issue here. You could try to jump to the top before closing the panel instead of after. You could also try to provide a key to your ListView so it forces flutter to re-render. Eventually, if it doesn't work wrap the body of the slide panel inside a SingleChildScrollview and disable ListView scrolling using physics. It's a bit difficult to visualize a fix though. If you can provide a link to DartPad it would be much easier.

Related

How can I scroll down and focus to a specific widget in flutter?

I am implementing a tutorial of app using https://pub.dev/packages/tutorial_coach_mark . This marked button of beyond the view. So when I need to target this button, I need to scroll/focus this specific part. But I can not find any solution. Can anyone help me with that please?
One Idea is to , Make one Listview with all your widgets . then
Use this :
scroll_to_index: ^2.1.1
Example:
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.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: 'Scroll To Index Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Scroll To Index Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const maxCount = 100;
static const double maxHeight = 1000;
final random = math.Random();
final scrollDirection = Axis.vertical;
late AutoScrollController controller;
late List<List<int>> randomList;
#override
void initState() {
super.initState();
controller = AutoScrollController(
viewportBoundaryGetter: () =>
Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
axis: scrollDirection);
randomList = List.generate(maxCount,
(index) => <int>[index, (maxHeight * random.nextDouble()).toInt()]);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: [
IconButton(
onPressed: () {
setState(() => counter = 0);
_scrollToCounter();
},
icon: Text('First'),
),
IconButton(
onPressed: () {
setState(() => counter = maxCount - 1);
_scrollToCounter();
},
icon: Text('Last'),
)
],
),
body: ListView(
scrollDirection: scrollDirection,
controller: controller,
children: randomList.map<Widget>((data) {
return Padding(
padding: EdgeInsets.all(8),
child: _getRow(data[0], math.max(data[1].toDouble(), 50.0)),
);
}).toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: _nextCounter,
tooltip: 'Increment',
child: Text(counter.toString()),
),
);
}
int counter = -1;
Future _nextCounter() {
setState(() => counter = (counter + 1) % maxCount);
return _scrollToCounter();
}
Future _scrollToCounter() async {
await controller.scrollToIndex(counter,
preferPosition: AutoScrollPosition.begin);
controller.highlight(counter);
}
Widget _getRow(int index, double height) {
return _wrapScrollTag(
index: index,
child: Container(
padding: EdgeInsets.all(8),
alignment: Alignment.topCenter,
height: height,
decoration: BoxDecoration(
border: Border.all(color: Colors.lightBlue, width: 4),
borderRadius: BorderRadius.circular(12)),
child: Text('index: $index, height: $height'),
));
}
Widget _wrapScrollTag({required int index, required Widget child}) =>
AutoScrollTag(
key: ValueKey(index),
controller: controller,
index: index,
child: child,
highlightColor: Colors.black.withOpacity(0.1),
);
}
This will work Perfectly
final dataKey = new GlobalKey();
SingleChildScrollView(
child: Column(
children: [
otherwidgets(),
otherwidgets(),
Container(
key: controller.dataKey,
child: helpPart(context),
),
otherwidgets(),
otherwidgets(),
],
),
on action: Scrollable.ensureVisible(dataKey.currentContext!);
This worked for me!

how to get a grid element accessible in flutter by clicking on it?

is there anyone who can help me ?
I am currently on a project where i want to visualize pathfinding-algorithms by using flutter (i want to use it as app later on).
My Problem:
I have a gridPaper and it's perfectly formatted for my needs... but how can i make the single elements in it accessible by clicking on them ?
I want to create a 'wall' between the start- and endnode to make it harder for the pathfinding-algorithm. (if that makes sense)
But at first i need to create a start- end endnode as well.
Here is what i have so far:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final appTitle = 'Path Finder';
final Color gridColor = Colors.lightBlue[100];
#override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: GridPaper(
child: Container(),
color: Colors.lightBlue[100],
interval: 20,
divisions: 1,
subdivisions: 1,
),
drawer: Drawer(
// Add a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
child: ListView(
// Important: Remove any padding from the ListView.
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('Drawer Header'),
decoration: BoxDecoration(
color: Colors.blue,
),
),
ListTile(
title: Text('Startpunkt'),
onTap: () {
// Update the state of the app
// ...
// Then close the drawer
Navigator.pop(context);
},
),
ListTile(
title: Text('Ziel'),
onTap: () {
// Update the state of the app
// ...
// Then close the drawer
Navigator.pop(context);
},
),
],
),
),
);
}
}
LG Robsen
Since your GridPaper is defined with intervals of 20, it will be quite easy to use the localPosition of the details of an onTapDown callback provided by a GestureDetector on the whole GridPaper:
Full source code
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final appTitle = 'Path Finder';
final Color gridColor = Colors.lightBlue[100];
#override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends HookWidget {
final double cellSize = 20.0;
final String title;
MyHomePage({
Key key,
this.title,
}) : super(key: key);
#override
Widget build(BuildContext context) {
final _activated = useState<List<Offset>>([]);
void _toggle(Offset offset) {
if (!_activated.value.remove(offset)) _activated.value.add(offset);
_activated.value = [..._activated.value];
}
return Scaffold(
appBar: AppBar(title: Text(title)),
body: GestureDetector(
onTapDown: (details) => _toggle(details.localPosition ~/ cellSize),
child: GridPaper(
child: Stack(
children: [
Container(color: Colors.white),
..._activated.value.map((offset) {
print('OFFSET: $offset');
return Positioned(
left: offset.dx * cellSize,
top: offset.dy * cellSize,
width: cellSize,
height: cellSize,
child: ColoredBox(color: Colors.green.shade200),
);
}).toList(),
],
),
color: Colors.lightBlue[100],
interval: cellSize,
divisions: 1,
subdivisions: 1,
),
),
drawer: Drawer(
// Add a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
child: ListView(
// Important: Remove any padding from the ListView.
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('Drawer Header'),
decoration: BoxDecoration(
color: Colors.blue,
),
),
ListTile(
title: Text('Startpunkt'),
onTap: () {
// Update the state of the app
// ...
// Then close the drawer
Navigator.pop(context);
},
),
ListTile(
title: Text('Ziel'),
onTap: () {
// Update the state of the app
// ...
// Then close the drawer
Navigator.pop(context);
},
),
],
),
),
);
}
}

How to Automaticallly scroll to a position of a Row inside SingleChildScrollView in Flutter

I am using a SingleChildScrollView. Its scrollDirection is set to Horizontal with >20 child widgets placed inside a Row Widget. I want to programmatically scroll to a position widget(i.e, 5th or 6th position) in the Row. Is there any way to do it programmatically ?
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: buttons,
),
)
The easiest way to doing this is using Scrollable.ensureVisible.
ensureVisible method
Scrolls the scrollables that enclose the given
context so as to make the given context visible.
Please see the code below :
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _value = 0;
static final List<GlobalKey> _key = List.generate(20, (index) => GlobalKey());
final List<Widget> buttons = List.generate(
20,
(index) => RaisedButton(
onPressed: () {},
color: index % 2 == 0 ? Colors.grey : Colors.white,
child: Text("Button No # ${index + 1}", key: _key[index]),
),
);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: buttons,
),
),
DropdownButton(
value: _value,
items: List.generate(
20,
(index) => DropdownMenuItem(
child: Text("Goto Button # ${index + 1}"), value: index),
),
onChanged: (value) {
setState(() {
_value = value;
print("calling");
Scrollable.ensureVisible(_key[value].currentContext);
});
},
)
],
),
);
}
}
You could define a ScrollController:
ScrollController _controller = new ScrollController();
Pass it to the SingleChildScrollView:
SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: Row(
children: buttons,
),
),
And programmatically scroll it as follows:
void scroll(double position) {
_scrollController.jumpTo(position);
}
Or, if a scroll animation is desired:
void scrollAnimated(double position) {
_scrollController.animateTo(position, Duration(seconds: 1), Curves.ease);
}
If you'd like to automatically scroll immediately after the layout has been built, you could do so by overriding the initState method:
#override
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => scroll(500)); // scroll automatically 500px (as an example)
}
You can add ScrollController in SingleChildScrollView and scroll to your specific position which you want
_scrollController.animateTo(
//here specifing position= 100 mean 100px
100,
curve: Curves.ease,
duration: Duration(seconds: 1),

Flutter persistent app bar across PageView

Ideally I would like to set up my Flutter app as follows
PageView to swipe left/right between 3 pages and a bottom navigation bar to serve as a label and also help with navigation
Persistent appbar on top with drawer and contextual icons
Page content in between
As can be seen in the image, I have this mostly set up the way I would like in the following manner
main.dart - app entry point, set up appbar, set up pageview with children for new PeoplePage, new TimelinePage, new StatsPage
people_page.dart
timeline_page.dart
stats_page.dart
These three pages just deliver the content to the PageView children as required.
Is this the correct way to achieve this? On the surface it works fine. The issue I am coming across is that on the people page I want to implement a selectable list that changes the appbar title/color as in this example, but the appbar is set up on the main page. Can I access the appbar globally?
I could build a new appbar for each page, but I dont want a new appbar swiping in when switching page. I'd prefer the appbar to look persistent and only have the content swipe in.
Any guidance on the best way to accomplish this would be appreciated.
I put together a quick example of how you might communicate from your screen down to the pages and then also back again. This should solve your problem.
https://gist.github.com/slightfoot/464fc225b9041c2d66ec8ab36fbdb935
import 'package:flutter/material.dart';
void main() => runApp(TestApp());
class TestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.green[900],
scaffoldBackgroundColor: Colors.grey[200],
),
home: MainScreen(),
);
}
}
class AppBarParams {
final Widget title;
final List<Widget> actions;
final Color backgroundColor;
AppBarParams({
this.title,
this.actions,
this.backgroundColor,
});
}
class MainScreen extends StatefulWidget {
final int initialPage;
const MainScreen({
Key key,
this.initialPage = 0,
}) : super(key: key);
#override
MainScreenState createState() => MainScreenState();
static MainScreenState of(BuildContext context) {
return context.ancestorStateOfType(TypeMatcher<MainScreenState>());
}
}
class MainScreenState extends State<MainScreen> {
final List<GlobalKey<MainPageStateMixin>> _pageKeys = [
GlobalKey(),
GlobalKey(),
GlobalKey(),
];
PageController _pageController;
AppBarParams _params;
int _page;
set params(AppBarParams value) {
setState(() => _params = value);
}
#override
void initState() {
super.initState();
_page = widget.initialPage ?? 0;
_pageController = PageController(initialPage: _page);
WidgetsBinding.instance.addPostFrameCallback((_) {
_pageKeys[0].currentState.onPageVisible();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: _params?.title,
actions: _params?.actions,
backgroundColor: _params?.backgroundColor,
),
body: PageView(
controller: _pageController,
onPageChanged: _onPageChanged,
children: <Widget>[
PeoplePage(key: _pageKeys[0]),
TimelinePage(key: _pageKeys[1]),
StatsPage(key: _pageKeys[2]),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _page,
onTap: _onBottomNavItemPressed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
title: Text('people'),
icon: Icon(Icons.people),
),
BottomNavigationBarItem(
title: Text('timeline'),
icon: Icon(Icons.history),
),
BottomNavigationBarItem(
title: Text('stats'),
icon: Icon(Icons.pie_chart),
),
],
),
);
}
#override
void reassemble() {
super.reassemble();
_onPageChanged(_page);
}
void _onPageChanged(int page) {
setState(() => _page = page);
_pageKeys[_page].currentState.onPageVisible();
}
void _onBottomNavItemPressed(int index) {
setState(() => _page = index);
_pageController.animateToPage(
index,
duration: Duration(milliseconds: 400),
curve: Curves.fastOutSlowIn,
);
}
}
abstract class MainPageStateMixin<T extends StatefulWidget> extends State<T> {
void onPageVisible();
}
class PeoplePage extends StatefulWidget {
const PeoplePage({Key key}) : super(key: key);
#override
PeoplePageState createState() => PeoplePageState();
}
class PeoplePageState extends State<PeoplePage> with MainPageStateMixin {
final List<Color> _colors = [
Colors.orange,
Colors.purple,
Colors.green,
];
int _personCount = 3;
#override
void onPageVisible() {
MainScreen.of(context).params = AppBarParams(
title: Text('People'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.person_add),
onPressed: () => setState(() => _personCount++),
),
],
backgroundColor: Colors.green,
);
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _personCount,
itemBuilder: (BuildContext context, int index) {
return Card(
child: InkWell(
onTap: () => _onTapCard(index),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Material(
type: MaterialType.circle,
color: _colors[index % _colors.length],
child: Container(
width: 48.0,
height: 48.0,
alignment: Alignment.center,
child: Text('$index', style: TextStyle(color: Colors.white)),
),
),
SizedBox(width: 16.0),
Text(
'Item #$index',
style: TextStyle(
color: Colors.grey[600],
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
},
);
}
void _onTapCard(int index) {
Scaffold.of(context).showSnackBar(SnackBar(content: Text('Item #$index')));
}
}
class TimelinePage extends StatefulWidget {
const TimelinePage({Key key}) : super(key: key);
#override
TimelinePageState createState() => TimelinePageState();
}
class TimelinePageState extends State<TimelinePage> with MainPageStateMixin {
#override
void onPageVisible() {
MainScreen.of(context).params = AppBarParams(
title: Text('Timeline'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.alarm_add),
onPressed: () {},
),
],
backgroundColor: Colors.purple,
);
}
#override
Widget build(BuildContext context) {
return Center(
child: Text('Coming soon'),
);
}
}
class StatsPage extends StatefulWidget {
const StatsPage({Key key}) : super(key: key);
#override
StatsPageState createState() => StatsPageState();
}
class StatsPageState extends State<StatsPage> with MainPageStateMixin {
#override
void onPageVisible() {
MainScreen.of(context).params = AppBarParams(
title: Text('Stats'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add_box),
onPressed: () {},
),
],
backgroundColor: Colors.orange,
);
}
#override
Widget build(BuildContext context) {
return Center(
child: Text('Coming soon'),
);
}
}
One way to tackle this would be to have the AppBar title and background color as state variables, and in your PageView set the onPageChanged to a function. This function takes in the page int and based on the page int it sets the state of the title and color to the values that you desire. For the multiselect list you set the title to the variable which keeps the values you have selected, may be keep it as a state variable in the main page and pass it down to the child component. You can use any of the state management strategies and that should probably work fine.
Example of onPageChanged function:
void onPageChanged(int page) {
String _temptitle = "";
Color _tempColor;
switch (page) {
case 0:
_temptitle = "People";
_tempColor = Colors.pink;
break;
case 1:
_temptitle = "Timeline";
_tempColor = Colors.green;
break;
case 2:
_temptitle = "Stats";
_tempColor = Colors.deepPurple;
break;
}
setState(() {
this._page = page;
this._title = _temptitle;
this._appBarColor = _tempColor;
});
}
So for the multiselect case, instead of setting the title to some constant you set the title to the variable which holds the values of the selected options.
Full code is here:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
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> {
PageController _pageController;
int _page = 0;
String _title = "MyApp";
Color _appBarColor = Colors.pink;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(_title),
backgroundColor: _appBarColor,
),
body: PageView(
children: <Widget>[
Container(
child: Center(child: Text("People")),
),
Container(
child: Center(child: Text("Timeline")),
),
Container(
child: Center(child: Text("Stats")),
),
],
controller: _pageController,
onPageChanged: onPageChanged,
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.people),
title: Text("People"),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_time),
title: Text("Timeline"),
),
BottomNavigationBarItem(
icon: Icon(Icons.pie_chart),
title: Text("Stats"),
),
],
onTap: navigateToPage,
currentIndex: _page,
),
);
}
void navigateToPage(int page) {
_pageController.animateToPage(page,
duration: Duration(milliseconds: 300), curve: Curves.ease);
}
void onPageChanged(int page) {
String _temptitle = "";
Color _tempColor;
switch (page) {
case 0:
_temptitle = "People";
_tempColor = Colors.pink;
break;
case 1:
_temptitle = "Timeline";
_tempColor = Colors.green;
break;
case 2:
_temptitle = "Stats";
_tempColor = Colors.deepPurple;
break;
}
setState(() {
this._page = page;
this._title = _temptitle;
this._appBarColor = _tempColor;
});
}
#override
void initState() {
super.initState();
_pageController = new PageController();
_title = "People";
}
#override
void dispose() {
super.dispose();
_pageController.dispose();
}
}
You can improve this code for your needs. Hope this was helpful in someway. Let me know if there is something I can improve about this answer.

How to implement a right navbar in flutter?

The flutter scaffold shows a right navbar, but I suppose there is no right nav widget. How do I implement a right navbar with scaffold in flutter?
Flutter Scaffold Image
The Scaffold now has a endDrawer property which swipes from right-to-left.
Hope this might help someone.
If you are trying to show a right bar/menu or Drawer in your app, whether it is has a permanent view or a temporary one. I was able to achieve this by building my own custom widget from Allign, Container and Column widgets, and by using setState to show or hide the menu bar based on user interaction, see this simple example.
My custom menu widget looks like the following:
class RightNavigationBar extends StatefulWidget {
#override
_RightNavigationBarState createState() => new _RightNavigationBarState();
}
class _RightNavigationBarState extends State<RightNavigationBar> {
#override
Widget build(BuildContext context) {
return new Align(
alignment: FractionalOffset.centerRight,
child: new Container(
child: new Column(
children: <Widget>[
new Icon(Icons.navigate_next),
new Icon(Icons.close),
new Text ("More items..")
],
),
color: Colors.blueGrey,
height: 700.0,
width: 200.0,
),
);
}
}
Then when the user presses the menu icon, an object of my custom RightNavigationBar widget is created inside setState :
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
var _myRightBar = null;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
actions: [new IconButton(
icon: new Icon (Icons.menu), onPressed: _showRightBar)
],
title: new Text("Right Navigation Bar Example"),
),
body: _myRightBar
);
}
_showRightBar() {
setState(() {
_myRightBar == null
? _myRightBar = new RightNavigationBar()
: _myRightBar = null;
});
}
}
vertical_navigation_bar
How to use it? #
Install
dependencies:
vertical_navigation_bar: ^0.0.1
Run flutter command
flutter pub get
import 'package:flutter/material.dart';
import 'package:vertical_navigation_bar/vertical_navigation_bar.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Abubakr Elghazawy (Software Developer)',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final pageController = PageController(
initialPage: 0,
keepPage: true
);
final navItems = [
SideNavigationItem(icon: FontAwesomeIcons.calendarCheck, title: "New task"),
SideNavigationItem(icon: FontAwesomeIcons.calendarAlt, title: "Personal task"),
SideNavigationItem(icon: FontAwesomeIcons.fileAlt, title: "Personal document"),
SideNavigationItem(icon: FontAwesomeIcons.calendar, title: "Company task"),
SideNavigationItem(icon: FontAwesomeIcons.arrowCircleRight, title: "Options")
];
final initialTab = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
SideNavigation(
navItems: this.navItems,
itemSelected: (index){
pageController.animateToPage(
index,
duration: Duration(milliseconds: 300),
curve: Curves.linear
);
},
initialIndex: 0,
actions: <Widget>[
],
),
Expanded(
child: PageView.builder(
itemCount: 5,
controller: pageController,
scrollDirection: Axis.vertical,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index){
return Container(
color: Colors.blueGrey.withOpacity(0.1),
child: Center(
child: Text("Page " + index.toString()),
)
);
},
),
)
],
),
);
}
}
More Dtailsenter link description here
Use the endDrawer property of the Scaffold like this:
Scaffold(
resizeToAvoidBottomInset: false,
key: _scaffoldKey,
endDrawer: const SideBar(),
body: CustomScrollView(