PageView的效果,跟Android的ViewPager类似。 它的使用主要关注PageController元素,和onPageChanged方法。
onPageChanged: /// Called whenever the page in the center of the viewport changes. 每当当前界面的Page发生改变的时候,就会回调该方法。
PageController控制Page控件的展示。
下面是一个完整的例子。它的执行过程是这样的:
1.创建一个PageView控件和PageIndicator,对齐方式是底部居中对齐。 2.PageView控件里面,创建三个子控件,作为page。并赋值onPageChanged的实现方法和PageController。
每次Page切换的时候,就会调用_handlePageViewChanged,调用setState,重新build,根据获取得到的index数值,更新PageIndicator。其中_currentPageIndex用来记录当前的位置,PageIndicator的切换时使用。每次build,都会重新构建PageIndicator。导致build的方法,一种是手动切换Page,一种是点击PageIndciator,通过调用 _pageViewController.animateToPage来实现Page切换,从而导致重新build。
/// Flutter code sample for [PageView].
void main() => runApp(const PageViewExampleApp());
class PageViewExampleApp extends StatelessWidget {
const PageViewExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('PageView Sample')),
body: const PageViewExample(),
),
);
}
}
class PageViewExample extends StatefulWidget {
const PageViewExample({super.key});
@override
State<PageViewExample> createState() => _PageViewExampleState();
}
class _PageViewExampleState extends State<PageViewExample>
with TickerProviderStateMixin {
late PageController _pageViewController;
late TabController _tabController;
int _currentPageIndex = 0;
@override
void initState() {
super.initState();
_pageViewController = PageController();
_tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
super.dispose();
_pageViewController.dispose();
_tabController.dispose();
}
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
PageView(
/// [PageView.scrollDirection] defaults to [Axis.horizontal].
/// Use [Axis.vertical] to scroll vertically.
controller: _pageViewController,
onPageChanged: _handlePageViewChanged,
children: <Widget>[
Center(
child: Text('First Page', style: textTheme.titleLarge),
),
Center(
child: Text('Second Page', style: textTheme.titleLarge),
),
Center(
child: Text('Third Page', style: textTheme.titleLarge),
),
],
),
PageIndicator(
tabController: _tabController,
currentPageIndex: _currentPageIndex,
onUpdateCurrentPageIndex: _updateCurrentPageIndex,
isOnDesktopAndWeb: true,
),
],
);
}
//每次界面上的page切换更新,都将回调该方法
//该方法,获取最新的index,然后通过调用setState,使用新的值重新build界面
void _handlePageViewChanged(int currentPageIndex) {
// if (!_isOnDesktopAndWeb) {
// return;
// }
_tabController.index = currentPageIndex;
setState(() {
_currentPageIndex = currentPageIndex;
});
}
void _updateCurrentPageIndex(int index) {
_tabController.index = index;
_pageViewController.animateToPage(
index,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
bool get _isOnDesktopAndWeb {
if (kIsWeb) {
return true;
}
switch (defaultTargetPlatform) {
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
return true;
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
return false;
}
}
}
/// Page indicator for desktop and web platforms.
///
/// On Desktop and Web, drag gesture for horizontal scrolling in a PageView is disabled by default.
/// You can defined a custom scroll behavior to activate drag gestures,
/// see https://docs.flutter.dev/release/breaking-changes/default-scroll-behavior-drag.
///
/// In this sample, we use a TabPageSelector to navigate between pages,
/// in order to build natural behavior similar to other desktop applications.
class PageIndicator extends StatelessWidget {
const PageIndicator({
super.key,
required this.tabController,
required this.currentPageIndex,
required this.onUpdateCurrentPageIndex,
required this.isOnDesktopAndWeb,
});
final int currentPageIndex;
final TabController tabController;
final void Function(int) onUpdateCurrentPageIndex;
final bool isOnDesktopAndWeb;
@override
Widget build(BuildContext context) {
if (!isOnDesktopAndWeb) {
return const SizedBox.shrink();
}
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
splashRadius: 16.0,
padding: EdgeInsets.zero,
onPressed: () {
if (currentPageIndex == 0) {
return;
}
onUpdateCurrentPageIndex(currentPageIndex - 1);
},
icon: const Icon(
Icons.arrow_left_rounded,
size: 32.0,
),
),
TabPageSelector(
controller: tabController,
color: colorScheme.surface,
selectedColor: colorScheme.primary,
),
IconButton(
splashRadius: 16.0,
padding: EdgeInsets.zero,
onPressed: () {
if (currentPageIndex == 2) {
return;
}
onUpdateCurrentPageIndex(currentPageIndex + 1);
},
icon: const Icon(
Icons.arrow_right_rounded,
size: 32.0,
),
),
],
),
);
}
}
PageView页面缓存&AutomaticKeepAlive
AutomaticKeepAlive 的组件的主要作用是将列表项的根 RenderObject 的 keepAlive 按需自动标记 为 true 或 false。为了方便叙述,我们可以认为根 RenderObject 对应的组件就是列表项的根 Widget,代表整个列表项组件,同时我们将列表组件的 Viewport区域 + cacheExtent(预渲染区域)称为加载区域 :
1.当 keepAlive 标记为 false 时,如果列表项滑出加载区域时,列表组件将会被销毁。
2.当 keepAlive 标记为 true 时,当列表项滑出加载区域后,Viewport 会将列表组件缓存起来;当列表项进入加载区域时,Viewport 从先从缓存中查找是否已经缓存,如果有则直接复用,如果没有则重新创建列表项。
那么 AutomaticKeepAlive 什么时候会将列表项的 keepAlive 标记为 true 或 false 呢?答案是开发者说了算!Flutter 中实现了一套类似 C/S 的机制,AutomaticKeepAlive 就类似一个 Server,它的子组件可以是 Client,这样子组件想改变是否需要缓存的状态时就向 AutomaticKeepAlive 发一个通知消息(KeepAliveNotification),AutomaticKeepAlive 收到消息后会去更改 keepAlive 的状态,如果有必要同时做一些资源清理的工作(比如 keepAlive 从 true 变为 false 时,要释放缓存)。
实现页面缓存,根据上面的描述实现思路就很简单了:让Page 页变成一个 AutomaticKeepAlive Client 即可。为了便于开发者实现,Flutter 提供了一个 AutomaticKeepAliveClientMixin ,我们只需要让 PageState 混入这个 mixin,且同时添加一些必要操作即可:
class _PageState extends State<Page> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用
return Center(child: Text("${widget.text}", textScaleFactor: 5));
}
@override
bool get wantKeepAlive => true; // 是否需要缓存
}
代码很简单,我们只需要提供一个 wantKeepAlive,它会表示 AutomaticKeepAlive 是否需要缓存当前列表项;另外我们必须在 build 方法中调用一下 super.build(context),该方法实现在 AutomaticKeepAliveClientMixin 中,功能就是根据当前 wantKeepAlive 的值给 AutomaticKeepAlive 发送消息,AutomaticKeepAlive 收到消息后就会开始工作。
import 'package:flutter/cupertino.dart';
class PageItem extends StatefulWidget{
const PageItem({
Key? key,
required this.text
}) : super(key: key);
final String text;
@override
_PageItemState createState()=> _PageItemState();
}
class _PageItemState extends State<PageItem> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用
print("build ${widget.text}");
return Center(child: Text("${widget.text}", textScaleFactor: 5));
}
@override
bool get wantKeepAlive => true; // 是否需要缓存
}
AutomaticKeepAliveClientMixin组件是一个StateFul组件,所以,PageItem也必须是一个StateFulWidget:
mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T>
现在我们重新运行一下示例,发现每个 Page 页只会 build 一次,缓存成功了。