Notes: 10. Animate the Item Switcher's Selector
The student materials have been reviewed and are updated as of August 2022.
The updated material uses null safety and also deploys the use coding practises encouraged by the Flutter as per the Flutter lint rules and a few addons of practises that are considered good here at raywenderlich (see analysis_options.yaml in the material for more info).
The next and stage in our animation sequence is to animate the position of the bottom selector widget. We want to animate horizontal position to the center of the selected item.
Let’s head over to where it is defined in our code base. You can see it is a child of a Stack widget and we used the Positioned widget to place it at its current location. We need a way to access the indivual button’s position and offset this selector’s left position to the center of that button.
Now, how does Flutter identify widgets in its tree? Yup, you got it right, it uses Keys.
This time around we would be using GlobalKeys.
- Aside from having the feature of being unique across the entire app, they also provide access to other objects that are associted with the widget in which it indentifies.
- With a GlobalKey, we can have access to stuffs like the BuildContext of the widget.
And the BuildContext is just an object that contains information about the location of widget in the widget tree. With this we can get other information about the widget and thats one of the main reason we’re using global keys here.
Back in VSCode, go ahead and update your code to the following:
...
GlobalKey _itemKey1 = GlobalKey();
GlobalKey _itemKey2 = GlobalKey();
GlobalKey _itemKey3 = GlobalKey();
...
void _selectIem(int id){
...
late GlobalKey globalKey;
switch (id) {
case 1:
globalKey = _itemKey1;
break;
case 2:
globalKey = _itemKey2;
break;
case 3:
globalKey = _itemKey3;
break;
default:
}
...
}
// Insdide Row of build method
InkWell(
key: _itemKey1,
...
InkWell(
key: _itemKey2,
...
InkWell(
key: _itemKey3,
...
We created 3 globalkey variables for the 3 buttons in our switcher. Then inside the _selectItem method, we create another globalkey. This would be used to get the key for the current selected item. And thats what we do in the next statement. We simply use a switch case statement to assign the selectedGlobalKey to the button that was clicked.
After that, we assigned the InkWell widgets the itemkeys we just created above. With this, we can be able to get the current context of the selected button. This doesnt do much for now. We need to use the current selected key to get the position information of the widget. Update your code to the following:
double _selectorPositionX = 24;
final double _selectorWidth = 30;
...
void _selectItem(int id) {
...
// Remove the setState() called here
_setWidgetPositionX(selectedGlobalKey);
}
...
// Paste this
_setWidgetPositionX(GlobalKey key) {
final widgetRenderBox =
key.currentContext?.findRenderObject() as RenderBox?;
if (widgetRenderBox != null) {
final widgetPosition = widgetRenderBox.localToGlobal(Offset.zero);
final widgetSize = widgetRenderBox.size;
_selectorPositionX =
widgetPosition.dx - ((_selectorWidth - widgetSize.width) / 2);
}
setState(() {});
}
...
// Inside the build method
AnimatedPositioned(
duration: const Duration(milliseconds: 450),
curve: Curves.fastOutSlowIn,
left: _selectorPositionX,
...
child: Container(
...
width: _selectorWidth,
...
In here, we created two variables. The first one is used to store the horizontal position of the selector widget and would be updated whenever we click on a new item. While the second variable holds the selector’s width and would be used to compute its position.
Then inside the _selectItem() method, we call a new method: _setWidgetPositionX() and passed the selectedGlobalKey to it.
This method is responsible for digging into to the selected button’s buildcontext to get some information about it.
Let’s break it down.
First, we get the renderbox of the button which is just the representation of what Flutter renders on our screen. So the findRenderObject() method would only return valid result after the widget has been laid out on our screen. Then we use the methods of the renderbox to get its position and size.
Then we use these values to compute the horizonatal position of the selector. Finally, we call setState() to rebuild the widget.
After that, we use the _selectorPositionX variable inside AnimatedPositioned widget as its left offset. This would move the widget whenever this value changes. Then finally, we set the width of the COntainer to the final variable we created earlier.
And if you notice, we changed the Positioned widget to its implicit animated version which is the AnimatedPositioned. Save your work and try it out. Nice, the selector’s position animates nicely.
We still have one last thing to do. It you look at the _selectorPositionX variable, we set it to 24 by default. This value is not dynamic and an x position of 24 would vary on different devices and this would produce an undesired effect. We need to way to set the position to the correct value once we can be able to get the first item’s position. Add the following code:
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_setWidgetPositionX(_itemKey1);
});
}
We call the addPostFrameCallback iniside the initState method. It takes a callback function that would be called immediately after the build method is done. In the callback function, we call the _setWidgetPositionX() and passed in the itemkey of the first button. With this setup, the selector would animate itself to the correct location immediately this widget’s build is complete.
Save your work. Do a hot restart. You can see that the selector animated to its correct position as soon as the widget is rendered.