Flutter - 自定义物理滚动
在本文中,我们将编写自己的内容ScrollPhysics
来更改中滚动的行为ListView
。
KISS(Keep It Simple… pleaSe)
重复出现的情况,我们有一组页面或幻灯片要迭代。
Default PageView
执行此操作的代码非常简单。我们只需要使用PageView
默认属性即可。
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
final List<int> pages = List.generate(4, (index) => index);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: PageView.builder(
itemCount: pages.length,
itemBuilder: (context, index) {
return Container(
color: randomColor,
margin: const EdgeInsets.all(20.0),
);
},
),
),
);
}
Color get randomColor =>
Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0).withOpacity(1.0);
}
真棒。但是,
有时我们想给用户一个提示,或者也许不是真正的页面,而是列表中我们要迭代的元素。在这种情况下,如果当前元素仅填充视口的一小部分,这样我们就可以看到下一个(或上一个元素)的一部分,那就太好了。
不用担心,Flutter掩盖了一切。我们可以使用PageController
。
PageView with viewportFraction
代码仍然非常简单。我们只需要将设置为viewportFraction
要由列表中的当前元素填充的视口的比例。
class MyHomePage extends StatelessWidget {
final List<int> pages = List.generate(4, (index) => index);
final _pageController = PageController(viewportFraction: 0.8);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: PageView.builder(
controller: _pageController,
itemCount: pages.length,
itemBuilder: (context, index) {
return Container(
color: randomColor,
margin: const EdgeInsets.all(20.0),
);
},
),
),
);
}
真棒。但是,
如果这不正是我们想要的呢?我不想让当前元素居中,而是希望它看起来更像是一个项目列表,但我仍然想一次滚动一个项目?
Best of both worlds
为了做到这一点,我们需要更深入地研究一个到目前为止尚未使用的属性ScrollPhysics
。
行与PageView
PageView
更适合用于用户在其上滑动的一组页面,有点像入门。我们的情况有些不同,因为我们想要一个项目列表,但也想一次滚动一个。抛弃PageView
并使用它是有意义的ListView
。
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: pages.length,
itemBuilder: (context, index) => Container(
height: double.infinity,
width: 300,
color: randomColor,
margin: const EdgeInsets.all(20.0),
),
),
),
);
}
简单。但是,如果现在滚动,您将看到我们失去了一对一的滚动。我们正在处理列表中的项目,而不再处理页面,因此我们需要自己构建页面的概念,我们可以使用中的属性physics
来实现ListView
。
ScrollPhysics
我们已经ScrollPhysics
可以使用不同的子类来控制滚动的发生方式,但是其中一个听起来很有趣:PageScrollPhysics
。
PageScrollPhysics
是PageView
内部使用的,但遗憾的是,如果我们将其与ListView
它一起使用不起作用。我们可以做的是构建自己的版本。让我们PageScrollPhysics
先来看一下。
ScrollPhysics用于PageView
class PageScrollPhysics extends ScrollPhysics {
/// Creates physics for a [PageView].
const PageScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
@override
PageScrollPhysics applyTo(ScrollPhysics ancestor) {
return PageScrollPhysics(parent: buildParent(ancestor));
}
double _getPage(ScrollPosition position) {
if (position is _PagePosition)
return position.page;
return position.pixels / position.viewportDimension;
}
double _getPixels(ScrollPosition position, double page) {
if (position is _PagePosition)
return position.getPixelsFromPage(page);
return page * position.viewportDimension;
}
double _getTargetPixels(ScrollPosition position, Tolerance tolerance, double velocity) {
double page = _getPage(position);
if (velocity < -tolerance.velocity)
page -= 0.5;
else if (velocity > tolerance.velocity)
page += 0.5;
return _getPixels(position, page.roundToDouble());
}
@override
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels)
return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
return null;
}
@override
bool get allowImplicitScrolling => false;
}
该方法createBallisticSimulation
是该类的入口,并具有滚动条中的位置和速度作为输入参数。基本上,这是在检查用户是否向右或向左滚动,并在这种情况下计算滚动中的新位置,基本上是当前位置的当前位置,加上或减去视口的范围,例如滚动PageView
是一个接一个的。
我们想做非常相似的事情,但是在我们的情况下,我们不使用viewport,而是使用一些自定义单位,因为每个viewport中有多个项。
在每个滚动条上列出Item和viewport
我们可以计算自己的这个自定义单位,它将滚动的总扩展数除以列表中的项数减一。为什么要减一?列表中的1个项目表示无滚动,2个项目表示滚动1个,…所以N个项目表示滚动N-1。
CustomScrollPhysics
class CustomScrollPhysics extends ScrollPhysics {
final double itemDimension;
CustomScrollPhysics({this.itemDimension, ScrollPhysics parent})
: super(parent: parent);
@override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(
itemDimension: itemDimension, parent: buildParent(ancestor));
}
double _getPage(ScrollPosition position) {
return position.pixels / itemDimension;
}
double _getPixels(double page) {
return page * itemDimension;
}
double _getTargetPixels(
ScrollPosition position, Tolerance tolerance, double velocity) {
double page = _getPage(position);
if (velocity < -tolerance.velocity) {
page -= 0.5;
} else if (velocity > tolerance.velocity) {
page += 0.5;
}
return _getPixels(page.roundToDouble());
}
@override
Simulation createBallisticSimulation(
ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels)
return ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
return null;
}
@override
bool get allowImplicitScrolling => false;
}
我们正在覆盖getPixels()
,它根据页码返回位置,而getPage()
,根据位置返回页。其他一切都可以正常工作,但是我们仍然需要传入itemDimension
构造函数。
使用CustomScrollPhysics
对我们来说幸运的是,ScrollController
可以为我们提供最大程度的滚动显示,但是直到构建了小部件之后,该滚动显示才可用。我们需要将页面转换为,StatefulWidget
并监听我们的控制器,直到知道尺寸可用为止,然后初始化CustomScrollPhysics
。
final _controller = ScrollController();
final List<int> pages = List.generate(4, (index) => index);
ScrollPhysics _physics;
@override
void initState() {
super.initState();
_controller.addListener(() {
if (_controller.position.haveDimensions && _physics == null) {
setState(() {
var dimension =
_controller.position.maxScrollExtent / (pages.length - 1);
_physics = CustomScrollPhysics(itemDimension: dimension);
});
}
});
}
就这样。有了这个,我们有了一个带有一个滚动条的item列表。
概括
这是一个基本示例,将通过创建自己的ScrollPhysics
类来帮助我们自定义滚动条。在这种情况下,我们将与一起使用一对一滚动ListView
。检查整个代码。
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _controller = ScrollController();
final List<int> pages = List.generate(4, (index) => index);
ScrollPhysics _physics;
@override
void initState() {
super.initState();
_controller.addListener(() {
if (_controller.position.haveDimensions && _physics == null) {
setState(() {
var dimension =
_controller.position.maxScrollExtent / (pages.length - 1);
_physics = CustomScrollPhysics(itemDimension: dimension);
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView.builder(
scrollDirection: Axis.horizontal,
controller: _controller,
physics: _physics,
itemCount: pages.length,
itemBuilder: (context, index) => Container(
height: double.infinity,
width: 300,
color: randomColor,
margin: const EdgeInsets.all(20.0),
),
),
),
);
}
Color get randomColor =>
Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0).withOpacity(1.0);
}
class CustomScrollPhysics extends ScrollPhysics {
final double itemDimension;
CustomScrollPhysics({this.itemDimension, ScrollPhysics parent})
: super(parent: parent);
@override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(
itemDimension: itemDimension, parent: buildParent(ancestor));
}
double _getPage(ScrollPosition position) {
return position.pixels / itemDimension;
}
double _getPixels(double page) {
return page * itemDimension;
}
double _getTargetPixels(
ScrollPosition position, Tolerance tolerance, double velocity) {
double page = _getPage(position);
if (velocity < -tolerance.velocity) {
page -= 0.5;
} else if (velocity > tolerance.velocity) {
page += 0.5;
}
return _getPixels(page.roundToDouble());
}
@override
Simulation createBallisticSimulation(
ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels)
return ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
return null;
}
@override
bool get allowImplicitScrolling => false;
}
谢谢阅读!
翻译自:https://medium.com/flutter-community/custom-scroll-physics-in-flutter-3224dd9e9b41