Android
的ViewPager2
在更新后的好用程度自是不用多说,我们只需要设置offScreenPageLimit
,就可以轻松实现预加载的效果。但尴尬的是——我们如果不希望它有预加载,就比较困难了;与之相反的是,Flutter
的PageView
却没有类似的属性来设置,每当我们切换到新的页面,child
都会重新走一遍build
的流程,那么,我们该怎么实现类似预加载的效果呢?
前置讨论
从源码入手
- 回想一下
SliverList/ListView
的创建过程,我们在使用ListView.builder
的时候不仅实现了按需加载,还能实现一定范围内的预加载。ListView
有个cacheExtent
属性,就是用来控制预加载的范围大小。 - 那么:根据我们的需求,我们是否可以创建一个可以横向滑动的列表来实现预加载效果?
- 这是我一开始预想的方法,但并未实现(可能在之后有时间会写写)。我更希望在最小的修改范围内实现需求。当我对比
ListView
和PageView
的实现后发现:PageView
在创建Viewport
时,因为考虑了allowImplicitScrolling
属性,实际上没办法兼顾预加载和辅助功能滑动问题 - 相关代码
...
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics = _ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(
widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context))
: widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
);
return NotificationListener(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged!(currentPage);
}
}
return false;
},
child: Scrollable(
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: _controller,
physics: physics,
restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
slivers: [
SliverFillViewport(
viewportFraction: _controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
),
],
);
},
),
);
}
...
AllowImplicitScrolling
- 我们在
package:flutter/lib/src/widgets/scroll_physics.dart
中溯源这个对象的解释,可以在注释中看到如下解释:Whether a viewport is allowed to change its scroll position implicitly in response to a call to [RenderObject.showOnScreen]
[RenderObject.showOnScreen] is for example used to bring a text field fully on screen after it has received focus. This property controls whether the viewport associated with this object is allowed to change the scroll position to fulfill such a request.
- 实际上这个属性的设置是为了考虑辅助功能下
PageView
滑动时焦点的获取问题,但是正因为有了这个属性,我们就无法实现cacheExtent
的自定义
解决办法
不考虑辅助功能
- 如果我们的业务中不考虑这个属性的问题,自然可以直接设置
cacheExtent
来设置预加载的页数。我们复制PageView
的源码,新建一个PreloadPageView
,对外暴露preloadPagesCount
属性,修改Viewport
部分,一行代码就可以满足我们的需求: preload_page_view.dart
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show clampDouble, precisionErrorTolerance;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
class _ForceImplicitScrollPhysics extends ScrollPhysics {
const _ForceImplicitScrollPhysics({
required this.allowImplicitScrolling,
super.parent,
});
@override
_ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
return _ForceImplicitScrollPhysics(
allowImplicitScrolling: allowImplicitScrolling,
parent: buildParent(ancestor),
);
}
@override
final bool allowImplicitScrolling;
}
class PreloadPageView extends StatefulWidget {
PreloadPageView({
super.key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
this.preloadPagesCount = 1,
this.controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
List children = const [],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
}) : childrenDelegate = SliverChildListDelegate(children);
PreloadPageView.builder({
super.key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
this.controller,
this.physics,
this.preloadPagesCount = 1,
this.pageSnapping = true,
this.onPageChanged,
required NullableIndexedWidgetBuilder itemBuilder,
ChildIndexGetter? findChildIndexCallback,
int? itemCount,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
}) : childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
findChildIndexCallback: findChildIndexCallback,
childCount: itemCount,
);
const PreloadPageView.custom({
super.key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
this.controller,
this.physics,
this.preloadPagesCount = 1,
this.pageSnapping = true,
this.onPageChanged,
required this.childrenDelegate,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
});
final int preloadPagesCount;
final bool allowImplicitScrolling;
final String? restorationId;
final Axis scrollDirection;
final bool reverse;
final PageController? controller;
final ScrollPhysics? physics;
final bool pageSnapping;
final ValueChanged<int>? onPageChanged;
final SliverChildDelegate childrenDelegate;
final DragStartBehavior dragStartBehavior;
final Clip clipBehavior;
final ScrollBehavior? scrollBehavior;
final bool padEnds;
@override
State createState() => _PreloadPageViewState();
}
class _PreloadPageViewState extends State<PreloadPageView> {
int _lastReportedPage = 0;
late PageController _controller;
@override
void initState() {
super.initState();
_initController();
_lastReportedPage = _controller.initialPage;
}
@override
void dispose() {
if (widget.controller == null) {
_controller.dispose();
}
super.dispose();
}
void _initController() {
_controller = widget.controller ?? PageController();
}
@override
void didUpdateWidget(PreloadPageView oldWidget) {
if (oldWidget.controller != widget.controller) {
if (oldWidget.controller == null) {
_controller.dispose();
}
_initController();
}
super.didUpdateWidget(oldWidget);
}
AxisDirection _getDirection(BuildContext context) {
switch (widget.scrollDirection) {
case Axis.horizontal:
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection;
case Axis.vertical:
return widget.reverse ? AxisDirection.up : AxisDirection.down;
}
}
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics = _ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(
widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context))
: widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
);
return NotificationListener(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged!(currentPage);
}
}
return false;
},
child: Scrollable(
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: _controller,
physics: physics,
restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
cacheExtent: widget.preloadPagesCount < 1
? 0
: (widget.preloadPagesCount == 1
? 1
: widget.scrollDirection == Axis.horizontal
? MediaQuery.of(context).size.width *
widget.preloadPagesCount -
1
: MediaQuery.of(context).size.height *
widget.preloadPagesCount -
1),
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
slivers: [
SliverFillViewport(
viewportFraction: _controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
),
],
);
},
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(EnumProperty('scrollDirection', widget.scrollDirection));
description.add(FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'));
description.add(DiagnosticsProperty('controller', _controller, showName: false));
description.add(DiagnosticsProperty('physics', widget.physics, showName: false));
description.add(FlagProperty('pageSnapping', value: widget.pageSnapping, ifFalse: 'snapping disabled'));
description.add(FlagProperty('allowImplicitScrolling', value: widget.allowImplicitScrolling, ifTrue: 'allow implicit scrolling'));
}
}
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics = _ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(
widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context))
: widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
);
return NotificationListener(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged!(currentPage);
}
}
return false;
},
child: Scrollable(
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: _controller,
physics: physics,
restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
cacheExtent: widget.preloadPagesCount < 1
? 0
: (widget.preloadPagesCount == 1
? 1
: widget.scrollDirection == Axis.horizontal
? MediaQuery.of(context).size.width *
widget.preloadPagesCount -
1
: MediaQuery.of(context).size.height *
widget.preloadPagesCount -
1),
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
slivers: [
SliverFillViewport(
viewportFraction: _controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
),
],
);
},
),
);
}
修改页面大小
- 这个方法在我的
app
里并不适用,但是它可能有用。PageController
中提供了viewportFraction
来设置当前页面的占比大小,如果我们将其设置为一个小于1
的数,两侧的页面就会被加载
...
late PageController _pageController;
...
_pageController = PageController(viewportFraction: 0.99);
最后
PageView
的预加载问题在笔者目前的版本中仍是一个TODO
,Flutter
团队暂时没有找到更好的办法来解决这个问题,如果使用ListView
相似的方法构建PageView
,可能会解决这个问题?
阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22017,转载请注明出处。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22017,转载请注明出处。
评论0