Flutter PageView:禁用左侧或右侧滚动

时间:2019-04-03 19:26:14

标签: dart flutter

我有一个PageView,如何禁用向左或向右滚动。我知道使用NeverScrollableScrollPhysics可以禁用滚动,但是如何禁用一个方向的滚动。

5 个答案:

答案 0 :(得分:3)

您还可以使用我最近编写的horizontal_blocked_scroll_physics库,该库将允许您阻止左右移动。

答案 1 :(得分:1)

您可以创建自己的ScrollPhysics,仅允许其向右移动:

            class CustomScrollPhysics extends ScrollPhysics {
              CustomScrollPhysics({ScrollPhysics parent}) : super(parent: parent);

              bool isGoingLeft = false;

              @override
              CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
                return CustomScrollPhysics(parent: buildParent(ancestor));
              }

              @override
              double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
                isGoingLeft = offset.sign < 0;
                return offset;
              }

              @override
              double applyBoundaryConditions(ScrollMetrics position, double value) {
                //print("applyBoundaryConditions");
                assert(() {
                  if (value == position.pixels) {
                    throw FlutterError(
                        '$runtimeType.applyBoundaryConditions() was called redundantly.\n'
                        'The proposed new position, $value, is exactly equal to the current position of the '
                        'given ${position.runtimeType}, ${position.pixels}.\n'
                        'The applyBoundaryConditions method should only be called when the value is '
                        'going to actually change the pixels, otherwise it is redundant.\n'
                        'The physics object in question was:\n'
                        '  $this\n'
                        'The position object in question was:\n'
                        '  $position\n');
                  }
                  return true;
                }());
                if (value < position.pixels && position.pixels <= position.minScrollExtent)
                  return value - position.pixels;
                if (position.maxScrollExtent <= position.pixels && position.pixels < value)
                  // overscroll
                  return value - position.pixels;
                if (value < position.minScrollExtent &&
                    position.minScrollExtent < position.pixels) // hit top edge

                  return value - position.minScrollExtent;

                if (position.pixels < position.maxScrollExtent &&
                    position.maxScrollExtent < value) // hit bottom edge
                  return value - position.maxScrollExtent;

                if (!isGoingLeft) {
                  return value - position.pixels;
                }
                return 0.0;
              }
            }

用法:

            @override
              Widget build(BuildContext context) {
                return Scaffold(
                    appBar: AppBar(),
                    body: PageView.builder(
                      itemCount: 4,
                      physics: CustomScrollPhysics(),
                      itemBuilder: (context, index) => Center(
                            child: Text("Item $index"),
                          ),
                    ));
              }

答案 2 :(得分:1)

为此准备一个更好的方法。

这种方法不需要实例化新的物理实例,并且可以动态更新。

使用功能参数onAttemptDrag允许或拒绝滑动请求。此函数中的代码应该高效,因为每秒将被调用多次(滚动时)。此外,您可能想在此函数中添加一个标志,以允许程序源请求通过。例如,在“下一个按钮”的情况下,此物理实现也将阻止功能jumpTo(..)animateTo的正常工作,因此您需要具有一个标志,该标志将暂时返回默认值true用于特定的页面过渡(如果按下了下一个按钮)。让我知道任何问题或改进方法。

class LockingPageScrollPhysics extends ScrollPhysics {
  /// Requests whether a drag may occur from the page at index "from"
  /// to the page at index "to". Return true to allow, false to deny.
  final Function(int from, int to) onAttemptDrag;

  /// Creates physics for a [PageView].
  const LockingPageScrollPhysics(
      {ScrollPhysics parent, @required this.onAttemptDrag})
      : super(parent: parent);

  @override
  LockingPageScrollPhysics applyTo(ScrollPhysics ancestor) {
    return LockingPageScrollPhysics(
        parent: buildParent(ancestor), onAttemptDrag: onAttemptDrag);
  }

  double _getPage(ScrollMetrics position) {
    if (position is PagePosition) return position.page;
    return position.pixels / position.viewportDimension;
  }

  double _getPixels(ScrollMetrics position, double page) {
    if (position is PagePosition) return position.getPixelsFromPage(page);
    return page * position.viewportDimension;
  }

  double _getTargetPixels(
      ScrollMetrics position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if (velocity < -tolerance.velocity)
      page -= 0.5;
    else if (velocity > tolerance.velocity) page += 0.5;
    return _getPixels(position, page.roundToDouble());
  }

  @override
  double applyBoundaryConditions(ScrollMetrics position, double value) {
    assert(() {
      if (value == position.pixels) {
        throw FlutterError('$runtimeType.applyBoundaryConditions() was called redundantly.\n'
            'The proposed new position, $value, is exactly equal to the current position of the '
            'given ${position.runtimeType}, ${position.pixels}.\n'
            'The applyBoundaryConditions method should only be called when the value is '
            'going to actually change the pixels, otherwise it is redundant.\n'
            'The physics object in question was:\n'
            '  $this\n'
            'The position object in question was:\n'
            '  $position\n');
      }
      return true;
    }());

    /*
     * Handle the hard boundaries (min and max extents)
     * (identical to ClampingScrollPhysics)
     */
    if (value < position.pixels && position.pixels <= position.minScrollExtent) // under-scroll
      return value - position.pixels;
    if (position.maxScrollExtent <= position.pixels && position.pixels < value) // over-scroll
      return value - position.pixels;
    if (value < position.minScrollExtent &&
        position.minScrollExtent < position.pixels) // hit top edge
      return value - position.minScrollExtent;
    if (position.pixels < position.maxScrollExtent &&
        position.maxScrollExtent < value) // hit bottom edge
      return value - position.maxScrollExtent;

    bool left = value < position.pixels;

    int fromPage, toPage;
    double overScroll = 0;

    if (left) {
      fromPage = position.pixels.ceil() ~/ position.viewportDimension;
      toPage = value ~/ position.viewportDimension;

      overScroll = value - fromPage * position.viewportDimension;
      overScroll = overScroll.clamp(value - position.pixels, 0.0);
    } else {
      fromPage =
          (position.pixels + position.viewportDimension).floor() ~/ position.viewportDimension;
      toPage = (value + position.viewportDimension) ~/ position.viewportDimension;

      overScroll = value - fromPage * position.viewportDimension;
      overScroll = overScroll.clamp(0.0, value - position.pixels);
    }

    if (fromPage != toPage && !onAttemptDrag(fromPage, toPage)) {
      return overScroll;
    } else {
      return super.applyBoundaryConditions(position, value);
    }
  }

这是我一直在使用的库,它使用的是经过修改的控制器https://github.com/RobluScouting/FlutterBoardView

答案 3 :(得分:0)

这是@diegoveloper 和@wdavies973 想法的另一个版本。我有必要只能使用一些编程命令进行滚动,而忽略用户手势。与主 Widget 中的一些复杂解决方法不同,我能够使用 ScrollPhysics 行为锁定屏幕以滚动。当用户交互时,applyPhysicsToUserOffsetapplyBoundaryConditions 之前被调用。考虑到这一点,这是我的示例,希望对某人有所帮助。

注意:一旦锁定,就可以使用 controller.animateTo(pageIndex)

浏览页面

/// Custom page scroll physics
// ignore: must_be_immutable
class CustomLockScrollPhysics extends ScrollPhysics {
  /// Lock swipe on drag-drop gesture
  /// If it is a user gesture, [applyPhysicsToUserOffset] is called before [applyBoundaryConditions];
  /// If it is a programming gesture eg. `controller.animateTo(index)`, [applyPhysicsToUserOffset] is not called.
  bool _lock = false;

  /// Lock scroll to the left
  final bool lockLeft;

  /// Lock scroll to the right
  final bool lockRight;

  /// Creates physics for a [PageView].
  /// [lockLeft] Lock scroll to the left
  /// [lockRight] Lock scroll to the right
  CustomLockScrollPhysics({ScrollPhysics parent, this.lockLeft = false, this.lockRight = false})
      : super(parent: parent);

  @override
  CustomLockScrollPhysics applyTo(ScrollPhysics ancestor) {
    return CustomLockScrollPhysics(parent: buildParent(ancestor), lockLeft: lockLeft, lockRight: lockRight);
  }

  @override
  double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
    if ((lockRight && offset < 0) || (lockLeft && offset > 0)) {
      _lock = true;
      return 0.0;
    }

    return offset;
  }

  @override
  double applyBoundaryConditions(ScrollMetrics position, double value) {
    assert(() {
      if (value == position.pixels) {
        throw FlutterError('$runtimeType.applyBoundaryConditions() was called redundantly.\n'
            'The proposed new position, $value, is exactly equal to the current position of the '
            'given ${position.runtimeType}, ${position.pixels}.\n'
            'The applyBoundaryConditions method should only be called when the value is '
            'going to actually change the pixels, otherwise it is redundant.\n'
            'The physics object in question was:\n'
            '  $this\n'
            'The position object in question was:\n'
            '  $position\n');
      }
      return true;
    }());

    /*
     * Handle the hard boundaries (min and max extents)
     * (identical to ClampingScrollPhysics)
     */
    // under-scroll
    if (value < position.pixels && position.pixels <= position.minScrollExtent) {
      return value - position.pixels;
    }
    // over-scroll
    else if (position.maxScrollExtent <= position.pixels && position.pixels < value) {
      return value - position.pixels;
    }
    // hit top edge
    else if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) {
      return value - position.pixels;
    }
    // hit bottom edge
    else if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) {
      return value - position.pixels;
    }

    var isGoingLeft = value <= position.pixels;
    var isGoingRight = value >= position.pixels;
    if (_lock && ((lockLeft && isGoingLeft) || (lockRight && isGoingRight))) {
      _lock = false;
      return value - position.pixels;
    }

    return 0.0;
  }
}

用法示例:


@override
void initState() {
    super.initState();
    controller.tabController = TabController(initialIndex: 0, length: 3, vsync: this);
}

void nextTab() => controller.tabController.animateTo(controller.tabController.index + 1);

@override
void build(BuildContext context)
    return TabBarView(
        controller: controller.tabController,
        physics: CustomLockScrollPhysics(lockLeft: true),
        children: [ InkWell(onTap: nextTab), InkWell(onTap: nextTab), Container() ],
    ),
}

最后一点:我尽一切努力避免在类中使用某些变量,因为 ScrollPhysics 是@immutable,但在这种情况下,这是成功知道输入是否为用户手势的唯一方法。

答案 4 :(得分:0)

如果你决定像我一样使用@Fabricio N. de Godoi 的回答,那就是一个错误。例如,在您阻止向右滚动后,向右滚动将无法按预期工作。但在那之后,如果您滚动相反方向页面将向右滚动。

这就是孤独; 找到这个代码

if (_lock && ((lockLeft && isGoingLeft) || (lockRight && isGoingRight))) {
  _lock = false;
  return value - position.pixels;
}

并替换为

if (_lock && ((lockLeft && isGoingLeft) || (lockRight && isGoingRight))) {
  _lock = false;
  return value - position.pixels;
} else {
  _lock = true;
}