PageView 中的 WheelEvent 上的 Flutter Web "smooth scrolling"
Flutter Web "smooth scrolling" on WheelEvent within a PageView
使用下面的代码
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) => MaterialApp(
home: const MyHomePage(),
);
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) => DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Center(
child: Text('use the mouse wheel to scroll')),
bottom: TabBar(
tabs: const [
Center(child: Text('ScrollView')),
Center(child: Text('PageView'))
],
),
),
body: TabBarView(
children: [
SingleChildScrollView(
child: Column(
children: [
for (int i = 0; i < 10; i++)
Container(
height: MediaQuery.of(context).size.height,
child: const Center(
child: FlutterLogo(size: 80),
),
),
],
),
),
PageView(
scrollDirection: Axis.vertical,
children: [
for (int i = 0; i < 10; ++i)
const Center(
child: FlutterLogo(size: 80),
),
],
),
],
),
),
);
}
你可以在 dartpad or from this video,
上看到,运行
使用鼠标滚轮滚动 PageView
提供的体验一般(充其量),
这是一个已知问题 #35687 #32120,但我正在尝试寻找解决方法
实现 PageView
的平滑滚动或至少防止“卡顿”。
有人可以帮助我或指出正确的方向吗?
我不确定问题出在 PageScrollPhysics
;
我直觉问题可能出在 WheelEvent
因为使用多点触控滚动滑动效果非常好
问题出在用户设置上,end-user 如何设置鼠标滚动。我有一个 Logitech 鼠标,它允许我通过 Logitech Options 打开或关闭平滑滚动功能。当我启用平滑滚动时,它可以完美运行并根据需要滚动,但如果禁用平滑滚动,它也会在项目中被禁用。该行为由 end-user.
设置
不过,如果有强制滚动平滑滚动的要求,只能通过设置相关动画来完成。目前还没有直接的方法。
问题是由一系列事件引起的:
- 用户将鼠标滚轮旋转一个档位,
Scrollable
接收PointerSignal
和callsjumpTo
方法,
_PagePosition
的jumpTo方法(派生自ScrollPositionWithSingleContext
)更新滚动位置并调用goBallistic
方法,
- requested from
PageScrollPhysics
simulation returns position back to initial value, since produced by an notch offset is too small to turn page,
- 从步骤 (1) 重复的另一个缺口和过程。
解决问题的一种方法是在调用 goBallistic
方法之前执行延迟。这可以在 _PagePosition class 中完成,但是 class 是私有的,我们必须修补 Flutter SDK:
// <FlutterSDK>/packages/flutter/lib/src/widgets/page_view.dart
// ...
class _PagePosition extends ScrollPositionWithSingleContext implements PageMetrics {
//...
// add this code to fix issue (mostly borrowed from ScrollPositionWithSingleContext):
Timer timer;
@override
void jumpTo(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
forcePixels(value);
didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
didEndScroll();
}
if (timer != null) timer.cancel();
timer = Timer(Duration(milliseconds: 200), () {
goBallistic(0.0);
timer = null;
});
}
// ...
}
另一种方法是将jumpTo替换为animateTo。这可以在不修补 Flutter SDK 的情况下完成,但看起来更复杂,因为我们需要禁用默认 PointerSignalEvent
listener:
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class PageViewLab extends StatefulWidget {
@override
_PageViewLabState createState() => _PageViewLabState();
}
class _PageViewLabState extends State<PageViewLab> {
final sink = StreamController<double>();
final pager = PageController();
@override
void initState() {
super.initState();
throttle(sink.stream).listen((offset) {
pager.animateTo(
offset,
duration: Duration(milliseconds: 200),
curve: Curves.ease,
);
});
}
@override
void dispose() {
sink.close();
pager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Mouse Wheel with PageView'),
),
body: Container(
constraints: BoxConstraints.expand(),
child: Listener(
onPointerSignal: _handlePointerSignal,
child: _IgnorePointerSignal(
child: PageView.builder(
controller: pager,
scrollDirection: Axis.vertical,
itemCount: Colors.primaries.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(color: Colors.primaries[index]),
);
},
),
),
),
),
);
}
Stream<double> throttle(Stream<double> src) async* {
double offset = pager.position.pixels;
DateTime dt = DateTime.now();
await for (var delta in src) {
if (DateTime.now().difference(dt) > Duration(milliseconds: 200)) {
offset = pager.position.pixels;
}
dt = DateTime.now();
offset += delta;
yield offset;
}
}
void _handlePointerSignal(PointerSignalEvent e) {
if (e is PointerScrollEvent && e.scrollDelta.dy != 0) {
sink.add(e.scrollDelta.dy);
}
}
}
// workaround https://github.com/flutter/flutter/issues/35723
class _IgnorePointerSignal extends SingleChildRenderObjectWidget {
_IgnorePointerSignal({Key key, Widget child}) : super(key: key, child: child);
@override
RenderObject createRenderObject(_) => _IgnorePointerSignalRenderObject();
}
class _IgnorePointerSignalRenderObject extends RenderProxyBox {
@override
bool hitTest(BoxHitTestResult result, {Offset position}) {
final res = super.hitTest(result, position: position);
result.path.forEach((item) {
final target = item.target;
if (target is RenderPointerListener) {
target.onPointerSignal = null;
}
});
return res;
}
}
Here is demo 在 CodePen 上。
非常相似但更容易设置:
将 smooth_scroll_web ^0.0.4 添加到您的 pubspec.yaml
...
dependencies:
...
smooth_scroll_web: ^0.0.4
...
用法:
import 'package:smooth_scroll_web/smooth_scroll_web.dart';
import 'package:flutter/material.dart';
import 'dart:math'; // only for demo
class Page extends StatefulWidget {
@override
PageState createState() => PageState();
}
class PageState extends State<Page> {
final ScrollController _controller = new ScrollController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("SmoothScroll Example"),
),
body: SmoothScrollWeb(
controller: controller,
child: Container(
height: 1000,
child: ListView(
physics: NeverScrollableScrollPhysics(),
controller: _controller,
children: [
// Your content goes here, thoses children are only for demo
for (int i = 0; i < 100; i++)
Container(
height: 60,
color: Color.fromARGB(1,
Random.secure().nextInt(255),
Random.secure().nextInt(255),
Random.secure().nextInt(255)),
),
],
),
),
),
);
}
}
谢谢hobbister!
参考 Github 上的 flutter's issue #32120。
我知道这个问题已经快 1.5 年了,但我找到了一种运行顺利的方法。也许这对任何阅读它的人都会有帮助。使用此代码向您的页面视图控制器添加一个侦听器(您可以调整持续时间或 nextPage/animateToPage/jumpToPage 等):
pageController.addListener(() {
if (pageController.position.userScrollDirection == ScrollDirection.reverse) {
pageController.nextPage(duration: const Duration(milliseconds: 60), curve: Curves.easeIn);
} else if (pageController.position.userScrollDirection == ScrollDirection.forward) {
pageController.previousPage(duration: const Duration(milliseconds: 60), curve: Curves.easeIn);
}
});
使用下面的代码
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) => MaterialApp(
home: const MyHomePage(),
);
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) => DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Center(
child: Text('use the mouse wheel to scroll')),
bottom: TabBar(
tabs: const [
Center(child: Text('ScrollView')),
Center(child: Text('PageView'))
],
),
),
body: TabBarView(
children: [
SingleChildScrollView(
child: Column(
children: [
for (int i = 0; i < 10; i++)
Container(
height: MediaQuery.of(context).size.height,
child: const Center(
child: FlutterLogo(size: 80),
),
),
],
),
),
PageView(
scrollDirection: Axis.vertical,
children: [
for (int i = 0; i < 10; ++i)
const Center(
child: FlutterLogo(size: 80),
),
],
),
],
),
),
);
}
你可以在 dartpad or from this video,
上看到,运行使用鼠标滚轮滚动 PageView
提供的体验一般(充其量),
这是一个已知问题 #35687 #32120,但我正在尝试寻找解决方法
实现 PageView
的平滑滚动或至少防止“卡顿”。
有人可以帮助我或指出正确的方向吗?
我不确定问题出在 PageScrollPhysics
;
我直觉问题可能出在 WheelEvent
因为使用多点触控滚动滑动效果非常好
问题出在用户设置上,end-user 如何设置鼠标滚动。我有一个 Logitech 鼠标,它允许我通过 Logitech Options 打开或关闭平滑滚动功能。当我启用平滑滚动时,它可以完美运行并根据需要滚动,但如果禁用平滑滚动,它也会在项目中被禁用。该行为由 end-user.
设置不过,如果有强制滚动平滑滚动的要求,只能通过设置相关动画来完成。目前还没有直接的方法。
问题是由一系列事件引起的:
- 用户将鼠标滚轮旋转一个档位,
Scrollable
接收PointerSignal
和callsjumpTo
方法,_PagePosition
的jumpTo方法(派生自ScrollPositionWithSingleContext
)更新滚动位置并调用goBallistic
方法,- requested from
PageScrollPhysics
simulation returns position back to initial value, since produced by an notch offset is too small to turn page, - 从步骤 (1) 重复的另一个缺口和过程。
解决问题的一种方法是在调用 goBallistic
方法之前执行延迟。这可以在 _PagePosition class 中完成,但是 class 是私有的,我们必须修补 Flutter SDK:
// <FlutterSDK>/packages/flutter/lib/src/widgets/page_view.dart
// ...
class _PagePosition extends ScrollPositionWithSingleContext implements PageMetrics {
//...
// add this code to fix issue (mostly borrowed from ScrollPositionWithSingleContext):
Timer timer;
@override
void jumpTo(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
forcePixels(value);
didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
didEndScroll();
}
if (timer != null) timer.cancel();
timer = Timer(Duration(milliseconds: 200), () {
goBallistic(0.0);
timer = null;
});
}
// ...
}
另一种方法是将jumpTo替换为animateTo。这可以在不修补 Flutter SDK 的情况下完成,但看起来更复杂,因为我们需要禁用默认 PointerSignalEvent
listener:
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class PageViewLab extends StatefulWidget {
@override
_PageViewLabState createState() => _PageViewLabState();
}
class _PageViewLabState extends State<PageViewLab> {
final sink = StreamController<double>();
final pager = PageController();
@override
void initState() {
super.initState();
throttle(sink.stream).listen((offset) {
pager.animateTo(
offset,
duration: Duration(milliseconds: 200),
curve: Curves.ease,
);
});
}
@override
void dispose() {
sink.close();
pager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Mouse Wheel with PageView'),
),
body: Container(
constraints: BoxConstraints.expand(),
child: Listener(
onPointerSignal: _handlePointerSignal,
child: _IgnorePointerSignal(
child: PageView.builder(
controller: pager,
scrollDirection: Axis.vertical,
itemCount: Colors.primaries.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(color: Colors.primaries[index]),
);
},
),
),
),
),
);
}
Stream<double> throttle(Stream<double> src) async* {
double offset = pager.position.pixels;
DateTime dt = DateTime.now();
await for (var delta in src) {
if (DateTime.now().difference(dt) > Duration(milliseconds: 200)) {
offset = pager.position.pixels;
}
dt = DateTime.now();
offset += delta;
yield offset;
}
}
void _handlePointerSignal(PointerSignalEvent e) {
if (e is PointerScrollEvent && e.scrollDelta.dy != 0) {
sink.add(e.scrollDelta.dy);
}
}
}
// workaround https://github.com/flutter/flutter/issues/35723
class _IgnorePointerSignal extends SingleChildRenderObjectWidget {
_IgnorePointerSignal({Key key, Widget child}) : super(key: key, child: child);
@override
RenderObject createRenderObject(_) => _IgnorePointerSignalRenderObject();
}
class _IgnorePointerSignalRenderObject extends RenderProxyBox {
@override
bool hitTest(BoxHitTestResult result, {Offset position}) {
final res = super.hitTest(result, position: position);
result.path.forEach((item) {
final target = item.target;
if (target is RenderPointerListener) {
target.onPointerSignal = null;
}
});
return res;
}
}
Here is demo 在 CodePen 上。
非常相似但更容易设置:
将 smooth_scroll_web ^0.0.4 添加到您的 pubspec.yaml
...
dependencies:
...
smooth_scroll_web: ^0.0.4
...
用法:
import 'package:smooth_scroll_web/smooth_scroll_web.dart';
import 'package:flutter/material.dart';
import 'dart:math'; // only for demo
class Page extends StatefulWidget {
@override
PageState createState() => PageState();
}
class PageState extends State<Page> {
final ScrollController _controller = new ScrollController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("SmoothScroll Example"),
),
body: SmoothScrollWeb(
controller: controller,
child: Container(
height: 1000,
child: ListView(
physics: NeverScrollableScrollPhysics(),
controller: _controller,
children: [
// Your content goes here, thoses children are only for demo
for (int i = 0; i < 100; i++)
Container(
height: 60,
color: Color.fromARGB(1,
Random.secure().nextInt(255),
Random.secure().nextInt(255),
Random.secure().nextInt(255)),
),
],
),
),
),
);
}
}
谢谢hobbister!
参考 Github 上的 flutter's issue #32120。
我知道这个问题已经快 1.5 年了,但我找到了一种运行顺利的方法。也许这对任何阅读它的人都会有帮助。使用此代码向您的页面视图控制器添加一个侦听器(您可以调整持续时间或 nextPage/animateToPage/jumpToPage 等):
pageController.addListener(() {
if (pageController.position.userScrollDirection == ScrollDirection.reverse) {
pageController.nextPage(duration: const Duration(milliseconds: 60), curve: Curves.easeIn);
} else if (pageController.position.userScrollDirection == ScrollDirection.forward) {
pageController.previousPage(duration: const Duration(milliseconds: 60), curve: Curves.easeIn);
}
});