preface
Before GrapeCity/ComponentOne do Microsoft Xaml series of controls, including Silverlight, WPF, Windows Phone, UWP, a set of code multi-terminal sharing, is really fragrant. This is handy for creating a list that can be scrolled horizontally or vertically. But on the Flutter platform, there doesn’t seem to be an out of the box ready-to-eat component.
I often hear people talk about the spicy chicken Flutter, what do not support. In fact, Flutter is open source and extended, and most things can be created if you put your heart into it, it’s just a matter of whether you’re willing to take the time to try.
It’s called FlexGrid, but it doesn’t have nearly as many features as C# FlexGrid, and some of them are not necessary for me to do so. In terms of design concept, Xaml and Flutter are very different. Xaml template, Flutter is immutable, but simple is fast. So for the FlexGrid version of The Flutter, the preference was for lightweight components designed in the form of the Flutter.
Now the following functions are supported:
- Lock the ranks
- in
TabBarView
/PageView
Medium horizontal rolling coherence - High performance with large amounts of data
- Refresh animation and incremental load
The principle of
There is little difficulty in designing this component. Silver is good enough.
structure
The following is pseudocode, just to give a realistic idea. As you can see, this structure can be constructed using the Sliver related components that Flutter provides. The final code is flex_grid.dart
CustomScrollView(
scrollDirection: Axis.vertical,
slivers: <Widget>[
/ / headers
SliverPinnedToBoxAdapter(
child: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: <Widget>[
// Lock the column, if any
SliverPinnedToBoxAdapter(),
SliverList(),
],
),
),
// Lock the rows, if any
SliverPinnedToBoxAdapter(
child: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: <Widget>[
// Lock the column, if any
SliverPinnedToBoxAdapter(),
SliverList(),
],
),
),
// Roll part
SliverList(
CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: <Widget>[
// Lock the column, if any
SliverPinnedToBoxAdapter(),
SliverList(),
],
))
],
);
Copy the code
Horizontal synchronous rolling
If you have read the article on The Flutter Tab Nested Slide Silk (Juejin.cn), this problem should not be difficult to solve either.
ScrollableState
Let’s take a look at ScrollableState again. Once you are familiar with this class, you will have a general idea of the rolling system in Flutter.
Where the gesture came from
In the setCanDrag method, we set the horizontal or vertical Drag listener according to Axis, registering the following events respectively.
. onDown = _handleDragDown .. onStart = _handleDragStart .. onUpdate = _handleDragUpdate .. onEnd = _handleDragEnd .. onCancel = _handleDragCancelCopy the code
_handleDragDown
Initializes a ScrollHoldController object that triggers the _disposeHold callback at _handleDragStart and _handleDragCancel.
Drag? _drag;
ScrollHoldController? _hold;
void _handleDragDown(DragDownDetails details) {
assert(_drag == null);
assert(_hold == null);
_hold = position.hold(_disposeHold);
}
Copy the code
_handleDragStart
Initialize a Drag object and register the _disposeDrag callback.
void _handleDragStart(DragStartDetails details) {
// It's possible for _hold to become null between _handleDragDown and
// _handleDragStart, for example if some user code calls jumpTo or otherwise
// triggers a new activity to begin.
assert(_drag == null);
_drag = position.drag(details, _disposeDrag);
assert(_drag ! =null);
assert(_hold == null);
}
Copy the code
_handleDragUpdate
Update status, and this is where you see the list start to scroll.
void _handleDragUpdate(DragUpdateDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null); _drag? .update(details); }Copy the code
_handleDragEnd
This is the inertia processing of the gesture
void _handleDragEnd(DragEndDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null); _drag? .end(details);assert(_drag == null);
}
Copy the code
_handleDragCancel
Call the cancel method, firing _disposeHold and _disposeDrag
void _handleDragCancel() {
// _hold might be null if the drag started.
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null); _hold? .cancel(); _drag? .cancel();assert(_hold == null);
assert(_drag == null);
}
Copy the code
_disposeHold and _disposeDrag
void _disposeHold() {
_hold = null;
}
void _disposeDrag() {
_drag = null;
}
Copy the code
From this we know how the Flutter gets gestures and feeds back to the scrolling component. There’s a lot of interesting stuff here, but I’ll talk about it in the next post.
DragHoldController
Next, we’ll wrap these methods together for the ScrollController to work on.
class DragHoldController {
DragHoldController(this.position);
final ScrollPosition position;
Drag? _drag;
ScrollHoldController? _hold;
void handleDragDown(DragDownDetails? details) {
assert(_drag == null);
assert(_hold == null);
_hold = position.hold(_disposeHold);
}
void handleDragStart(DragStartDetails details) {
// It's possible for _hold to become null between _handleDragDown and
// _handleDragStart, for example if some user code calls jumpTo or otherwise
// triggers a new activity to begin.
assert(_drag == null);
_drag = position.drag(details, _disposeDrag);
assert(_drag ! =null);
assert(_hold == null);
}
void handleDragUpdate(DragUpdateDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null); _drag? .update(details); }void handleDragEnd(DragEndDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null); _drag? .end(details);assert(_drag == null);
}
void handleDragCancel() {
// _hold might be null if the drag started.
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null); _hold? .cancel(); _drag? .cancel();assert(_hold == null);
assert(_drag == null);
}
void _disposeHold() {
_hold = null;
}
void _disposeDrag() {
_drag = null;
}
void forceCancel() {
_hold = null;
_drag = null;
}
bool gethasDrag => _drag ! =null;
bool gethasHold => _hold ! =null;
double get extentAfter => position.extentAfter;
double get extentBefore => position.extentBefore;
}
Copy the code
ScrollController
We can see that both ScrollHoldController and Drag are created by ScrollPosition, and a single ScrollPosition controls a single list, So should we use the ScrollController directly to control multiple ScrollPositions?
To do this, I created a SyncControllerMixin to synchronize ScrollPosition.
- in
attach
Create the correspondingDragHoldController
And synchronizationposition
的pixels
- in
hold
和drag
Dessynchronous scrolling in related methods detach
When removing
mixin SyncControllerMixin on ScrollController {
final Map<ScrollPosition, DragHoldController> _positionToListener =
<ScrollPosition, DragHoldController>{};
@override
void attach(ScrollPosition position) {
super.attach(position);
assert(! _positionToListener.containsKey(position));// After the list is reclaimed, it is created again, and needs to synchronize the current scroll
if (_positionToListener.isNotEmpty) {
final double pixels = _positionToListener.keys.first.pixels;
if(position.pixels ! = pixels) { position.correctPixels(pixels); } } _positionToListener[position] = DragHoldController(position); }@override
void detach(ScrollPosition position) {
super.detach(position);
assert(_positionToListener.containsKey(position)); _positionToListener[position]! .forceCancel(); _positionToListener.remove(position); }@override
void dispose() {
forceCancel();
super.dispose();
}
void handleDragDown(DragDownDetails? details) {
for (final DragHoldController item in_positionToListener.values) { item.handleDragDown(details); }}void handleDragStart(DragStartDetails details) {
for (final DragHoldController item in_positionToListener.values) { item.handleDragStart(details); }}void handleDragUpdate(DragUpdateDetails details) {
for (final DragHoldController item in_positionToListener.values) { item.handleDragUpdate(details); }}void handleDragEnd(DragEndDetails details) {
for (final DragHoldController item in_positionToListener.values) { item.handleDragEnd(details); }}void handleDragCancel() {
for (final DragHoldController item in_positionToListener.values) { item.handleDragCancel(); }}void forceCancel() {
for (final DragHoldController item in_positionToListener.values) { item.forceCancel(); }}}Copy the code
HorizontalSyncScrollMinxin
So what we’re going to do is we’re going to put everything together, we’re going to put it into horizontal_sync_scroll_minxin.dart.
- Sign up for gesture listen
- Passes to SyncControllerMixin to control the level of synchronous scrolling. If I reach the scrolling boundary,
External TabbarView and PageView, let outside into the outerHorizontalSyncController over gestures
mixin HorizontalSyncScrollMinxin {
Map<Type, GestureRecognizerFactory>? _gestureRecognizers;
Map<Type, GestureRecognizerFactory>? get gestureRecognizers =>
_gestureRecognizers;
SyncControllerMixin? get horizontalController;
SyncControllerMixin? get outerHorizontalSyncController;
ScrollPhysics? get physics;
void initGestureRecognizers() {
_gestureRecognizers = <Type, GestureRecognizerFactory>{ HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>( () => HorizontalDragGestureRecognizer(), (HorizontalDragGestureRecognizer instance) { instance .. onDown = (DragDownDetails details) { _handleDragDown( details, ); }.. onStart = (DragStartDetails details) { _handleDragStart( details, ); }.. onUpdate = (DragUpdateDetails details) { _handleDragUpdate( details, ); }.. onEnd = (DragEndDetails details) { _handleDragEnd( details, ); }.. onCancel = () { _handleDragCancel(); }.. minFlingDistance = physics? .minFlingDistance .. minFlingVelocity = physics? .minFlingVelocity .. maxFlingVelocity = physics? .maxFlingVelocity; })}; }void_handleDragDown( DragDownDetails details, ) { outerHorizontalSyncController? .forceCancel(); horizontalController? .forceCancel(); horizontalController? .handleDragDown(details); }void_handleDragStart(DragStartDetails details) { horizontalController? .handleDragStart(details); }void _handleDragUpdate(DragUpdateDetails details) {
_handleTabView(details);
if(outerHorizontalSyncController? .hasDrag ??false) { outerHorizontalSyncController! .handleDragUpdate(details); }else {
horizontalController!.handleDragUpdate(details);
}
}
void _handleDragEnd(DragEndDetails details) {
if(outerHorizontalSyncController? .hasDrag ??false) { outerHorizontalSyncController! .handleDragEnd(details); }else {
horizontalController!.handleDragEnd(details);
}
}
void_handleDragCancel() { horizontalController? .handleDragCancel(); outerHorizontalSyncController? .handleDragCancel(); }bool _handleTabView(DragUpdateDetails details) {
if(outerHorizontalSyncController ! =null) {
final double delta = details.delta.dx;
// If there are external controllers, such as TabbarView and PageView,
// We need to have the external controller take over the gesture when the table scrolls to the boundary.
if ((delta < 0&& horizontalController! .extentAfter ==0&& outerHorizontalSyncController! .extentAfter ! =0) ||
(delta > 0&& horizontalController! .extentBefore ==0&& outerHorizontalSyncController! .extentBefore ! =0)) {
if(! outerHorizontalSyncController! .hasHold && ! outerHorizontalSyncController! .hasDrag) { outerHorizontalSyncController! .handleDragDown(null); outerHorizontalSyncController! .handleDragStart(DragStartDetails( globalPosition: details.globalPosition, localPosition: details.localPosition, sourceTimeStamp: details.sourceTimeStamp, )); }return true; }}return false;
}
RawGestureDetector buildGestureDetector({required Widget child}) {
return RawGestureDetector(
gestures: gestureRecognizers!,
child: child,
);
}
}
Copy the code
use
parameter | describe | The default |
---|---|---|
frozenedColumnsCount | Number of locked columns | 0 |
frozenedRowsCount | Number of locked rows | 0 |
cellBuilder | The callback used to create the table | required |
headerBuilder | The callback used to create the table header | required |
columnsCount | The number of columns must be greater than 0 | required |
source | The data source for FlexGrid | required |
rowWrapper | This callback is used to decorate the Row Widget | null |
rebuildCustomScrollView | When data changes, whether or not to build it from [LoadingMoreCustomScrollView] | false |
controller | Vertical [ScrollController] | null |
horizontalController | Horizontal [SyncControllerMixin] | null |
outerHorizontalSyncController | Outside of theSyncControllerMixin For use inExtendedTabBarView orExtendedPageView Up here, let’s make the horizontal roll more continuous |
null |
physics | Horizontal and vertical methodsScrollPhysics |
null |
highPerformance | If true, the horizontal and vertical element sizes are forced to improve scrolling performance | false |
headerStyle | Styles are used to describe table headers | CellStyle.header() |
cellStyle | Styles are used to describe tables | CellStyle.cell() |
indicatorBuilder | A callback to create different load states fromLoadingMoreCustomScrollView |
null |
extendedListDelegate | A setting for setting some extension features, which comes fromLoadingMoreCustomScrollView |
null |
headersBuilder | Used to create custom table headers | null |
conclusion
Overall, the implementation of this component is not very difficult, but it is mainly a reintroduction to ScrollableState, and some things related to scrolling are left unexplored for the next article. Yes, another article on extended_nested_scroll_view. It’s been three years and the issue is still there. Why did I refactor this component?
loveFlutter
Love,candy
Welcome aboardFlutter CandiesTogether they produce cute little candies called FlutterQQ group: 181398081
Finally put the Flutter on the bucket. It smells good.