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
  • inTabBarView/PageViewMedium 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.

  • inattachCreate the correspondingDragHoldControllerAnd synchronizationpositionpixels
  • inholddragDessynchronous scrolling in related methods
  • detachWhen 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 theSyncControllerMixinFor use inExtendedTabBarVieworExtendedPageViewUp 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?

loveFlutterLove,candyWelcome aboardFlutter CandiesTogether they produce cute little candies called FlutterQQ group: 181398081

Finally put the Flutter on the bucket. It smells good.