Related
Hi i have a riveanimation that changes flower when the user has checkin, however the page of the animation would not change state once build as it is in a bottomnavigationbar. Is there anyway to reload the page to change the state of the animation. I only want to reload only one page and maintain the others.
This is the animation picture, it should change the leaf to green
here is my bottomnavigationbar page
class bottomnavbar extends StatefulWidget {
const bottomnavbar ({Key? key}) : super(key: key);
#override
_bottomnavbarState createState() => _bottomnavbarState();
}
class _bottomnavbarState extends State<bottomnavbar> {
double screenHeight = 0;
double screenWidth = 0;
String currentDate = DateFormat('yyyy-MM-dd').format(DateTime.now());
Color primary = const Color(0xffeef444c);
int currentIndex = 0;
List<IconData> navigationIcons = [
FontAwesomeIcons.personPraying,
FontAwesomeIcons.leaf,
FontAwesomeIcons.dice,
FontAwesomeIcons.person,
];
#override
Widget build(BuildContext context) {
screenHeight = MediaQuery.of(context).size.height;
screenWidth = MediaQuery.of(context).size.width;
print("Current save date is ${Provider.of<checkinlist>(context, listen: false).checkDate()}");
if( Provider.of<checkinlist>(context, listen: false).checkDate() == 'not set'){
Provider.of<checkinlist>(context, listen: false).saveDatenow();
}
else if (Provider.of<checkinlist>(context, listen: false).checkDate() != currentDate ){
Provider.of<checkinlist>(context, listen: false).overwriteSaveDate();
Provider.of<checkinlist>(context, listen: false).dailyreset();
}
else if (Provider.of<checkinlist>(context, listen: false).checkDate() == currentDate ){
}
return Scaffold(
backgroundColor: Colors.blueGrey[900],
body: IndexedStack(
index: currentIndex,
children: const [
mainmenu(),
FlowerGarden(),
Gamespage(),
ProfilePage(),
],
),
bottomNavigationBar: Container(
height: 70,
margin: const EdgeInsets.only(
left: 12,
right: 12,
bottom: 24,
),
decoration: const BoxDecoration(
color: Colors.lime,
borderRadius: BorderRadius.all(Radius.circular(40)),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: Offset(2, 2),
),
],
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(40)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
for(int i = 0; i < navigationIcons.length; i++)...<Expanded>{
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
currentIndex = i;
});
},
child: Container(
height: screenHeight,
width: screenWidth,
color: Colors.deepPurple[900],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
navigationIcons[i],
color: i == currentIndex ? primary : Colors.white60,
size: i == currentIndex ? 30 : 26,
),
i == currentIndex ? Container(
margin: const EdgeInsets.only(top: 6),
height: 3,
width: 22,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(40)),
color: primary,
),
) : const SizedBox(),
],
),
),
),
),
),
}
],
),
),
),
);
}
}
And this is my riveanimation page
class FlowerGarden extends StatefulWidget{
const FlowerGarden({Key? key,
}) : super(key: key);
#override
_SimpleFlowerGardenState createState() => _SimpleFlowerGardenState();
}
final myCoordinates = Coordinates(3.139003, 101.686855);
// Replace with your own location lat, lng.
final params = CalculationMethod.karachi.getParameters();
final prayerTimes = PrayerTimes.today(myCoordinates, params);
class _SimpleFlowerGardenState extends State<FlowerGarden>{
SMITrigger? _SubuhEarly;
SMITrigger? _SubuhLate;
SMITrigger? _ZohorEarly;
SMITrigger? _ZohorLate;
SMITrigger? _AsarEarly;
SMITrigger? _AsarLate;
SMITrigger? _MaghribEarly;
SMITrigger? _MaghribLate;
SMITrigger? _IsyakEarly;
SMITrigger? _IsyakLate;
SMITrigger? _solatPerfect;
SMITrigger? _solatImperfect;
Artboard? _riveArtboard;
String? message;
#override
void initState() {
super.initState();
// Load the animation file from the bundle, note that you could also
// download this. The RiveFile just expects a list of bytes.
rootBundle.load('android/assets/lotus.riv').then(
(data) async {
// Load the RiveFile from the binary data.
final file = RiveFile.import(data);
// The artboard is the root of the animation and gets drawn in the
// Rive widget.
final artboard = file.mainArtboard;
var controller =
StateMachineController.fromArtboard(artboard, 'State Machine ',onStateChange: _onStateChange);
if (controller != null) {
artboard.addController(controller);
_SubuhEarly = controller.findInput<bool>('Subuh Early') as SMITrigger;
_SubuhLate = controller.findInput<bool>('Subuh Late') as SMITrigger;
_ZohorEarly = controller.findInput<bool>('Zohor Early') as SMITrigger;
_ZohorLate = controller.findInput<bool>('Zohor late') as SMITrigger;
_AsarEarly = controller.findInput<bool>('Asar Early') as SMITrigger;
_AsarLate = controller.findInput<bool>('Asar Late') as SMITrigger;
_MaghribEarly = controller.findInput<bool>('Maghrib early') as SMITrigger;
_MaghribLate = controller.findInput<bool>('Maghrib late') as SMITrigger;
_IsyakEarly = controller.findInput<bool>('Ishak early') as SMITrigger;
_IsyakLate = controller.findInput<bool>('Ishak late') as SMITrigger;
_solatPerfect = controller.findInput<bool>('Solat Perfect') as SMITrigger;
_solatImperfect = controller.findInput<bool>('Solat Imperfect') as SMITrigger;
}
setState(() => _riveArtboard = artboard);
},
);
}
void _onStateChange(
String stateMachineName,
String stateName,
) =>
setState(
() => print('State Changed in $stateMachineName to $stateName') ,
);
void flowercheck(){
if (Provider.of<checkinlist>(context, listen: false).getSubuh() == 2){
_solatImperfect?.fire();
}
else if (Provider.of<checkinlist>(context, listen: false).getZohor() == 2){
_solatImperfect?.fire();
}
else if (Provider.of<checkinlist>(context, listen: false).getAsar() == 2){
_solatImperfect?.fire();
}
else if (Provider.of<checkinlist>(context, listen: false).getMaghrib() == 2){
_solatImperfect?.fire();
}
else if (Provider.of<checkinlist>(context, listen: false).getIsyak() == 2){
_solatImperfect?.fire();
}
else {
_solatPerfect?.fire();
}
}
void leafcheck(){
int subuh = Provider.of<checkinlist>(context, listen: false).getSubuh();
int zohor = Provider.of<checkinlist>(context, listen: false).getZohor();
int asar = Provider.of<checkinlist>(context, listen: false).getAsar();
int maghrib = Provider.of<checkinlist>(context, listen: false).getMaghrib();
int isyak = Provider.of<checkinlist>(context, listen: false).getIsyak();
double health = Provider.of<solatPoints>(context, listen: false).getHealth();
switch(subuh){
case 1:
_SubuhEarly?.fire();
break;
case 2:
_SubuhLate?.fire();
if (health > 5) {
Provider.of<solatPoints>(context, listen: false).decreasehealth();
}
break;
}
switch(zohor){
case 1:
_ZohorEarly?.fire();
break;
case 2:
_ZohorLate?.fire();
if (health > 5) {
Provider.of<solatPoints>(context, listen: false).decreasehealth();
}
break;
}
switch(asar){
case 1:
_AsarEarly?.fire();
break;
case 2:
_AsarLate?.fire();
if (health > 5) {
Provider.of<solatPoints>(context, listen: false).decreasehealth();
}
break;
}
switch(maghrib){
case 1:
_MaghribEarly?.fire();
break;
case 2:
_MaghribLate?.fire();
if (health > 5) {
Provider.of<solatPoints>(context, listen: false).decreasehealth();
}
break;
}
switch(isyak){
case 1:
_IsyakEarly?.fire();
break;
case 2:
_IsyakLate?.fire();
if (health > 5) {
Provider.of<solatPoints>(context, listen: false).decreasehealth();
}
break;
default:
break;
}
}
#override
Widget build(BuildContext context) {
setState(() {
leafcheck();
});
return Scaffold(
backgroundColor: Colors.blueGrey[900],
appBar: AppBar(
backgroundColor: Colors.deepPurple[900],
title: const Text('Bunga Solat'),
),
body: Center(
child: GestureDetector(
child: _riveArtboard == null
? const SizedBox()
: Stack(
children: [
Positioned.fill(
child: Rive(
artboard: _riveArtboard!,
),
),
]
),
),
)
);
}
}
class ClassProfile extends State<ClassProfile> with AutomaticKeepAliveClientMixin {
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => false;
#override
Widget build(BuildContext context) {
if(wantKeepAlive) {
super.build(context);
}
.
.
.
.
first set - with AutomaticKeepAliveClientMixin
then, override wantkeepalive variable
at last, defined super.build with if condition in your main widget builder function
In future if you want to persist state of your class then set wantKeepAlive => true
I have been writing a Sorting visualiser in flutter, I am so far able to animate the movement of blocks. But I also want to update the colours of the block, when the block goes through the states of being scanned, moved, and finally when it is completely sorted. I looked up the State management in flutter, and it is rather confusing to know what approach should I be using in my project. Below is the DashBoard Class:
import 'package:algolizer/sortingAlgorithms/Block.dart';
import 'package:flutter/material.dart';
import 'dart:math';
class DashBoard extends StatefulWidget {
double width;
double height;
DashBoard(#required this.width, #required this.height);
#override
_DashBoardState createState() => _DashBoardState();
}
class _DashBoardState extends State<DashBoard> {
double currentSliderValue = 50;
List<double> arr = new List(500);
List<Block> blockList;
bool running = false;
#override
void initState() {
// TODO: implement initState
super.initState();
fillArr((widget.width * 0.6) / 50, (widget.width * 0.1) / 50,
widget.height * 0.7);
}
void InsertionSort() async {
setState(() {
running = true;
});
int delay = (pow(15, 4) / pow(currentSliderValue, 2)).round();
for (int i = 1; i < currentSliderValue; i++) {
if (blockList[i] == null) break;
Block key = blockList[i];
int j = i - 1;
while (j >= 0 && blockList[j].height > key.height) {
setState(() {
blockList[j + 1] = blockList[j];
});
await Future.delayed(Duration(milliseconds: delay));
j--;
}
blockList[j + 1] = key;
}
setState(() {
running = false;
});
}
void BubbleSort() async {
setState(() {
running = true;
});
int delay = (pow(15, 4) / pow(currentSliderValue, 2)).round();
for (int i = 0; i < currentSliderValue - 1; i++) {
for (int j = 0; j < currentSliderValue - i - 1; j++) {
if (blockList[j].height > blockList[j + 1].height) {
Block temp = blockList[j + 1];
setState(() {
blockList[j + 1] = blockList[j];
blockList[j] = temp;
});
await Future.delayed(Duration(milliseconds: delay));
}
}
}
setState(() {
running = false;
});
}
// Map<String, >
void fillArr(double width, double margin, double height) {
for (int i = 0; i < arr.length; i++) arr[i] = null;
var rng = new Random();
for (int i = 0; i < currentSliderValue; i++) {
double val = rng.nextDouble() * height;
if (val == 0)
continue;
else
arr[i] = val;
}
blockList = [...arr.map((height) => Block(height, width, margin))];
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
SizedBox(height: 20),
Row(
children: [
Text(
"Length",
),
Slider(
value: currentSliderValue,
min: 5,
max: 200,
onChanged: (double value) {
setState(() {
currentSliderValue = value;
});
double newwidth =
(MediaQuery.of(context).size.width * 0.6) /
currentSliderValue;
double newmargin =
(MediaQuery.of(context).size.width * 0.1) /
currentSliderValue;
fillArr(newwidth, newmargin, widget.height * 0.7);
}),
RaisedButton(
child: Text("Insertion Sort"),
onPressed: InsertionSort,
),
RaisedButton(
onPressed: BubbleSort, child: Text("Bubble Sort")),
RaisedButton(onPressed: () {}, child: Text("Merge Sort")),
RaisedButton(onPressed: () {}, child: Text("Quick Sort")),
RaisedButton(
onPressed: () {}, child: Text("Counting Sort")),
RaisedButton(onPressed: () {}, child: Text("Radix Sort")),
RaisedButton(
onPressed: () {}, child: Text("Selection Sort")),
RaisedButton(onPressed: () {}, child: Text("Heap Sort")),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [...blockList],
),
// Row(
// children: [
// Container(
// child: Row(children: [
// Text("Algorithm")
// ],)
// )]
// ),
],
),
),
);
}
}
Here's the Block class:
import 'package:flutter/material.dart';
class Block extends StatefulWidget {
Block(#required this.height, #required this.width, #required this.mar);
double height;
double width;
double mar;
#override
_BlockState createState() => _BlockState();
}
class _BlockState extends State<Block> {
Color col = Colors.blue;
// void isKey() {
// setState(() {
// col = Colors.pink;
// });
// }
// void notKey() {
// setState(() {
// col = Colors.purple;
// });
// }
#override
Widget build(BuildContext context) {
return (widget.height == null)
? Container()
: Container(
height: this.widget.height,
width: widget.width,
margin: EdgeInsets.all(widget.mar),
decoration: BoxDecoration(
color: col,
),
);
}
}
As far as which state management route to go with, it really can be done with any of them. GetX to me is the easiest and has the least boilerplate.
Here's one way to do this. I just updated the insertionSort method to get you started and you can go from there. Any other changes you notice in your other classes are just to get rid of linter errors.
All your methods and variables can now live in a GetX class. With the exception of color, the rest are now observable streams.
class BlockController extends GetxController {
RxDouble currentSliderValue = 50.0.obs; // adding .obs makes variable obserable
RxList arr = List(500).obs;
RxList blockList = [].obs;
RxBool running = false.obs;
Color color = Colors.red;
void insertionSort() async {
running.value = true; // adding .value access the value of observable variable
color = Colors.blue;
int delay = (pow(15, 4) / pow(currentSliderValue.value, 2)).round();
for (int i = 1; i < currentSliderValue.value; i++) {
if (blockList[i] == null) break;
Block key = blockList[i];
int j = i - 1;
while (j >= 0 && blockList[j].height > key.height) {
blockList[j + 1] = blockList[j];
await Future.delayed(Duration(milliseconds: delay));
j--;
}
blockList[j + 1] = key;
}
color = Colors.green;
update(); // only needed for the color property because its not an observable stream
running.value = false;
}
void bubbleSort() async {
running.value = true;
int delay = (pow(15, 4) / pow(currentSliderValue.value, 2)).round();
for (int i = 0; i < currentSliderValue.value - 1; i++) {
for (int j = 0; j < currentSliderValue.value - i - 1; j++) {
if (blockList[j].height > blockList[j + 1].height) {
Block temp = blockList[j + 1];
blockList[j + 1] = blockList[j];
blockList[j] = temp;
await Future.delayed(Duration(milliseconds: delay));
}
}
}
running.value = false;
}
// Map<String, >
void fillArr(double width, double margin, double height) {
for (int i = 0; i < arr.length; i++) arr[i] = null;
var rng = new Random();
for (int i = 0; i < currentSliderValue.value; i++) {
double val = rng.nextDouble() * height;
if (val == 0)
continue;
else
arr[i] = val;
}
blockList = [...arr.map((height) => Block(height, width, margin))].obs;
}
}
Initialize the controller in your main before running your app. Generally it can be done anywhere as long as its before you try to access the controller.
Get.put(BlockController());
Here's your much less busy DashBoard now that all that logic is tucked away in a GetX class. Here we find the controller, and use it access all those variables and methods.
Obx is the GetX widget that rebuilds on changes.
class DashBoard extends StatefulWidget {
final double width;
final double height;
DashBoard(this.width, this.height);
#override
_DashBoardState createState() => _DashBoardState();
}
class _DashBoardState extends State<DashBoard> {
final controller = Get.find<BlockController>(); // finding same instance of BlockConroller that you initialized in `Main`
#override
void initState() {
super.initState();
controller.fillArr((widget.width * 0.6) / 50, (widget.width * 0.1) / 50,
widget.height * 0.7);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
SizedBox(height: 50),
Obx(
// rebuilds when observable variables change
() => Column(
// changed to Column because a Row was overflowing
children: [
Text(
"Length",
),
Slider(
value: controller.currentSliderValue.value,
min: 5,
max: 200,
onChanged: (double value) {
controller.currentSliderValue.value = value;
double newwidth =
(MediaQuery.of(context).size.width * 0.6) /
controller.currentSliderValue.value;
double newmargin =
(MediaQuery.of(context).size.width * 0.1) /
controller.currentSliderValue.value;
controller.fillArr(
newwidth, newmargin, widget.height * 0.7);
}),
RaisedButton(
child: Text("Insertion Sort"),
onPressed: controller.insertionSort,
),
RaisedButton(
onPressed: controller.bubbleSort,
child: Text("Bubble Sort")),
RaisedButton(onPressed: () {}, child: Text("Merge Sort")),
RaisedButton(onPressed: () {}, child: Text("Quick Sort")),
RaisedButton(onPressed: () {}, child: Text("Counting Sort")),
RaisedButton(onPressed: () {}, child: Text("Radix Sort")),
RaisedButton(onPressed: () {}, child: Text("Selection Sort")),
RaisedButton(onPressed: () {}, child: Text("Heap Sort")),
],
),
),
Obx(
// rebuilds when observable variables change
() => Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [...controller.blockList],
),
),
// Row(
// children: [
// Container(
// child: Row(children: [
// Text("Algorithm")
// ],)
// )]
// ),
],
),
),
);
}
}
And here's your Block which can now be stateless. Key thing of note here is the GetBuilder widget that updates the color.
class Block extends StatelessWidget {
// now can be stateless
Block(this.height, this.width, this.mar);
final double height;
final double width;
final double mar;
#override
Widget build(BuildContext context) {
return (height == null)
? Container()
: GetBuilder<BlockController>(
// triggers rebuilds when update() is called from GetX class
builder: (controller) => Container(
height: this.height,
width: width,
margin: EdgeInsets.all(mar),
decoration: BoxDecoration(
color: controller.color,
),
),
);
}
}
I want to create condition that if first api didn't finish load when click on bottom navigation tab it will not navigate to other tab. So I want to pass boolean that will change to true when api is loaded, the problem is I don't know how to pass this dynamic value to my custom Bottom Navigation Bar. Is there an other way instead of using global variable?
This is my code.
landingPage.dart
// This global variable is change to true when receive callback from api
bool isComplete = false;
class _NavigationItem {
_NavigationItem(this.iconFile, this.caption, this.page);
final String iconFile;
final String caption;
final Widget page;
}
class LandingPage extends StatefulWidget {
final String username;
LandingPage({Key key, this.username}) : super(key: key);
#override
_LandingPageState createState() => _LandingPageState();
}
class _LandingPageState extends State<LandingPage> {
int _selectedIndex = 0;
List<_NavigationItem> _navItems = [];
double _bottomNavBarHeight = 46;
double _iconSize = 26;
double _circleSize = 52;
int _animationDuration = 300;
double _circleStrokeWidth = 0;
BottomNavigationController _navigationController;
#override
void didChangeDependencies() {
// Context of a state is available to us from the moment the State loads its dependencies
// Since we need context object so it need to be accessed inside this overridden method.
_navItems = [
_NavigationItem(
"assets/icons/home.svg",
S.of(context).landing_nav_home,
HomePage(
username: widget.username,
callback: (value) { isComplete = value; }, // receiving callback data
)),
_NavigationItem("assets/icons/friend.svg",
S.of(context).landing_nav_friend, FriendPage()),
_NavigationItem(
"assets/icons/chat.svg", S.of(context).landing_nav_chat, ChatPage()),
_NavigationItem("assets/icons/widget.svg",
S.of(context).landing_nav_widget, WidgetPage()),
_NavigationItem("assets/icons/more.svg",
S.of(context).landing_nav_setting, SettingPage()),
];
super.didChangeDependencies();
}
#override
void initState() {
super.initState();
_navigationController = new BottomNavigationController(_selectedIndex);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Container(
padding: EdgeInsets.only(bottom: 90),
child: _navItems.elementAt(_selectedIndex).page,
),
_createBottomNavigationBar()
],
)
);
}
void _onNavigationBarItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
Widget _createBottomNavigationBar() {
return Align(alignment: Alignment.bottomCenter, child: _bottomNav());
}
Widget _bottomNav() {
List<TabItem> tabItems = List.of([
new TabItem(Icons.home_outlined, "", CommonColor.accent,
asset: "assets/icons/home.svg"),
new TabItem(Icons.person, "", CommonColor.accent,
asset: "assets/icons/friends.svg"),
new TabItem(Icons.chat_bubble_outline, "", CommonColor.accent),
new TabItem(Icons.widgets_outlined, "", CommonColor.accent),
new TabItem(Icons.more_horiz, "", CommonColor.accent),
]);
return BottomNavigation(
tabItems,
controller: _navigationController,
barHeight: _bottomNavBarHeight,
iconsSize: _iconSize,
circleSize: _circleSize,
selectedIconColor: Colors.white,
normalIconColor: CommonColor.accent,
circleStrokeWidth: _circleStrokeWidth,
barBackgroundColor: Colors.white,
animationDuration: Duration(milliseconds: _animationDuration),
selectedCallback: (int selectedPos) {
print("selected: $_selectedIndex");
_onNavigationBarItemTapped(selectedPos);
},
);
}
#override
void dispose() {
super.dispose();
_navigationController.dispose();
}
}
BottomNavigation.dart
typedef BottomNavSelectedCallback = Function(int selectedPos);
class BottomNavigation extends StatefulWidget {
final List<TabItem> tabItems;
final int selectedPos;
final double barHeight;
final double padding;
final Color barBackgroundColor;
final double circleSize;
final double circleStrokeWidth;
final double iconsSize;
final Color selectedIconColor;
final Color normalIconColor;
final Duration animationDuration;
final BottomNavSelectedCallback selectedCallback;
final BottomNavigationController controller;
BottomNavigation(this.tabItems,
{this.selectedPos = 0,
this.barHeight = 60,
this.barBackgroundColor = Colors.white,
this.circleSize = 58,
this.circleStrokeWidth = 4,
this.iconsSize = 32,
this.padding = 16,
this.selectedIconColor = Colors.white,
this.normalIconColor = Colors.deepPurpleAccent,
this.animationDuration = const Duration(milliseconds: 300),
this.selectedCallback,
this.controller})
: assert(tabItems != null && tabItems.length >= 2 && tabItems.length <= 5,
"tabItems is required");
#override
State<StatefulWidget> createState() => _BottomNavigationState();
}
class _BottomNavigationState extends State<BottomNavigation>
with TickerProviderStateMixin {
Curve _animationsCurve = Cubic(0.27, 1.21, .77, 1.09);
AnimationController itemsController;
Animation<double> selectedPosAnimation;
Animation<double> itemsAnimation;
List<double> _itemsSelectedState;
int selectedPos;
int previousSelectedPos;
BottomNavigationController _controller;
#override
void initState() {
super.initState();
if (widget.controller != null) {
_controller = widget.controller;
previousSelectedPos = selectedPos = _controller.value;
} else {
previousSelectedPos = selectedPos = widget.selectedPos;
_controller = BottomNavigationController(selectedPos);
}
_controller.addListener(_newSelectedPosNotify);
_itemsSelectedState = List.generate(widget.tabItems.length, (index) {
return selectedPos == index ? 1.0 : 0.0;
});
itemsController = new AnimationController(
vsync: this, duration: widget.animationDuration);
itemsController.addListener(() {
setState(() {
_itemsSelectedState.asMap().forEach((i, value) {
if (i == previousSelectedPos) {
_itemsSelectedState[previousSelectedPos] =
1.0 - itemsAnimation.value;
} else if (i == selectedPos) {
_itemsSelectedState[selectedPos] = itemsAnimation.value;
} else {
_itemsSelectedState[i] = 0.0;
}
});
});
});
selectedPosAnimation = makeSelectedPosAnimation(
selectedPos.toDouble(), selectedPos.toDouble());
itemsAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: itemsController, curve: _animationsCurve));
}
Animation<double> makeSelectedPosAnimation(double begin, double end) {
return Tween(begin: begin, end: end).animate(
CurvedAnimation(parent: itemsController, curve: _animationsCurve));
}
void onSelectedPosAnimate() {
setState(() {});
}
void _newSelectedPosNotify() {
_setSelectedPos(widget.controller.value);
}
#override
Widget build(BuildContext context) {
double fullWidth = MediaQuery.of(context).size.width;
double fullHeight =
widget.barHeight + (widget.circleSize / 2) + widget.circleStrokeWidth;
double sectionsWidth = (fullWidth / 1.2) / widget.tabItems.length;
//Create the boxes Rect
List<Rect> boxes = List();
widget.tabItems.asMap().forEach((i, tabItem) {
double left = (i + 0.5) * sectionsWidth;
double top = fullHeight - widget.barHeight;
double right = left + sectionsWidth;
double bottom = fullHeight;
boxes.add(Rect.fromLTRB(left, top, right, bottom));
});
List<Widget> children = List();
// This is the full view transparent background (have free space for circle)
children.add(Padding(
padding: EdgeInsets.only(bottom: widget.padding),
child: Container(
width: fullWidth,
height: fullHeight,
)));
// This is the bar background (bottom section of our view)
children.add(Positioned.fill(
child: Padding(
padding: EdgeInsets.only(
left: widget.padding,
right: widget.padding,
bottom: widget.padding),
child: Container(
width: MediaQuery.of(context).size.width,
height: widget.barHeight,
decoration: BoxDecoration(
color: widget.barBackgroundColor,
borderRadius: BorderRadius.all(Radius.circular(20)),
boxShadow: [
new BoxShadow(color: Colors.black12, blurRadius: 8.0)
]),
),
),
top: fullHeight - widget.barHeight,
));
// This is the circle handle on selected
children.add(new Positioned(
child: Container(
width: widget.circleSize,
height: widget.circleSize,
child: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(14)),
color: widget.barBackgroundColor),
),
Container(
margin: EdgeInsets.all(widget.circleStrokeWidth),
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(14)),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
CommonColor.primary,
widget.tabItems[selectedPos].circleColor,
CommonColor.slipBg,
])),
),
],
),
),
left: boxes[selectedPos].center.dx - (widget.circleSize / 2),
top: 0,
));
//Here are the Icons and texts of items
boxes.asMap().forEach((int pos, Rect r) {
// Icon
Color iconColor = pos == selectedPos
? widget.selectedIconColor
: widget.normalIconColor;
double scaleFactor = pos == selectedPos ? 1.2 : 1.0;
children.add(
Positioned(
child: Transform.scale(
scale: scaleFactor,
child: widget.tabItems[pos].asset != null
? SvgPicture.asset(widget.tabItems[pos].asset,
color: iconColor,
width: widget.iconsSize,
height: widget.iconsSize,
fit: BoxFit.cover)
: Icon(
widget.tabItems[pos].icon,
size: widget.iconsSize,
color: iconColor,
),
),
left: r.center.dx - (widget.iconsSize / 2),
top: r.center.dy -
(widget.iconsSize / 2) -
(_itemsSelectedState[pos] *
((widget.barHeight / 2) + widget.circleStrokeWidth)),
),
);
if (pos != selectedPos) {
children.add(Positioned.fromRect(
child: GestureDetector(
onTap: () {
_controller.value = pos;
},
),
rect: r,
));
}
});
return Stack(
children: children,
);
}
void _setSelectedPos(int pos) {
previousSelectedPos = selectedPos;
selectedPos = pos;
itemsController.forward(from: 0.0);
selectedPosAnimation = makeSelectedPosAnimation(
previousSelectedPos.toDouble(), selectedPos.toDouble());
selectedPosAnimation.addListener(onSelectedPosAnimate);
if (widget.selectedCallback != null) {
widget.selectedCallback(selectedPos);
}
}
#override
void dispose() {
super.dispose();
itemsController.dispose();
_controller.removeListener(_newSelectedPosNotify);
}
}
class BottomNavigationController extends ValueNotifier<int> {
BottomNavigationController(int value) : super(value);
}
You should use a FutureBuilder/ StreamBuilder to listen to an event after your api call is finished. Something like this:
Future<dynamic> yourApiCall() async {
// Execute your functions
return someValue;
}
Here in the UI, you can listen to the output of this function with:
FutureBuilder(
future: yourApiCall(),
builder: (context, snapshot) {
return BottomNavigation(
tabItems,
// ...other properties
selectedCallback: (int selectedPos) {
print("selected: $_selectedIndex");
if (snapshot.hasData) // Here you check if the snapshot.data is different from null. That means your api has finished and returned some value
_onNavigationBarItemTapped(selectedPos); // Only then, you notify the callback function
},
);
});
I am trying to make a scrollable ListView to the next element so that it is always at the beginning (or center) of the page (as in PageView)
My problem is inertia, the backward movement of an element after scrolling.
How can I implements item's behavior without inertia?
Code:
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: 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> {
List<ScrollController> _horizontalControllers;
ScrollController _verticalController;
var _itemCountHorizontal = 15;
bool _inProgress;
Orientation get isPortrait => MediaQuery.of(context).orientation;
double get _height => MediaQuery.of(context).size.height;
double get _width => MediaQuery.of(context).size.width;
double get horizontalPadding {
double _padd;
if (isPortrait == Orientation.portrait) {
_padd = _width * 0.01;
} else {
_padd = _width * 0.01;
}
return _padd;
}
double get verticalPadding {
double _padd;
if (isPortrait == Orientation.portrait) {
_padd = cardHeight * 0.005;
} else {
_padd = (_height - cardHeight) / 2;
}
return _padd;
}
double get cardHeight {
double cardH;
if (isPortrait == Orientation.portrait) {
cardH = cardWidth * 1.7;
} else {
cardH = _height * 0.9;
}
return cardH;
}
double get cardWidth {
var cardW = _width * 0.99;
if (cardW > _height / 1.7) {
cardW = _height / 1.77;
}
return cardW;
}
#override
void initState() {
_horizontalControllers = [
ScrollController(),
ScrollController(),
ScrollController(),
ScrollController(),
ScrollController(),
];
_verticalController = ScrollController();
_inProgress = false;
super.initState();
}
#override
void dispose() {
_horizontalControllers.forEach((element) {
element.dispose();
});
_verticalController.dispose();
super.dispose();
}
void _onEndScrollVertical(ScrollMetrics metrics) {
print("scroll before = ${metrics.extentBefore}");
print("scroll after = ${metrics.extentAfter}");
print("scroll inside = ${metrics.extentInside}");
print("index = ${metrics.axisDirection}");
print("item HEIGHT => $cardHeight");
final topPadd = MediaQuery.of(context).padding.top;
print('TOPPPPPPPPPP $topPadd');
/* int point = metrics.extentAfter ~/ (_height - topPadd);
var offset = (_height - topPadd) * point;
_inProgress = true;
Future.delayed(Duration(milliseconds: 100), () {
_verticalController.animateTo(offset,
duration: Duration(milliseconds: 1000), curve: Curves.fastOutSlowIn);
});
_inProgress = false;
*/
var halfOfTheHeight = cardHeight / 2;
var offsetOfItem = metrics.extentBefore % cardHeight;
if (offsetOfItem < halfOfTheHeight) {
final offset = metrics.extentBefore - offsetOfItem;
print("offsetOfItem1 = $offsetOfItem offset = $offset");
Future.delayed(Duration(milliseconds: 50), () {
_verticalController.animateTo(offset,
duration: Duration(milliseconds: 1000),
curve: Curves.fastOutSlowIn);
});
} else if (offsetOfItem > halfOfTheHeight) {
final offset = metrics.extentBefore + offsetOfItem;
print("offsetOfItem2 = $offsetOfItem offset = $offset");
Future.delayed(Duration(milliseconds: 50), () {
_verticalController.animateTo(offset,
duration: Duration(milliseconds: 1000),
curve: Curves.fastOutSlowIn);
});
}
}
void _onEndScrollHorizontal(ScrollMetrics metrics, int index) {
print("scroll before = ${metrics.extentBefore}");
print("scroll after = ${metrics.extentAfter}");
print("scroll inside = ${metrics.extentInside}");
print("item WIDTH => $cardWidth");
var halfOfTheWidth = _width / 2;
var offsetOfItem = metrics.extentBefore % _width;
if (offsetOfItem < halfOfTheWidth) {
final offset = metrics.extentBefore - offsetOfItem;
print("offsetOfItem1 = $offsetOfItem offset = $offset");
_inProgress = true;
Future.delayed(Duration(milliseconds: 10), () {
_horizontalControllers[index].animateTo(offset,
duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
});
_inProgress = false;
} else if (offsetOfItem > halfOfTheWidth) {
_inProgress = true;
final offset = metrics.extentBefore + offsetOfItem;
print("offsetOfItem2 = $offsetOfItem offset = $offset");
Future.delayed(Duration(milliseconds: 10), () {
_horizontalControllers[index].animateTo(offset,
duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
});
_inProgress = false;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification &&
scrollNotification.depth == 0) {
if (!_inProgress) {
print('ScrollEndNotification ===> $scrollNotification');
_onEndScrollVertical(scrollNotification.metrics);
}
}
return null;
},
child: buildListViewVertical(),
),
),
);
}
Widget buildListViewVertical() {
return ListView.builder(
itemCount: _itemCountHorizontal,
itemExtent: cardHeight,
controller: _verticalController,
itemBuilder: (BuildContext context, int index) {
return NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification &&
scrollNotification.depth == 0) {
print('ScrollEndNotification ===> $scrollNotification');
_onEndScrollHorizontal(scrollNotification.metrics, index);
}
return null;
},
child: buildListViewHorizontal(index));
},
);
}
Widget buildListViewHorizontal(int index) {
return ListView.builder(
controller: _horizontalControllers[index],
physics: ClampingScrollPhysics(),
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: _itemCountHorizontal + 1,
itemBuilder: (BuildContext context, int index) =>
index < _itemCountHorizontal
? Padding(
padding: EdgeInsets.only(
left: horizontalPadding,
right: horizontalPadding,
top: verticalPadding,
bottom: verticalPadding,
),
child: Container(height: 340, width: 200, color: Colors.red),
)
: SizedBox(
width: 50,
),
);
}
}
Here is a working example for DartPad
Update:
I add CustomScrollPhysics() to ListView, and that solution removed the inertia on reverse motions. Howewer, inertia persisted when moving from index 0 and above...
Code:
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
class CustomScrollPhysics extends ScrollPhysics {
const CustomScrollPhysics({ScrollPhysics parent}) : super(parent: parent);
#override
SpringDescription get spring => SpringDescription(damping: 0.1);
#override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(parent: buildParent(ancestor));
}
}
you can use DraggableScrollableSheet and SingleChildScrollView Widgets
first you need to change you widget into statefullwidget then you need to use Global key to scroll to the position that you want. and the position that you want to scroll to "widget ex." must have a name .
I have a need recently,I need to measure the distance between the child elements in the sliver and the top, but always prompt that findrendereobject is empty。
I can't even try widgetsbinding.instance.addpostframecallback
Console error:
════════ Exception caught by scheduler library
═════════════════════════════════════════════════════
The following
NoSuchMethodError was thrown during a scheduler callback: The method
'findRenderObject' was called on null. Receiver: null Tried calling:
findRenderObject()
Please Everybody look what happened,why can't measuring height~thank you very much!!!
My Code:
class GoodsDetailPage extends StatefulWidget {
final IndexVOS bean;
GoodsDetailPage(this.bean);
#override
_GoodsDetailPageState createState() => _GoodsDetailPageState();
}
class _GoodsDetailPageState extends State<GoodsDetailPage> with SingleTickerProviderStateMixin{
int productId;
int areaId;
String serviceTime;
ProductInfoBean productInfoBean;
String _selectName;
Norms norms;
double screenWidth;
bool _isShow = false;
TabController _tabController;
List<String> tabList;
ScrollController _scrollController = ScrollController();
var globalKeyOne = GlobalKey();
var globalKeyTwo = GlobalKey();
var globalKeyThree = GlobalKey();
var oneY = 0.0;
var twoY = 0.0;
var threeY = 0.0;
ProductModel model = ProductModel();
GoodsSpecModel _specModel = GoodsSpecModel();
#override
void initState() {
productId = widget.bean.productId;
areaId = widget.bean.areaId;
serviceTime = widget.bean.serviceTimeBegin;
tabList = [
"商品",
"评价",
"详情",
];
_tabController = TabController(
length: tabList.length,
vsync: this,
);
_scrollController.addListener(() {
var of = _scrollController.offset;
setState(() {
_isShow = of >= screenWidth-50;
});
if (of > threeY - oneY) {
_tabController.animateTo(2);
}else if (of > twoY - oneY) {
_tabController.animateTo(1);
} else {
_tabController.animateTo(0);
}
print("滚动了$of one=${twoY - oneY}=two=${threeY - oneY}");
});
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
//等待界面渲染完成
void _afterLayout(_){
oneY = getY(globalKeyOne.currentContext);
twoY = getY(globalKeyTwo.currentContext);
threeY = getY(globalKeyThree.currentContext);
}
static double getY(BuildContext buildContext) {
final RenderBox box = buildContext.findRenderObject();
//final size = box.size;
final topLeftPosition = box.localToGlobal(Offset.zero);
return topLeftPosition.dy;
}
Future _request() async{
model.requestGoodsDetail(productId: productId,areaId: areaId,serviceTime: serviceTime);
}
#override
Widget build(BuildContext context) {
screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
child: ProviderWidget<ProductModel>(
model: model,
onModelReady: (model) => model.requestGoodsDetail(productId: productId,areaId: areaId,serviceTime: serviceTime),
builder: (context, model, child) {
if (model.busy) {
return ViewStateBusyWidget();
}
if (model.error) {
return ViewStateErrorWidget(error: model.viewStateError, onPressed: _request);
}
return Column(
children: <Widget>[
Expanded(child: _body()),
_bottom()
],
);
}
),
)
);
}
Widget _body(){
var _normalBack = Image.asset(ImagePath.normalBack,height: dp40,width: dp40);
var _detailBack = Image.asset(ImagePath.detailBack,height: dp60,width: dp60);
var _normalShare = Image.asset(ImagePath.detailShareSmall,height: dp40,width: dp60);
var _detailShare = Image.asset(ImagePath.detailShare,height: dp60,width: dp140);
var _normalDot = Image.asset(ImagePath.threeDot,height: dp40,width: dp40);
var _detailDot = Image.asset(ImagePath.detailDot,height: dp60,width: dp60);
return CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
key: globalKeyOne,
title: _isShow?_topTabTitle():Container(),
centerTitle: true,
expandedHeight: screenWidth,
floating: false,
pinned: true,
snap: false,
elevation: 0.5,
leading: IconButton(
icon: _isShow?_normalBack:_detailBack,
onPressed: () => pop(),
),
actions: <Widget>[
GestureDetector(
child: _isShow?_normalShare:_detailShare,
onTap: (){
print("分享");
},
),
SizedBox(width: dp24),
GestureDetector(
child: _isShow?_normalDot:_detailDot,
onTap: _showPopup,
),
SizedBox(width: dp30),
],
flexibleSpace: FlexibleSpaceBar(
background: GoodsDetailBannerWidget(model),
),
),
SliverList(
delegate: SliverChildListDelegate([
_activityImage(),
GoodsDetailInfoWidget(model),
gap20,
GoodsDetailOtherWidget(model,serviceTime),
GoodsDetailCategoryWidget(click: _clickSpec,name: _selectName),
gap20,
Column(
key: globalKeyTwo,
children: <Widget>[
GoodsDetailCommentWidget(model),
gap20,
],
),
Container(
key: globalKeyThree,
child: GoodsDetailDescWidget(model),
)
]),
),
],
);
}
Widget _bottom(){
return GoodsDetailBottomWidget(
clickBuy: (){
if(_selectName == null){
_clickSpec();
return;
}
routePush(ConfirmOrderPage(productInfoBean: model.productInfoBean,norms: norms,count: _specModel.countNumber));
},
);
}
Widget _activityImage(){
return Container(
width: double.infinity,
height: dp80,
child: Image.asset(ImagePath.goodsActivity),
);
}
Widget _topTabTitle(){
return Container(
height: dp60,
child: TabBar(
controller: _tabController,
labelColor: ColorConfig.themeGreen,
unselectedLabelColor: ColorConfig.C09,
labelStyle: StyleConfig.green_36Style,
unselectedLabelStyle: StyleConfig.normalTitle36Style,
indicatorColor: ColorConfig.themeGreen,
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 2,
isScrollable: true,
labelPadding: EdgeInsets.symmetric(horizontal: dp20),
tabs: tabList.map((item) {
return Tab(
text: item,
);
}).toList(),
onTap: (index){
switch(index){
case 0:
_scrollController.jumpTo(0);
_tabController.animateTo(0);
break;
case 1:
_scrollController.jumpTo(twoY - oneY);
_tabController.animateTo(1);
break;
case 2:
_scrollController.jumpTo(threeY - oneY);
_tabController.animateTo(2);
break;
}
},
),
);
}
void _showPopup(){
showPopupWindow(
context,
gravity: KumiPopupGravity.rightTop,
underStatusBar: true,
underAppBar: true,
offsetX: -dp30,
offsetY: -dp20,
childFun: (pop){
return Container(
key: GlobalKey(),
child: GoodsDetailPopupMenuWidget(),
);
}
);
}
void _clickSpec(){
_specModel.initData(model.normInfoBean.norms);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return GoodsSpecSelectDialog(
model: _specModel,
onSelected: (bean, count) {
norms = bean;
setState(() {
_selectName = "已选:"+bean.normName + ",数量:" + count.toString();
});
},
);
},
);
}
#override
void dispose() {
_specModel.dispose();
_scrollController.dispose();
_tabController.dispose();
super.dispose();
}
}
Your global keys are attached to the widgets in _body() method but this method is just called when model.busy = false and model.error = false. It means globalKeyOne.currentContext will be null when model.busy = true || model.error = true. Your _afterLayout(...) is called in all the cases, that's why it fails with NPE.
use widgetsbinding.instance.addpostframecallback
if you put the method getPosition in the widgetsbinding..... then remember to add this in the parameter of getPosition(_)
for some reason this works
Here is the Sample Code
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
GlobalKey _akey = GlobalKey();
var top;
var left;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: 200,
width: 300,
key: _akey,
color: Colors.blue,
),
),
);
}
_afterLayout(_) {
_getPosition();
}
_getPosition() async {
final RenderBox rb = _akey.currentContext.findRenderObject();
var position = rb.localToGlobal(Offset.zero);
top = position.dy;
left = position.dx;
print('$left , $top');
}
}
try putting it in build Function not initState()