I want to change the icon of an active bottomnavigationbar item, ie if the item is selected the icon is filled and if it’s unselected it shows the outline.
Check the code below with explanations:
import 'package:flutter/material.dart';
class MainScreen extends StatefulWidget {
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen>
with SingleTickerProviderStateMixin {
// we need this to switch between tabs
TabController _tabController;
// here we remember the current tab, by default is the first one (index 0)
int _currentTabIndex = 0;
#override
void initState() {
super.initState();
// init the TabController
_tabController = TabController(vsync: this, length: _Tab.values.length);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
backgroundColor: Colors.white,
title: Text(_getTitleForCurrentTab(_Tab.values[_currentTabIndex])), // set the title in the AppBar
),
body: TabBarView(
controller: _tabController, // we set our instantiated TabController as the controller
children: <Widget>[
// here we put the screen widgets corresponding to each tab
// the order must correspond to the order given below in bottomNavigationBar
Tab1Widget(), // these are your custom widgets for each tab you have
Tab2Widget(),
Tab3Widget(),
],
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (int index) {
// when a tab icon was tapped we just change the current index of the tab with the new one
// this set state call will re-render the screen and will set new the tab as active
setState(() {
_currentTabIndex = index;
});
// also we want to change the screen content not just the active tab icon
// so we use the TabController to go to another tab giving it the index of the tab which was just clicked
_tabController.animateTo(index);
},
// here we render all the icons we want in the BottomNavigationBar
// we get all the values of the _Tab enum and for each one we render a BottomNavigationBarItem
items: _Tab.values
.map((_Tab tab) => BottomNavigationBarItem(
title: Text(_getTitleForCurrentTab(tab)), // set the title of the tab icon
icon: Image.asset(
_getAssetForTab(tab),
width: 24.0,
height: 24.0,
))) // set the icon of the tab
.toList(),
),
);
}
/// Get the asset icon for the given tab
String _getAssetForTab(_Tab tab) {
// check if the given tab parameter is the current active tab
final active = tab == _Tab.values[_currentTabIndex];
// now given the tab param get its icon considering the fact that if it is active or not
if (tab == _Tab.TAB1) {
return active ? 'assets/tab1_active.png' : 'assets/tab1.png';
} else if (tab == _Tab.TAB2) {
return active ? 'assets/tab2_active.png' : 'assets/tab2.png';
}
return active ? 'assets/tab3_active.png' : 'assets/tab3.png';
}
/// Get the title for the current selected tab
String _getTitleForCurrentTab(_Tab tab) {
if (tab == _Tab.TAB1) {
return 'tab1_title';
} else if (tab == _Tab.TAB2) {
return 'tab2_title';
}
return 'tab3_title';
}
}
// Just an enum with all the tabs we want to have
enum _Tab {
TAB1,
TAB2,
TAB3,
}
Related
I have a bottom navigation bar like this:
When i click (1) in tab Home, it navigate to Detail page of item in tab My Learning (2) but not tab My Learning (using Navigator.of(context).push()). But not change tab to My Learning in bottom navigation bar, it still tab Home. So how i fix that. Thank you.
Some code:
Class main_view_model (contain bottom navigation bar):
BottomTabItemAfterSignIn _currentBottomTab = BottomTabItemAfterSignIn.home;
BottomTabItemAfterSignIn get currentBottomTab => _currentBottomTab;
void changeTab(BottomTabItemAfterSignIn tab, {bool isBackClick = false}) {
if (_currentBottomTab != tab) {
_currentBottomTab = tab;
}
}
Class main_view:
#override
Widget buildView(BuildContext context) {
return WillPopScope(
onWillPop: () async {
_onPressBackDevice();
return false;
},
child: Selector<MainUserViewModel, BottomTabItemAfterSignIn>(
selector: (_, viewModel) => viewModel.currentBottomTab,
builder: (_, currentBottomTab, __) {
return Scaffold(
backgroundColor: AppColor.neutrals.shade900,
body: _buildBody(),
bottomNavigationBar: BottomNavigationUserWidget(
currentTab: currentBottomTab,
onSelectTab: _selectTab,
),
);
},
),
);
}
Widget _buildBody() {
if (viewModel.currentBottomTab == BottomTabItemAfterSignIn.home) {
return _buildTabItem(
BottomTabItemAfterSignIn.home,
_cacheBottomTabWidgets[BottomTabItemAfterSignIn.home],
);
} else if (viewModel.currentBottomTab == BottomTabItemAfterSignIn.course) {
return _buildTabItem(
BottomTabItemAfterSignIn.course,
_cacheBottomTabWidgets[BottomTabItemAfterSignIn.course],
);
} else if (viewModel.currentBottomTab ==
BottomTabItemAfterSignIn.myLearning) {
return _buildTabItem(
BottomTabItemAfterSignIn.myLearning,
_cacheBottomTabWidgets[BottomTabItemAfterSignIn.myLearning],
);
} else {
return _buildTabItem(
BottomTabItemAfterSignIn.setting,
_cacheBottomTabWidgets[BottomTabItemBeforeSignIn.setting],
);
}
}
Widget _buildTabItem(BottomTabItemAfterSignIn tabItem, Widget? child) {
// Cache Widget
return Offstage(
offstage: viewModel.currentBottomTab != tabItem,
child: child ??
(viewModel.currentBottomTab == tabItem
? _buildCacheTab(tabItem)
: Container()),
);
}
Widget _buildCacheTab(BottomTabItemAfterSignIn tabItem) {
return _cacheBottomTabWidgets[tabItem] =
BottomBodyNavigationUserWidget(
bottomMenuBar: tabItem,
navigatorKey: _bottomTabKeys[tabItem]!,
);
}
void _selectTab(BottomTabItemAfterSignIn bottomMenuBar) {
viewModel.changeTab(bottomMenuBar);
}
I hope you are using stateful widget if so then you have to use setState((){}) to to effect any value change so your code for _currentBottomTab = tab; should like this:
setState((){
_currentBottomTab = tab;
});
For more info on stateful widget please check here.
use the same fun on 1-tab nd pass the index number where you want to go
changeTab(2),
I have a bottom navigation bar in my flutter application that contains multiple tabs. In this app, I am primarily facing 2 issues:
If I press the android back button on the home page of any of the tabs, I am navigated back to my phone's home screen, I expect to be taken to the last tab. I referred this question but they are using the maybePop() method on State inside the onWillPop function of the WillPopScope widget which is giving me an error.
This is the widget handling tab navigation:
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int currentPage = 0;
final GlobalKey<NavigatorState> mainKey = GlobalKey();
final GlobalKey<NavigatorState> settingsKey = GlobalKey();
#override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
body: Stack(
children: [
buildOffstatgeNavigator(0, mainKey),
buildOffstatgeNavigator(1, settingsKey),
],
),
backgroundColor: const Color(0xff262647),
bottomNavigationBar: GNav(
tabBorderRadius: 25,
gap: 5, // the tab button gap between icon and text
color: const Color(0xff9A9A9A),
// activeColor: const Color.fromARGB(
// 255, 47, 15, 83), // selected icon and text color
activeColor: const Color(0xff472C69),
tabBackgroundColor: const Color(0xffBA99FF)
.withOpacity(0.9), // selected tab background color
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 15), // navigation bar padding
tabs: const [
GButton(
icon: TimeTypeIcons.home,
text: "Home",
),
GButton(
icon: Icons.settings,
text: "Settings",
),
],
selectedIndex: currentPage,
onTabChange: (index) {
setState(() {
currentPage = index;
});
},
),
),
onWillPop: () async {
return false;
});
}
Map<String, WidgetBuilder> _routeBuilders(BuildContext context, int index) {
return {
'/': (context) {
return [
MainPage(),
SettingsPage()
].elementAt(index);
},
};
}
Widget buildOffstatgeNavigator(int index, GlobalKey key) {
var routeBuilders = _routeBuilders(context, index);
return Offstage(
offstage: currentPage != index,
child: WillPopScope(
child: Navigator(
key: key,
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(
builder: (context) =>
routeBuilders[routeSettings.name]!(context),
);
},
),
onWillPop: () async {
return !(await key.currentState!.maybePop());
}),
);
}
}
I have a button in the MainPage widget which further routes to another page. After I press the back button on this page, I expect the app to navigate me back to the MainPage. Instead, this back press is handled by the tab navigator and navigates me back to my phone's home screen.
What can I do to solve these problems?
My app has an introductory feature where it simply informs the user on an action to take, the issue is this help action text (Container(...)) does not get removed one the setState() function is called.
Logical overview of process:
-> `User launches app`
|-> `login`
|-> `show main UI (with help action if first time launch)`
|-> first time launch ? show help text : don't show
| User acknowledges help text, set in preferences
Below are some code snippets of the dart fragments
UiHomePage (main UI - this is the parent UI)
class HomePage extends StatefulWidget {
const HomePage({Key key}) : super(key: key);
#override
_HomePage createState() => _HomePage();
}
class _HomePage extends State<HomePage> {
#override
Widget build(BuildContext context) {
Widget pageDashboardUser() {
...
// Notify UiComponentPartnerSelector if we should show help action text based on AppSharedPreferences().isFirstTap()
Widget middleBrowseCard() {
return new FutureBuilder(
builder: (context, snapshot) {
return UiComponentPartnerSelector(
_displayProfiles, snapshot.data);
},
future: AppSharedPreferences().isFirstTap());
}
var search = topSearch();
var selector = middleBrowseCard();
return Stack(
children: [search, selector],
);
return Scaffold(...)
}
This Widget displays a bunch of profiles with a base card, a text overlay, and a hint text component.
The main focus is showHint define in the constructur (true if the app is launched for the first time), showTapTutorial() which either returns the hint component or an empty container and finally the _onTap(Profile) which handles the onclick event of a card.
UiComponentPartnerSelector (sub UI - the help text is shown here
class UiComponentPartnerSelector extends StatefulWidget {
bool showHint;
final List<Profile> items;
UiComponentPartnerSelector(this.items, this.showHint, {Key key})
: super(key: key);
#override
_UiComponentPartnerSelector createState() => _UiComponentPartnerSelector();
}
class _UiComponentPartnerSelector extends State<UiComponentPartnerSelector> {
UiComponentCard _activeCard;
int _tappedImageIndex = 0;
Widget showTapTutorial() {
if (!widget.showHint) {
return Container();
}
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.6),
borderRadius: BorderRadius.all(Radius.circular(5)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.touch_app,
color: Colors.black.withOpacity(0.6),
),
Text(
"Touch to view partner profile",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black),
)
],
),
);
}
#override
Widget build(BuildContext context) {
Color _standard = Colors.white;
//
// _cache = widget.items.map((e) => {
// e.imageUri.toString(),
// Image.network(e.imageUri.toString())
// });
Future _onTap(Profile e) async {
if (!widget.showHint) {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => UiViewProfile(e)));
} else {
AppSharedPreferences().setFirstTap(false).then((value) {
setState(() {
widget.showHint = false;
});
});
}
}
UiComponentCard createComponentCard(Profile e) {
...
return UiComponentCard(
onTap: () {
_onTap(e);
},
wImage: Center(
child: Image.network(
e.profileImageLink.toString(),
fit: BoxFit.fill,
),
),
wContent:
// Center(
// child: UiTextLine(text: e.displayName),
// ),
Column(
children: [
topBasicInfo(),
Expanded(child: Container()),
showTapTutorial(),
Expanded(child: Container()),
bottomBio()
],
),
);
}
return Container(
child: Stack(...)
);
Problem:
When _onTap(Profile) is clicked and showHint is true.
What should happen:
What SHOULD happen next is AppSharedPreferences().setFirstTap(false) should set the initial tap flag to false, then when finished setState() including setting showHint to false, then rerendering the UI and removing the hint text container (found in showTapTutorial()).
What happens:
What infact happens is when _onTap() is called, it updates the preferences correctly, setState() is called and showHint == false and !widget.showHint in showTapTutorial() is true returning Container() BUT the UI itself doesn't rerender.
Thus after clicking this "button" for the first time, the UI remains (doesn't change). Clicking a second time executes the Navigator.of(context).push(MaterialPageRoute(builder: (context) => UiViewProfile(e))); part WHILE the action help text (tutorial) is still showing. If I click on the same card again
Am I missing something or doing something wrong?
I'm using ListView widget to show items as a list. In a window three, items viewing must the middle item place in the middle.
So how can I detect position of ListView when scrolling stop?
How to detect ListView Scrolling stopped?
I used NotificationListener that is a widget that listens for notifications bubbling up the tree. Then use ScrollEndNotification, which indicates that scrolling has stopped.
For scroll position I used _scrollController that type is ScrollController.
NotificationListener(
child: ListView(
controller: _scrollController,
children: ...
),
onNotification: (t) {
if (t is ScrollEndNotification) {
print(_scrollController.position.pixels);
}
//How many pixels scrolled from pervious frame
print(t.scrollDelta);
//List scroll position
print(t.metrics.pixels);
},
),
majidfathi69's answer is good, but you don't need to add a controller to the list:
(Change ScrollUpdateNotification to ScrollEndNotification when you only want to be notified when scroll ends.)
NotificationListener<ScrollUpdateNotification>(
child: ListView(
children: ...
),
onNotification: (notification) {
//How many pixels scrolled from pervious frame
print(notification.scrollDelta);
//List scroll position
print(notification.metrics.pixels);
},
),
You can also achieve this functionality with the following steps
import 'package:flutter/material.dart';
class YourPage extends StatefulWidget {
YourPage({Key key}) : super(key: key);
#override
_YourPageState createState() => _YourPageState();
}
class _YourPageState extends State<YourPage> {
ScrollController _scrollController;
double _scrollPosition;
_scrollListener() {
setState(() {
_scrollPosition = _scrollController.position.pixels;
});
}
#override
void initState() {
_scrollController = ScrollController();
_scrollController.addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text('Position $_scrollPosition pixels'),
),
body: Container(
child: ListView.builder(
controller: _scrollController,
itemCount: 200,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.mood),
title: Text('Item: $index'),
);
},
),
),
);
}
}
The NotificationListener now accepts a type argument which makes the code shorter :)
NotificationListener<ScrollEndNotification>(
child: ListView(
controller: _scrollController,
children: ...
),
onNotification: (notification) {
print(_scrollController.position.pixels);
// Return true to cancel the notification bubbling. Return false (or null) to
// allow the notification to continue to be dispatched to further ancestors.
return true;
},
),
If you want to detect the scroll position of your ListView, you can simply use this;
Scrollable.of(context).position.pixels
In addition to #seddiq-sorush answer, you can compare the current position to _scrollController.position.maxScrollExtent and see if the list is at the bottom
https://coflutter.com/flutter-check-if-the-listview-reaches-the-top-or-the-bottom/ Source
If some want to Detect the bottom of a listview then use this way
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.metrics.atEdge) {
if (notification.metrics.pixels == 0) {
print('At top');
} else {
print('At bottom');
}
}
return true;
},
child: ListView.builder(itemBuilder: (BuildContext context) {
return YourItemWidget;
})
)
I would say You can easily detect Scroll Position by
#override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(() {
var _currectScrollPosition = _scrollController.position.pixels;//this is the line
});
}
but If you are going to call setState in addListener() ; It is okay, but this will cause to rebuild your entire build(context). This is not a good practice for Animations specially.
What I would recommand is to seprate your scrolling widget into a seprate StatefulWidget , Then get the sate of that widget by calling
_yourScrollableWidgetKey.currentState?.controller.addListener(() {
//code.......
setState(() {});
});
Note: Set a GlobalKey, and assign to your StatFulWidget.
final _yourScrollableWidgetKey = GlobalKey<_YourScrollAbleWidget>();
StackedPositionedAnimated(
key: _yourScrollableWidgetKey,
),
So I want to set the children of my GridView dynamically so I can set it when I want. Currently this is the whole class.
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/CustomColors.dart';
import 'package:flutter_app/quote/Quote.dart';
import 'package:flutter_app/quote/QuoteView.dart';
import 'package:flutter_app/section/Section.dart';
import 'package:flutter_app/quote/QuoteData.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
static List<QuoteData> data = [];
_readJson() async {
data.clear();
var url = 'https://www.dropbox.com/s/7k280ca5dktlhoo/quotes.json?dl=1';
var httpClient = new HttpClient();
httpClient.getUrl(Uri.parse(url)).then((HttpClientRequest request) {
return request.close();
}).then((HttpClientResponse response) {
response.transform(utf8.decoder).listen((contents) {
List<Map> decoded = JSON.decode(contents);
decoded.forEach((m) {
String url = m["url"];
String title = m["title"];
String sectionString = m["section"];
Section section;
for (Section element in Section.values) {
if (element.toString() == "Section." + sectionString) {
section = element;
}
}
List<Quote> quotes = m["quotes"];
data.add(new QuoteData(url, title, section, quotes));
});
});
});
}
#override
Widget build(BuildContext context) {
_readJson();
return new MaterialApp(
title: 'Quotes',
theme: new ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or press Run > Flutter Hot Reload in IntelliJ). Notice that the
// counter didn't reset back to zero; the application is not restarted.
primarySwatch: CustomColors.black,
),
home: new MyHomePage(title: 'Quotes'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static Section currentSection = Section.movies;
void _onTileClicked(QuoteData quote) {
Navigator.push(context,
new MaterialPageRoute(builder: (context) => new QuoteView(quote)));
}
List<Widget> _getTiles(Section section) {
final List<Widget> tiles = <Widget>[];
for (var i in MyApp.data) {
if (i.section != section) {
continue;
}
tiles.add(new GridTile(
child: new InkResponse(
enableFeedback: true,
child: new Image.network(
i.url,
fit: BoxFit.cover,
),
onTap: () => _onTileClicked(i),
)));
}
return tiles;
}
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
final double itemHeight = (size.height - kToolbarHeight - 24) / 2;
final double itemWidth = size.width / 2;
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return new Scaffold(
appBar: new AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text(currentSection
.toString()
.replaceAll("Section.", "")
.substring(0, 1)
.toUpperCase() +
currentSection.toString().replaceAll("Section.", "").substring(1)),
),
body: new Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: new GridView.count(
crossAxisCount: 2,
childAspectRatio: (itemWidth / itemHeight),
padding: const EdgeInsets.all(4.0),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
children: _getTiles(currentSection)),
),
);
}
}
So right now on startup, it launches the application but MyApp.data is empty because the json has to be read so the GridView will be empty, when I refresh the application, the GridView won't be empty because MyApp.data won't be empty anymore. I want to be able to set the children after the json gets read, also I need to be able to change it dynamically because I will add a function for switching sections.
The same goes for the title, I also need to be able to change it dynamically when switching sections.
You can create a loading widget and wait for the request to finish when the request is finished hide the loading and show the grid than rebuild the state. but you need to have a statefullWidget not statelessWidget.
You can see how i use it on the code below I added my comments to the code
class _ChildrenPageState extends State<ChildrenPage> {
//declare the _load to true so it will show the loading when the page is loaded
bool _load = true;
ChildrenService _childrenService = new ChildrenService();
List children = [];
//I call the _rebuild function to rebuild the widgets and show the data
void _rebuild() {
setState(() {
});
}
#override
Widget build(BuildContext context) {
//I call the request to get my data when it is finished i put _load to false so the loading will hide and call the _rebuild function to rebuild the widgets and i have put if so when the widget is built it will not rebuild it a second time.
_childrenService.getChildren(children).then((data){
if(data && _load){
_load = false;
_rebuild();
}
});
//the below widget show the loading container if _load is true else it will show the dataTable in your app you should add the grid and you can pass the data that you got from the request
Widget loadingIndicator = _load ? new Container(
color: Colors.grey[300],
width: 70.0,
height: 70.0,
child: new Padding(
padding: const EdgeInsets.all(5.0),
child: new Center(
child: new CircularProgressIndicator()
)
),
) : new JLDataTable(
data: children,
);
return new Scaffold(
drawer: new Drawer(
child: MenuList.menuList,
),
appBar: new AppBar(title: const Text('Data tables')),
body: new ListView(
padding: const EdgeInsets.all(20.0),
children: <Widget>[
new Align(
child: loadingIndicator,
alignment: FractionalOffset.center
)
]
)
);
}
}