[譯者注: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;
}
複製程式碼
就寫這麼多,歡迎提問互動。感謝您的閱讀!