From d7f334663288c3090388d9b2a83d41aefc0cdac3 Mon Sep 17 00:00:00 2001 From: "n.morozov" Date: Wed, 20 Apr 2022 18:28:38 +0300 Subject: [PATCH 1/4] speed up scrolling by mouse wheel --- .../lib/src/scrollable_positioned_list.dart | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart b/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart index 60045e9b..c03fd424 100644 --- a/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart +++ b/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:math'; import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -57,6 +58,7 @@ class ScrollablePositionedList extends StatefulWidget { this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.minCacheExtent, + this.extraScrollSpeed, }) : assert(itemCount != null), assert(itemBuilder != null), itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?, @@ -87,12 +89,14 @@ class ScrollablePositionedList extends StatefulWidget { this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, this.minCacheExtent, + this.extraScrollSpeed, }) : assert(itemCount != null), assert(itemBuilder != null), assert(separatorBuilder != null), itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?, scrollOffsetNotifier = scrollOffsetListener as ScrollOffsetNotifier?, super(key: key); + final int? extraScrollSpeed; /// Number of items the [itemBuilder] can produce. final int itemCount; @@ -324,6 +328,30 @@ class _ScrollablePositionedListState extends State double previousOffset = 0; + void _speedUpScrollListener(ScrollController controller) { + if (widget.extraScrollSpeed == null || widget.extraScrollSpeed == 0) { + return; + } + // if (!controller.hasClients) { + // return; + // } + ScrollDirection scrollDirection = controller.position.userScrollDirection; + if (scrollDirection != ScrollDirection.idle) { + double scrollEnd = controller.offset + + (scrollDirection == ScrollDirection.reverse + ? widget.extraScrollSpeed! + : -widget.extraScrollSpeed!); + scrollEnd = min(controller.position.maxScrollExtent, + max(controller.position.minScrollExtent, scrollEnd)); + controller.jumpTo(scrollEnd); + } + } + + void _speedupClosurePrimary() => + _speedUpScrollListener(primary.scrollController); + void _speedupClosureSecondary() => + _speedUpScrollListener(secondary.scrollController); + @override void initState() { super.initState(); @@ -338,6 +366,8 @@ class _ScrollablePositionedListState extends State widget.scrollOffsetController?._attach(this); primary.itemPositionsNotifier.itemPositions.addListener(_updatePositions); secondary.itemPositionsNotifier.itemPositions.addListener(_updatePositions); + primary.scrollController.addListener(_speedupClosurePrimary); + secondary.scrollController.addListener(_speedupClosureSecondary); primary.scrollController.addListener(() { final currentOffset = primary.scrollController.offset; final offsetChange = currentOffset - previousOffset; @@ -369,6 +399,8 @@ class _ScrollablePositionedListState extends State .removeListener(_updatePositions); secondary.itemPositionsNotifier.itemPositions .removeListener(_updatePositions); + primary.scrollController.removeListener(_speedupClosurePrimary); + secondary.scrollController.removeListener(_speedupClosureSecondary); _animationController?.dispose(); super.dispose(); } @@ -612,9 +644,13 @@ class _ScrollablePositionedListState extends State if (opacity.value >= 0.5) { // Secondary [ListView] is more visible than the primary; make it the // new primary. + primary.scrollController.removeListener(_speedupClosurePrimary); var temp = primary; + secondary.scrollController.removeListener(_speedupClosureSecondary); primary = secondary; + primary.scrollController.addListener(_speedupClosurePrimary); secondary = temp; + secondary.scrollController.removeListener(_speedupClosureSecondary); } _isTransitioning = false; opacity.parent = const AlwaysStoppedAnimation(0); From dea3b03810374159999f1c16ad174ac15d4a9d47 Mon Sep 17 00:00:00 2001 From: "n.morozov" Date: Mon, 22 Aug 2022 15:38:23 +0300 Subject: [PATCH 2/4] scoll acceleration now wouldn't conflict with touchscreen gestures --- .../lib/src/scrollable_positioned_list.dart | 100 ++++++++++-------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart b/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart index c03fd424..7a05a710 100644 --- a/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart +++ b/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:math'; import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -323,18 +324,18 @@ class _ScrollablePositionedListState extends State void Function() startAnimationCallback = () {}; bool _isTransitioning = false; + bool _isTouchScreen = false; var _animationController; double previousOffset = 0; void _speedUpScrollListener(ScrollController controller) { - if (widget.extraScrollSpeed == null || widget.extraScrollSpeed == 0) { + if (widget.extraScrollSpeed == null || + widget.extraScrollSpeed == 0 || + _isTouchScreen) { return; } - // if (!controller.hasClients) { - // return; - // } ScrollDirection scrollDirection = controller.position.userScrollDirection; if (scrollDirection != ScrollDirection.idle) { double scrollEnd = controller.offset + @@ -441,57 +442,34 @@ class _ScrollablePositionedListState extends State builder: (context, constraints) { final cacheExtent = _cacheExtent(constraints); return Listener( - onPointerDown: (_) => _stopScroll(canceled: true), - child: Stack( - children: [ - PostMountCallback( - key: primary.key, - callback: startAnimationCallback, - child: FadeTransition( - opacity: ReverseAnimation(opacity), - child: NotificationListener( - onNotification: (_) => _isTransitioning, - child: PositionedList( - itemBuilder: widget.itemBuilder, - separatorBuilder: widget.separatorBuilder, - itemCount: widget.itemCount, - positionedIndex: primary.target, - controller: primary.scrollController, - itemPositionsNotifier: primary.itemPositionsNotifier, - scrollDirection: widget.scrollDirection, - reverse: widget.reverse, - cacheExtent: cacheExtent, - alignment: primary.alignment, - physics: widget.physics, - shrinkWrap: widget.shrinkWrap, - addSemanticIndexes: widget.addSemanticIndexes, - semanticChildCount: widget.semanticChildCount, - padding: widget.padding, - addAutomaticKeepAlives: widget.addAutomaticKeepAlives, - addRepaintBoundaries: widget.addRepaintBoundaries, - ), - ), - ), - ), - if (_isTransitioning) + onPointerMove: (event) { + _isTouchScreen = event.kind == PointerDeviceKind.touch; + }, + onPointerHover: (event) => + _isTouchScreen = event.kind == PointerDeviceKind.touch, + child: GestureDetector( + onPanDown: (_) => _stopScroll(canceled: true), + excludeFromSemantics: true, + child: Stack( + children: [ PostMountCallback( - key: secondary.key, + key: primary.key, callback: startAnimationCallback, child: FadeTransition( - opacity: opacity, + opacity: ReverseAnimation(opacity), child: NotificationListener( - onNotification: (_) => false, + onNotification: (_) => _isTransitioning, child: PositionedList( itemBuilder: widget.itemBuilder, separatorBuilder: widget.separatorBuilder, itemCount: widget.itemCount, - itemPositionsNotifier: secondary.itemPositionsNotifier, - positionedIndex: secondary.target, - controller: secondary.scrollController, + positionedIndex: primary.target, + controller: primary.scrollController, + itemPositionsNotifier: primary.itemPositionsNotifier, scrollDirection: widget.scrollDirection, reverse: widget.reverse, cacheExtent: cacheExtent, - alignment: secondary.alignment, + alignment: primary.alignment, physics: widget.physics, shrinkWrap: widget.shrinkWrap, addSemanticIndexes: widget.addSemanticIndexes, @@ -503,7 +481,39 @@ class _ScrollablePositionedListState extends State ), ), ), - ], + if (_isTransitioning) + PostMountCallback( + key: secondary.key, + callback: startAnimationCallback, + child: FadeTransition( + opacity: opacity, + child: NotificationListener( + onNotification: (_) => false, + child: PositionedList( + itemBuilder: widget.itemBuilder, + separatorBuilder: widget.separatorBuilder, + itemCount: widget.itemCount, + itemPositionsNotifier: + secondary.itemPositionsNotifier, + positionedIndex: secondary.target, + controller: secondary.scrollController, + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + cacheExtent: cacheExtent, + alignment: secondary.alignment, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + addSemanticIndexes: widget.addSemanticIndexes, + semanticChildCount: widget.semanticChildCount, + padding: widget.padding, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + ), + ), + ), + ), + ], + ), ), ); }, From 93b3644d8fb79dc871afd42c11e3d992be1ec6ae Mon Sep 17 00:00:00 2001 From: "n.morozov" Date: Mon, 19 Sep 2022 16:40:57 +0300 Subject: [PATCH 3/4] update for flutter 3.3.*: touchpad scrolling became kinetic and flutter now detects it as "Trackpad" device kind --- .../lib/src/scrollable_positioned_list.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart b/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart index 7a05a710..a597fad1 100644 --- a/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart +++ b/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart @@ -443,10 +443,11 @@ class _ScrollablePositionedListState extends State final cacheExtent = _cacheExtent(constraints); return Listener( onPointerMove: (event) { - _isTouchScreen = event.kind == PointerDeviceKind.touch; + _isTouchScreen = event.kind == PointerDeviceKind.touch || event.kind == PointerDeviceKind.trackpad; + }, + onPointerHover: (event) { + _isTouchScreen = event.kind == PointerDeviceKind.touch || event.kind == PointerDeviceKind.trackpad; }, - onPointerHover: (event) => - _isTouchScreen = event.kind == PointerDeviceKind.touch, child: GestureDetector( onPanDown: (_) => _stopScroll(canceled: true), excludeFromSemantics: true, From fca251aa7501121d529df2bcdcb6b61053bd9d68 Mon Sep 17 00:00:00 2001 From: "n.morozov" Date: Wed, 26 Oct 2022 10:34:21 +0300 Subject: [PATCH 4/4] Optimization of switching between different scroll devices: touchscreen/touchpad/mouse wheel. From Flutter 3.3.0 there is new Listener's event usage (see https://github.com/flutter/flutter/issues/112880#issuecomment-1270504869) --- .../lib/src/scrollable_positioned_list.dart | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart b/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart index a597fad1..d24e71ef 100644 --- a/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart +++ b/packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart @@ -442,11 +442,29 @@ class _ScrollablePositionedListState extends State builder: (context, constraints) { final cacheExtent = _cacheExtent(constraints); return Listener( + onPointerDown: (event) { + // here we're checking if it's tap by touchscreen + _isTouchScreen = event.kind == PointerDeviceKind.touch || + event.kind == PointerDeviceKind.trackpad; + }, onPointerMove: (event) { - _isTouchScreen = event.kind == PointerDeviceKind.touch || event.kind == PointerDeviceKind.trackpad; + // onPointerMove triggers when finger are dragging to scroll + _isTouchScreen = event.kind == PointerDeviceKind.touch || + event.kind == PointerDeviceKind.trackpad; }, onPointerHover: (event) { - _isTouchScreen = event.kind == PointerDeviceKind.touch || event.kind == PointerDeviceKind.trackpad; + _isTouchScreen = event.kind == PointerDeviceKind.touch || + event.kind == PointerDeviceKind.trackpad; + }, + onPointerPanZoomStart: (event) { + // onPointerPanZoomStart triggers when scrolling by touchpad + _isTouchScreen = event.kind == PointerDeviceKind.touch || + event.kind == PointerDeviceKind.trackpad; + }, + onPointerSignal: (event) { + if (event is PointerScrollEvent) { + _isTouchScreen = event.kind != PointerDeviceKind.mouse; + } }, child: GestureDetector( onPanDown: (_) => _stopScroll(canceled: true),