Flutter PageView check if page changed using drag gesture or animateToPage() - flutter

In a Scaffold page with something like the following structure
#override
Widget build(BuildContext context){
body: PageView(
controller: _controller;
children: <Widget>[Page1(), Page2(), Page3()];
);
bottomNavigationBar: BottomNavBar(
onItemSelected: (index) => _controller.animateToPage()
)
}
there are two ways to go from Page2() to Page1():
Swipe the screen from left to right
Tap the Page1() icon on the bottomNavigationBar, and thus calling _controller.animateToPage(0)
The problem is, how can I tell if the page is changed through swiping gesture or animateToPage() function?
Thanks.

Maybe you can add a flag to set if animateToPage is ongoing or not.
Sample:
import 'package:flutter/material.dart';
void main() => runApp(Root());
class Root extends StatelessWidget {
#override
Widget build(BuildContext context) {
final PageController controller = PageController();
bool isAnimateToPage = false;
controller.addListener(() {
print('LISTENER isAnimateToPage = $isAnimateToPage');
});
return MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
Expanded(
child: PageView(
controller: controller,
onPageChanged: (int page) {
print(
'ONPAGECHANGED isAnimateToPage = $isAnimateToPage ; page = $page');
},
children: <Widget>[
const Center(child: Text('page 1')),
const Center(child: Text('page 2')),
const Center(child: Text('page 3')),
],
),
),
FlatButton(
onPressed: () async {
isAnimateToPage = true;
await controller.animateToPage(
controller.page.toInt() + 1,
duration: const Duration(seconds: 1),
curve: Curves.easeIn,
);
isAnimateToPage = false;
},
child: const Text('Next'),
),
],
),
),
);
}
}

Related

Flutter-Button with icon onselected

I am new to flutter. I use group button to build such a screen, what kind of widget can i use? I need it to appear when the button is selected and disappear when it is not selected.
I tried to do it with a group button but can’t add an icon there, i also tried to do it using the chip widget, but there the close icon only works on clicking on it.
you can use InputChip widget like this
import 'package:flutter/material.dart';
void main() => runApp(const ChipApp());
class ChipApp extends StatelessWidget {
const ChipApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
home: const InputChipExample(),
);
}
}
class InputChipExample extends StatefulWidget {
const InputChipExample({super.key});
#override
State<InputChipExample> createState() => _InputChipExampleState();
}
class _InputChipExampleState extends State<InputChipExample> {
int inputs = 3;
int? selectedIndex;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('InputChip Sample'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Wrap(
alignment: WrapAlignment.center,
spacing: 5.0,
children: List<Widget>.generate(
inputs,
(int index) {
return InputChip(
label: Text('Person ${index + 1}'),
selected: selectedIndex == index,
onSelected: (bool selected) {
setState(() {
if (selectedIndex == index) {
selectedIndex = null;
} else {
selectedIndex = index;
}
});
},
onDeleted: () {
setState(() {
inputs = inputs - 1;
});
},
);
},
).toList(),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
setState(() {
inputs = 3;
});
},
child: const Text('Reset'),
)
],
),
),
);
}
}
check the details in the official documentation

Flutter: Change Scroll Offset in a child through NavigationBar

I currently have a Homepage, which consists of a Scaffold, with a bottomAppBar for navigation:
The body has 5 pages, the first page is a feed, which consists of a ListView of Widgets.
What I want to do is same as Instagram has it:
when I scroll down the feed and I click the Feed Button on the Navigation Bar, then I want the ListView to scroll back to the top automatically.
This is part of my code:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
int _selectedIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: [
HomeFeed(),
Page2(),
...
].elementAt(_selectedIndex),
bottomNavigationBar: BottomAppBar(
child: Row(
children: <Widget>[
IconButton(
icon: FaIcon(FontAwesomeIcons.houseUser),
onPressed: (){
if (_selectedIndex == 0) {
//add logic to make the HomeFeed ListView scroll up
} else {
setState((){
_selectedIndex = 0;
});
}
},
IconButton(
icon: FaIcon(FontAwesomeIcons.compass),
onPressed: (){
setState((){
_selectedIndex = 1;
});
},
...
],
),
), //BottomAppBar
), //Scaffold
}
I know that if I had the code of the HomeFeed inside the Scaffold.body then I could just use a Scrollcontroller and the animateTo method. The problem is that the Homefeed is another stateful widget and even though setState is called when clicking the feed icon, the HomeFeed widget is not rebuilding.
I tried defining a Scrollcontroller in the Homepage and pass it to the HomeFeed but it did not work.
Can anyone help me with that?
You can set a GlobalKey for the state of the HomeFeed widget. Using this GlobalKey you can call the functions of the state of the HomeFeed widget.
Main code:
GlobalKey<HomeFeedState> feedKey = new GlobalKey<HomeFeedState>(); // this is new
#override
Widget build(BuildContext context) {
return Scaffold(
body: [
HomeFeed(key: feedKey), // this is new
Page3(),
].elementAt(_selectedIndex),
bottomNavigationBar: BottomAppBar(
child: Row(
children: <Widget>[
IconButton(
icon: FaIcon(FontAwesomeIcons.houseUser),
onPressed: (){
if (_selectedIndex == 0) {
feedKey.currentState.jumpUp(); // this is new
} else {
setState(() {
_selectedIndex = 0;
});
}
},
),
IconButton(
icon: FaIcon(FontAwesomeIcons.compass),
onPressed: (){
setState(() {
_selectedIndex = 1;
});
},
),
],
),
), //BottomAppBar
);
}
HomeFeed:
class HomeFeed extends StatefulWidget {
final GlobalKey<HomeFeedState> key; // this is new
HomeFeed({this.key}) : super(key: key); // this is new
#override
HomeFeedState createState() => HomeFeedState();
}
class HomeFeedState extends State<HomeFeed> {
var _scrollController = new ScrollController();
jumpUp() { // this will be called when tapped on the home icon
_scrollController.animateTo(0,
duration: Duration(seconds: 2), curve: Curves.ease);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
shrinkWrap: true,
controller: _scrollController,
itemCount: 100,
itemBuilder: (context, index) {
return Container(
height: 300,
child: Card(
child: Center(
child: Text('$index'),
),
),
);
},
),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
int _selectedIndex = 0;
Widget homeWidget = HomeFeed();
PageController pageController = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: pageController,
children: <Widget>[
homeWidget,
Second(),
...
],
),
bottomNavigationBar: BottomAppBar(
child: Row(
children: <Widget>[
IconButton(
icon: FaIcon(FontAwesomeIcons.houseUser),
onPressed: (){
setState((){
homeWidget = HomeFeed();
_selectedIndex = 0;
pageController.jumpToPage(0);
});
},
IconButton(
icon: FaIcon(FontAwesomeIcons.compass),
onPressed: (){
setState((){
_selectedIndex = 1;
pageController.jumpToPage(1);
});
},
...
],
),
), //BottomAppBar
), //Scaffold
}
use like this

Flutter: PageView - The method 'jumpToPage' was called on null

I'm relatively new to flutter so it might be a very simple solution. I have implemented a drawer in a Scaffold() with a PageView as the body. I want to be able to jump to the PageView page.no as the Drawer list item is TappedOn.
I know I'm not initialising or setting something somewhere.
I've also tried keepPage: true, but doesn't make a difference.
class MyScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyScreenState();
}
}
class _MyScreenState extends State<MyScreen> {
var _currentIndex = 0;
PageController _pageController;
#override
void initState() {
super.initState();
_currentIndex = 0;
PageController(initialPage: _currentIndex, keepPage: false);
}
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: const Text('AppBar',),
elevation: 0.0,
leading: Builder(
builder: (context) => IconButton(
icon: new Icon(Icons.toc, size: 35.0),
onPressed: () => Scaffold.of(context).openDrawer(),
),
),
actions: <Widget>[
],
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero, // Important: Remove any padding from the ListView.
children: <Widget>[
Container(
height: 120,
child: DrawerHeader(
child: Text('Heading',),
),
),
ListTile(
title: Text('Home',),
onTap: () {
setState(() {
_currentIndex = 2;
});
Navigator.pop(context);
_pageController.jumpToPage(_currentIndex);
},
),
ListTile(),
ListTile(),
....
],
),
),
body: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
children: <Widget>[
new Page1(),
new Page2(),
....
],
)
);
}
}
The above code is throwing the following error:
flutter: The following NoSuchMethodError was thrown while handling a
gesture:
flutter: The method 'jumpToPage' was called on null.
flutter: Receiver: null
flutter: Tried calling: jumpToPage(0)
Just make your PagController public like so: PageController pageController;.

Hide bottom navigation bar on scroll down and vice versa

I have a list in the body and bottom navigation bar. I want to hide bottom navigation bar with a slide down animation when the posts list is scrolled down and visible with a slide up animation when scrolled up. How to do it?
While Naveen's solution works perfectly, I didn't like the idea of using setState to handle the visibility of the navbar. Every time the user would change scroll direction, the entire homepage including the appbar and body would rebuild which can be an expensive operation. I created a separate class to handle the visibility that uses a ValueNotifier to track the current hidden status.
class HideNavbar {
final ScrollController controller = ScrollController();
final ValueNotifier<bool> visible = ValueNotifier<bool>(true);
HideNavbar() {
visible.value = true;
controller.addListener(
() {
if (controller.position.userScrollDirection ==
ScrollDirection.reverse) {
if (visible.value) {
visible.value = false;
}
}
if (controller.position.userScrollDirection ==
ScrollDirection.forward) {
if (!visible.value) {
visible.value = true;
}
}
},
);
}
void dispose() {
controller.dispose();
visible.dispose();
}
}
Now all you do is create a final instance of HideNavbar in your HomePage widget.
final HideNavbar hiding = HideNavbar();
Now pass the instance's ScrollController to the ListView or CustomScrollView body of your Scaffold.
body: CustomScrollView(
controller: hiding.controller,
...
Then surround your bottomNavigationBar with a ValueListenableBuilder that takes the ValueNotifier from the HideNavbar instance and then set the height property of the bottomNavigationBar to be either 0 or any other value depending on the status of the ValueNotifier.
bottomNavigationBar: ValueListenableBuilder(
valueListenable: hiding.visible,
builder: (context, bool value, child) => AnimatedContainer(
duration: Duration(milliseconds: 500),
height: value ? kBottomNavigationBarHeight : 0.0,
child: Wrap(
children: <Widget>[
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.blue,
fixedColor: Colors.white,
unselectedItemColor: Colors.white,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
title: Text('Offers'),
),
BottomNavigationBarItem(
icon: Icon(Icons.account_box),
title: Text('Account'),
),
],
),
],
),
),
),
This approaches keeps avoids countless rebuilds and doesn't require any external libraries. You can also implement this as a stream-based approach but that would require another library such as dart:async and would not be changing anything. Make sure to call the dispose function of HideNavbar inside HomePage's dispose function to clear all resources used.
Working code with BottomNavigationBar.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScrollController _hideBottomNavController;
bool _isVisible;
#override
initState() {
super.initState();
_isVisible = true;
_hideBottomNavController = ScrollController();
_hideBottomNavController.addListener(
() {
if (_hideBottomNavController.position.userScrollDirection ==
ScrollDirection.reverse) {
if (_isVisible)
setState(() {
_isVisible = false;
});
}
if (_hideBottomNavController.position.userScrollDirection ==
ScrollDirection.forward) {
if (!_isVisible)
setState(() {
_isVisible = true;
});
}
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: CustomScrollView(
controller: _hideBottomNavController,
shrinkWrap: true,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(10.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _getItem(context),
childCount: 20,
),
),
),
],
),
),
bottomNavigationBar: AnimatedContainer(
duration: Duration(milliseconds: 500),
height: _isVisible ? 56.0 : 0.0,
child: Wrap(
children: <Widget>[
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.blue,
fixedColor: Colors.white,
unselectedItemColor: Colors.white,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
title: Text('Offers'),
),
BottomNavigationBarItem(
icon: Icon(Icons.account_box),
title: Text('Account'),
),
],
),
],
),
),
);
}
_getItem(BuildContext context) {
return Card(
elevation: 3,
margin: EdgeInsets.all(8),
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Item',
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
)
],
),
),
),
],
),
);
}
}
Working Model
Screenshot (Null safe + Optimized)
Code:
class MyPage extends StatefulWidget {
#override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
late final ScrollListener _model;
late final ScrollController _controller;
final double _bottomNavBarHeight = 56;
#override
void initState() {
super.initState();
_controller = ScrollController();
_model = ScrollListener.initialise(_controller);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: _model,
builder: (context, child) {
return Stack(
children: [
ListView.builder(
controller: _controller,
itemCount: 20,
itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
),
Positioned(
left: 0,
right: 0,
bottom: _model.bottom,
child: _bottomNavBar,
),
],
);
},
),
);
}
Widget get _bottomNavBar {
return SizedBox(
height: _bottomNavBarHeight,
child: BottomNavigationBar(
backgroundColor: Colors.amber,
items: [
BottomNavigationBarItem(icon: Icon(Icons.call), label: 'Call'),
BottomNavigationBarItem(icon: Icon(Icons.message), label: 'Message'),
],
),
);
}
}
class ScrollListener extends ChangeNotifier {
double bottom = 0;
double _last = 0;
ScrollListener.initialise(ScrollController controller, [double height = 56]) {
controller.addListener(() {
final current = controller.offset;
bottom += _last - current;
if (bottom <= -height) bottom = -height;
if (bottom >= 0) bottom = 0;
_last = current;
if (bottom <= 0 && bottom >= -height) notifyListeners();
});
}
}

Flutter - Modify AppBar from a page

So I have a Flutter application with multiple pages, this is done via a PageView. Before this page view I create my AppBar so it is persistent at the top of the application and doesn't animate when scrolling between pages.
I then want on one of the pages to create a bottom App bar, but for that I need to access the App bar element, however I have no idea how to do this.
This is the main class, the page I am trying to edit the app bar on is PlanPage.
final GoogleSignIn googleSignIn = GoogleSignIn();
final FirebaseAuth auth = FirebaseAuth.instance;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: '',
home: _handleCurrentScreen()
);
}
Widget _handleCurrentScreen() {
return StreamBuilder<FirebaseUser>(
stream: auth.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
print(snapshot);
if (snapshot.connectionState == ConnectionState.waiting) {
return SplashPage();
} else {
if (snapshot.hasData) {
return Home();
}
return LoginPage();
}
}
);
}
}
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return HomeState();
}
}
class HomeState extends State<Home> {
PageController _pageController;
PreferredSizeWidget bottomBar;
int _page = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: bottomBar,
),
body: PageView(
children: [
Container(
child: SafeArea(
child: RecipesPage()
),
),
Container(
child: SafeArea(
child: PlanPage()
),
),
Container(
child: SafeArea(
child: ShoppingListPage()
),
),
Container(
child: SafeArea(
child: ExplorePage()
),
),
],
/// Specify the page controller
controller: _pageController,
onPageChanged: onPageChanged
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.book),
title: Text('Recipes')
),
BottomNavigationBarItem(
icon: Icon(Icons.event),
title: Text('Plan')
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart),
title: Text('Shopping List')
),
BottomNavigationBarItem(
icon: Icon(Icons.public),
title: Text("Explore"),
),
],
onTap: navigationTapped,
currentIndex: _page,
),
);
}
void onPageChanged(int page){
setState((){
this._page = page;
});
}
void setBottomAppBar(PreferredSizeWidget appBar) {
this.bottomBar = appBar;
print("setBottomAppBar: "+ appBar.toString());
}
/// Called when the user presses on of the
/// [BottomNavigationBarItem] with corresponding
/// page index
void navigationTapped(int page){
// Animating to the page.
// You can use whatever duration and curve you like
_pageController.animateToPage(
page,
duration: const Duration(milliseconds: 300),
curve: Curves.ease
);
}
#override
void initState() {
super.initState();
initializeDateFormatting();
_pageController = PageController();
}
#override
void dispose(){
super.dispose();
_pageController.dispose();
}
}
The PlanPage class looks like this
class PlanPage extends StatefulWidget {
var homeState;
PlanPage(this.homeState);
#override
State<StatefulWidget> createState() {
return _PlanState(homeState);
}
}
class _PlanState extends State<PlanPage> with AutomaticKeepAliveClientMixin<PlanPage>, SingleTickerProviderStateMixin {
var homeState;
TabController _tabController;
_PlanState(this.homeState);
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
//homeState.setBottomAppBar(_buildTabBar());
return Scaffold(
appBar: AppBar(
bottom: _buildTabBar(),
),
body: TabBarView(
controller: _tabController,
children: Plan.now().days.map((day) {
return ListView.builder(
itemCount: MealType.values.length,
itemBuilder: (BuildContext context, int index){
var mealType = MealType.values[index];
return Column(
children: <Widget>[
Text(
mealType.toString().substring(mealType.toString().indexOf('.')+1),
style: TextStyle(
//decoration: TextDecoration.underline,
fontSize: 30.0,
fontWeight: FontWeight.bold
),
),
Column(
children: day.meals.where((meal) => meal.mealType == mealType).map((meal) {
return RecipeCard(meal.recipe);
}).toList(),
)
],
);
}
);
}).toList(),
)
);
}
Widget _buildTabBar() {
return TabBar(
controller: _tabController,
isScrollable: true,
tabs: List.generate(Plan.now().days.length,(index) {
return Tab(
child: Column(
children: <Widget>[
Text(DateFormat.E().format(Plan.now().days[index].day)),
Text(DateFormat('d/M').format(Plan.now().days[index].day)),
],
),
);
}, growable: true),
);
}
#override
void initState() {
super.initState();
_tabController = new TabController(
length: Plan.now().days.length,
vsync: this,
initialIndex: 1
);
}
}
However the way it works now, makes it show 2 app bars.[
Usually it's a not a best practice to have two nested scrollable areas. Same for two nested Scaffolds.
That said, you can listen to page changes ( _pageController.addListener(listener) ) to update a page state property, and build a different AppBar.bottom (in the Home widget, so you can remove the Scaffold in PlanPage) depending on the page the user is viewing.
-EDIT-
In your Home widget you can add a listener to the _pageController like so:
void initState() {
super.initState();
_pageController = PageController()
..addListener(() {
setState(() {});
});
}
to have your widget rebuilt every time the user scrolls within your PageView. The setState call with an empty function might looks confusing, but it simply allows you to have the widget rebuilt when _pageController.page changes, which is not the default behavior. You could also have a page state property and update it in the setState call to reflect the _pageController.page property, but the result would be the same.
This way you can build a different AppBar.bottom depending on the _pageController.page:
// in your build function
final bottomAppBar = _pageController.page == 2 ? TabBar(...) : null;
final appBar = AppBar(
bottom: bottomAppBar,
...
);
return Scaffold(
appBar: appBar,
...
);