1

I am using Flutter ListView, and need to let user scroll only when the user is using two fingers. If the user uses only one finger, I should disable scrolling.

I have no idea how to implement this :/ What I have tried: I have tried to subclass the ScrollPhysics and rewrite the shouldAcceptUserOffset or applyPhysicsToUserOffset, but none works. I have also tried reading through the doc, without finding anything related.

Thanks very much for any suggestions!

ch271828n
  • 15,854
  • 5
  • 53
  • 88

1 Answers1

1

After digging into the Flutter source code (though a lot of code, but looks beautiful), I finally find out a hack. This works well for my iOS simulator and Android phone.

Remark:

  • If you also want to scroll even with single finger, but only want to fix this bug (scrolling with two fingers scrolls twice as fast), then set onlyScrollableWhenMultiFinger: true of the MultiFingerListViewController in the sample below.

Steps to use:

  1. Add MultiFingerListViewParent as a parent of the listview.
  2. set the physics of ListView.

Sample:

class TestDoubleFingerScrollV2 extends StatefulWidget {
  @override
  _TestDoubleFingerScrollV2State createState() => _TestDoubleFingerScrollV2State();
}

class _TestDoubleFingerScrollV2State extends State<TestDoubleFingerScrollV2> {
  final controller = MultiFingerListViewController(onlyScrollableWhenMultiFinger: true);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('TestDoubleFingerScrollV2')),
        body: MultiFingerListViewParent(
          controller: controller,
          child: ListView.builder(
              physics: MultiFingerScrollPhysics(controller: controller),
              itemCount: 100,
              itemBuilder: (context, index) =>
                  Container(height: 60, color: Colors.green.withAlpha((index * 50) % 255), child: Text('$index'))),
        ));
  }
}

Source:

import 'package:flutter/material.dart';
import 'package:flutter_proj/components/finger_aware_listener.dart';
import 'package:flutter_proj/components/mux_listener.dart';

class MultiFingerListViewController {
  final bool onlyScrollableWhenMultiFinger;
  final _fingerAwareListener = FingerAwareListener();

  bool _lastShouldScrollable = false;

  MultiFingerListViewController({@required this.onlyScrollableWhenMultiFinger});

  bool _updateShouldScrollable() {
    _lastShouldScrollable =
        (!onlyScrollableWhenMultiFinger) || _fingerAwareListener.info.isCurrentRunMultiFingerUpToNow;
    return _lastShouldScrollable;
  }
}

class MultiFingerListViewParent extends StatelessWidget {
  final MultiFingerListViewController controller;
  final Widget child;

  const MultiFingerListViewParent({Key key, this.child, this.controller}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MuxListener(
      onPointer: controller._fingerAwareListener.handlePointer,
      child: child,
    );
  }
}

// This sub-class should have some boilerplate code, e.g. look at [NeverScrollableScrollPhysics] to see it
/// NOTE (WARN): This is a HACK, VIOLATING what the comments said for `applyPhysicsToUserOffset`. But works well for me.
class MultiFingerScrollPhysics extends ScrollPhysics {
  final MultiFingerListViewController controller;

  const MultiFingerScrollPhysics({ScrollPhysics parent, @required this.controller}) : super(parent: parent);

  @override
  MultiFingerScrollPhysics applyTo(ScrollPhysics ancestor) {
    return MultiFingerScrollPhysics(controller: controller, parent: buildParent(ancestor));
  }

  /// NOTE This **HACK** is actually **VIOLATING** what the comment says!
  /// The comment in [ScrollPhysics.applyPhysicsToUserOffset] says:
  /// "This method must not adjust parts of the offset that are entirely within
  ///  the bounds described by the given `position`."
  /// In addition, when looking at [BouncingScrollPhysics.applyPhysicsToUserOffset],
  /// we see they directly return the original `offset` when `!position.outOfRange`
  double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
    if (controller._updateShouldScrollable()) {
      // When k fingers are dragging, the speed is actually *k* times the normal speed. So we divide it by k.
      // (see https://github.com/flutter/flutter/issues/11884)
      final currNumFinger = controller._fingerAwareListener.info.currentRunSeenPointers.length;
      return offset / currNumFinger;
    } else {
      return 0.0;
    }
  }

  @override
  Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
    // When this method is called, the fingers seem to all *have left* the screen. Thus we cannot calculate the
    // current information, but should use the previous cache.
    if (controller._lastShouldScrollable) {
      return super.createBallisticSimulation(position, velocity);
    } else {
      return null;
    }
  }
}

It depends on several other tiny components (very short code), and I put them here.

Hope it helps! If you have better solution please let me know :)

ch271828n
  • 15,854
  • 5
  • 53
  • 88