Flutter прилипание скролла (scroll snap) в SliverList
Пытаюсь сделать прилипание скролла для SliverList:
Всё прекрасно работает ровно до ресайза:
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class _ScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
}
const String title = 'Snapping Slivers';
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
scrollBehavior: _ScrollBehavior(),
title: title,
home: const _SliverParentWidget(),
);
}
class _SliverParentWidget extends StatefulWidget {
const _SliverParentWidget({Key? key}) : super(key: key);
@override
State<_SliverParentWidget> createState() => _SliverParentWidgetState();
}
class _SliverParentWidgetState extends State<_SliverParentWidget> {
final List<int> _list = List<int>.generate(30, (index) => index);
final int itemsCount = 3;
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text(title),
),
body: CustomScrollView(
physics: _Physics(itemsCount: itemsCount),
scrollDirection: Axis.horizontal,
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
key: ValueKey(index),
width: MediaQuery.of(context).size.width / itemsCount,
alignment: Alignment.center,
color: Colors.blue[200 + _list[index] % itemsCount * 100],
child: Text(_list[index].toString()),
),
childCount: _list.length,
),
),
],
),
);
}
class _Physics extends ScrollPhysics {
const _Physics({required this.itemsCount, ScrollPhysics? parent})
: super(parent: parent);
final int itemsCount;
@override
_Physics applyTo(ScrollPhysics? ancestor) =>
_Physics(itemsCount: itemsCount, parent: buildParent(ancestor));
@override
double applyBoundaryConditions(ScrollMetrics position, double value) {
if (position.pixels <= position.minScrollExtent &&
value < position.pixels) {
return value - position.pixels;
}
if (position.pixels >= position.maxScrollExtent &&
value > position.pixels) {
return value - position.pixels;
}
if (position.pixels > position.minScrollExtent &&
value < position.minScrollExtent) {
return value - position.minScrollExtent;
}
if (position.pixels < position.maxScrollExtent &&
value > position.maxScrollExtent) {
return value - position.maxScrollExtent;
}
return 0;
}
@override
double get maxFlingVelocity => 1400;
double _getTargetPixels(
ScrollMetrics position, Tolerance tolerance, double velocity) {
double page = position.pixels / position.viewportDimension;
if (velocity < -tolerance.velocity) {
page -= 0.5;
} else if (velocity > tolerance.velocity) {
page += 0.5;
}
return page * position.viewportDimension;
}
@override
Simulation? createBallisticSimulation(
ScrollMetrics position, double velocity) {
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;
double target = _getTargetPixels(position, tolerance, velocity);
// new physics
final double distance = 200 *
math.exp(1.2 * math.log(.6 * velocity.abs() / 800)) *
velocity.sign;
final double itemSize = position.viewportDimension / itemsCount;
final int itemPosition = ((position.pixels + distance) / itemSize).round();
target = itemPosition * itemSize;
if (target == position.pixels) {
return null;
}
return ScrollSpringSimulation(
spring,
position.pixels,
target,
velocity,
tolerance: tolerance,
);
}
@override
bool get allowImplicitScrolling => false;
}
Ответы (1 шт):
Очень похоже, что ошибка где-то внутри самого флаттера. Во время расчета позиции экрана после проявления бага (в функции createBallisticSimulation), объект FixedScrollMetrics внутри ScrollMetrics, выдает значения отличные от того, что видно на экране. Если верить приходящим значениям, то сдвиг viewPort от начала списка совпадает с координатой элемента списка, но это не так.
Так же ошибка воспроизводится, если использовать вместо SliverList ListView.Builder с PageScrollPhysics физикой:
class ListViewWidget extends StatelessWidget {
const ListViewWidget({
Key? key,
required List<int> list,
required this.itemsCount,
}) : _list = list,
super(key: key);
final int itemsCount;
final List<int> _list;
@override
Widget build(BuildContext context) {
return ListView.builder(
physics: const PageScrollPhysics(),
scrollDirection: Axis.horizontal,
itemCount: _list.length,
itemBuilder: (context, index) {
return SizedBox(
width: MediaQuery.of(context).size.width / itemsCount,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
index.toString(),
style: const TextStyle(fontSize: 22.0),
),
),
),
);
},
);
}
}
Рекомендую завести баг в репозитории флаттера. (https://github.com/flutter/flutter/issues/new/choose)
Как временное решение, можно прокручивать список к началу при изменении размеров окна.

