[译者注:ScrollPhysics 非常强大好用,可以定制各种滑动效果,通过设置阻尼系数等等实现]
在这篇文章中我们将定制ScrollPhysics来改变ListView的滚动行为KISS (保持简单....别想多了,哈哈)
在一个多页面集合或者幻灯片集合循环访问,是一个经常出现的场景。
实现这个效果的代码非常简单,我们只需要使用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就能做到。
代码依然非常简单。我们只需要把想要的视图的百分比设置到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),
);
},
),
),
);
}
非常酷炫. 但是.
如果这不是我们想要的效果呢?我想要元素想一整个列表一样,而不是居中;但是又想要一次滚动一个元素。
为了实现这个效果,我们需要深入了解一下个我们还没用过的属性:ScrollPhysics
Row vs 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),
),
),
),
);
}
简单。但是如果你向右滑动,就会发现不能一次滑动一个元素。 我们现在是在处理List里面的元素了,不再是页面。所以我们需要自己建立页面的概念,我们可以使用ListView的physics属性做到这种效果。
ScrollPhysics
已经有不同的ScrollPhysics之类可以用来控制滑动效果;其中有一个看起来非常有趣,PageScrollPhysics。PageView内部使用的就是PageScrollPhysics, 不兴的是,在ListView中使用无效。我们可以自己设计一个出来,先看看PageScrollPhysics的实现。
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是这个类的入口,将滚动条中的位置和速度作为输入参数。 首先这是在检查用户是向右滚动还是向左滚动,接着计算滚动条中的新位置,也就是将当前加或减视图的范围,因为页面视图中的滚动是一个接一个的。
我们要做的非常类似,但是我们没有使用视图(viewport),而是使用自定义的大小,因为每个视图有多个元素。
这个自定义的大小我们可以自己计算,它是滚动条的总大小除以列表元素个数再减1.为什么要减1呢?列表中有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可以获取滚动条的长度;但是呢,需要等到widget被创建出来之后才可以拿到。我们需要把我们的Page改为StatefulWidget,去监听dimensions的有效性通知,然后初始化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);
});
}
});
}
到此,我们可以让List一次滑动一个元素了。
总结
这是一个自定义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;
}
就写这么多,欢迎提问互动。感谢您的阅读!