A Deep Dive Into PageView In Flutter (With Custom Transitions)
Exploring the PageView widget and creating custom page transitions
This article is the seventh in the series of articles which take an in-depth look at Flutter’s built-in widgets.
- ListView/ScrollPhysics
- TextFields
- FloatingActionButtons
- Hero Widget
- Transform Widget
- Draggable/DragTarget
In this article, we will take a look at PageView
then later on, make a few custom effects for it.
NOTE: The
ListView
Deep Dive is a precursor to this article. The elements covered in that article will not be repeated since they are almost the same. You can read my ListView article here
Exploring PageViews
A PageView
is a widget which generates scrollable pages on the screen. This can either be a fixed list of pages or a builder function which builds repeating pages. PageView
acts similar to a ListView
in the sense of constructing elements.
The types of PageView are:
- PageView
- PageView.builder
- PageView.custom
PageView (Default constructor)
This type takes a fixed list of children (pages) and makes them scrollable.
PageView(
children: <Widget>[
Container(
color: Colors.pink,
),
Container(
color: Colors.cyan,
),
Container(
color: Colors.deepPurple,
),
],
)
The above code produces the following result:
PageView.builder
This constructor takes a itemBuilder
function and an itemCount
similar to ListView.builder
PageView.builder(
itemBuilder: (context, position) {
return _buildPage();
},
itemCount: listItemCount, // Can be null
)
Like a ListView.builder
, this builds children on demand.
If the itemCount
is set to null (not set), an infinite list of pages can be generated.
For example, this code:
PageView.builder(
itemBuilder: (context, position) {
return Container(
color: position % 2 == 0 ? Colors.pink : Colors.cyan,
);
},
)
Gives an infinite list of pages with alternating pink and cyan colors:
Note:
PageView.custom
works the same way asListView.custom
(Discussed in earlier Deep Dive) and we will not be discussing it here.
Orientation
All types of Page Views can have horizontal or vertically scrolling pages.
PageView(
children: <Widget>[
// Add children here
],
scrollDirection: Axis.vertical,
)
The above code gives us:
PageSnapping
Page snapping allows us to keep the page at intermediate values. This is done by switching off the pageSnapping
attribute. In this case the page will not scroll to an integer position and behave like a normal ListView
.
PageView(
children: <Widget>[
// Add children here
],
pageSnapping: false,
)
ScrollPhysics
A PageView
can have custom scrolling behavior in the same way as ListViews. We will not repeat different types of ScrollPhysics since it is discussed in the ListView Deep Dive.
ScrollPhysics
can be changed using the physics parameter:
PageView(
children: <Widget>[
// Add children here
],
physics: BouncingScrollPhysics(),
)
Controlling a PageView
A PageView can be programmatically controlled by attaching a PageController
.
// Outside build method
PageController controller = PageController();// Inside build method
PageView(
controller: controller,
children: <Widget>[
// Add children
],
)
The scroll position, current page, etc can be checked using the controller.
Note: The
controller.currentPage
returns a double value. For example, when the page is being swiped the value goes from 1 to 2 gradually and does not instantly jump to 2.
Adding Custom Transitions to PageViews
Let’s discuss adding a few custom transition to the pages using Transform
+ PageView
. This part will use the Transform
widget extensively and I recommend reading one of the multiple articles on the widget.
My recommendations would be the Deep Dive I wrote and WM Leler’s Transform article.
Transition 1
The setup
We first use a basic PageView.builder
PageView.builder(
controller: controller,
itemBuilder: (context, position) {
},
itemCount: 10,
)
Let’s have 10 items for now.
We use a PageController
and a variable which holds the value of the currentPage
.
Defining the PageController and variables:
PageController controller = PageController();
var currentPageValue = 0.0;
Updating the variable when the PageView
is scrolled.
controller.addListener(() {
setState(() {
currentPageValue = controller.page;
});
});
Finally, we construct the PageView
.
Now, let’s check for three conditions:
- If the page is the page being swiped from
- If the page is the page being swiped to
- If the page is a page off screen
PageView.builder(
controller: controller,
itemBuilder: (context, position) {
if (position == currentPageValue.floor()) { } else if (position == currentAnimationValue.floor() + 1){
} else {
}
},
itemCount: 10,
)
Now we return the same page but wrapped in a Transform
widget to transform our pages when we swipe it.
PageView.builder(
controller: controller,
itemBuilder: (context, position) {
if (position == currentPageValue.floor()) {
return Transform(
transform: Matrix4.identity()..rotateX(currentPageValue - position),
child: Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
),
);
} else if (position == currentPageValue.floor() + 1){
return Transform(
transform: Matrix4.identity()..rotateX(currentPageValue - position),
child: Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
),
);
} else {
return Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
);
}
},
itemCount: 10,
)
Here, we transform the page being swiped from and the page being swiped to.
currentPageValue.floor() gives us the page on the left and
currentPageValue.floor() gives us the page on the right
In this example, we rotate the page on the X direction as it is swiped by a value of currentPageValue
minus the index
in radiants. You can amplify the effect by multiplying this value.
We can tweak this transform and alignment of transform to give us multiple types of new page transitions.
Transition 2
Similar code structure, just with a different transformation:
PageView.builder(
controller: controller,
itemBuilder: (context, position) {
if (position == currentPageValue.floor()) {
return Transform(
transform: Matrix4.identity()..rotateY(currentPageValue - position)..rotateZ(currentPageValue - position),
child: Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
),
);
} else if (position == currentPageValue.floor() + 1){
return Transform(
transform: Matrix4.identity()..rotateY(currentPageValue - position)..rotateZ(currentPageValue - position),
child: Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
),
);
} else {
return Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
);
}
},
itemCount: 10,
)
Here, we rotate around both Y and Z axes.
Transition 3
This is a similar type of transition last time but with a 3-D effect added.
PageView.builder(
controller: controller,
itemBuilder: (context, position) {
if (position == currentPageValue.floor()) {
return Transform(
transform: Matrix4.identity()..setEntry(3, 2, 0.004)..rotateY(currentPageValue - position)..rotateZ(currentPageValue - position),
child: Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
),
);
} else if (position == currentPageValue.floor() + 1){
return Transform(
transform: Matrix4.identity()..setEntry(3, 2, 0.004)..rotateY(currentPageValue - position)..rotateZ(currentPageValue - position),
child: Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
),
);
} else {
return Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
);
}
},
itemCount: 10,
)
The line
..setEntry(3, 2, 0.004)
gives the pages a 3-D like effect.
Transition 4
PageView.builder(
controller: controller,
itemBuilder: (context, position) {
if (position == currentPageValue.floor()) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()..setEntry(3, 2, 0.001)
..rotateX(currentPageValue - position)
..rotateY(currentPageValue - position)
..rotateZ(currentPageValue - position),
child: Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
),
);
} else if (position == currentPageValue.floor() + 1){
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()..setEntry(3, 2, 0.001)
..rotateX(currentPageValue - position)
..rotateY(currentPageValue - position)
..rotateZ(currentPageValue - position),
child: Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
),
);
} else {
return Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Center(
child: Text(
"Page",
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
);
}
},
itemCount: 10,
)
A lot more types can be created by simply changing rotation angles, axes, alignments and translations.
Demo App using PageView
To demonstrate a simple app using PageView
in Flutter, I created an example app to study words for the GRE. This app displays and lets the user save the hardest words using SQLite to save them. It also has Text-To-Speech for pronouncing the word itself.
You can find this app here: https://github.com/deven98/FlutterGREWords
That’s it for this article! I hope you enjoyed it, and leave a few claps if you did. Follow me for more Flutter articles and comment for any feedback you might have about this article.
Feel free to check out my other profiles and articles as well: