likes
comments
collection
share

Flutter自定义输入框滑动偏移(PC端)

作者站长头像
站长
· 阅读数 41

一、前言

  • PC端使用固定高度多行输入框
  • 鼠标在输入框中滑动滚轮
  • 输入框从第一行直接滑动到了最后一行,导致部分内容查看不到
  • Why?能不能让它滑动偏移小一点、滑动时看清每一行内容?

二、想一想

我们使用滑动Widget的时候,一般都有个ScrollController来控制滑动,那么TextField是不是也有这么个东西来控制滑动呢?查看TextField定义果真有一个scrollController属性。我们知道ScrollController主要作用是:控制滚动位置和监听滚动事件,就很可疑。

三、玩一玩

先写个输入框、随便输入点内容、设置一个ScrollController、再监听一下滚动。

import 'package:flutter/material.dart';
class CustomTextField extends StatefulWidget {
  const CustomTextField({Key? key}) : super(key: key);

  @override
  _CustomTextFieldState createState() => _CustomTextFieldState();
}

class _CustomTextFieldState extends State<CustomTextField> {
  final ScrollController scrollController = ScrollController();
  final TextEditingController controller = TextEditingController();

  @override
  void dispose() {
    controller.dispose();
    textController.dispose();
    super.dispose();
  }
  
  @override
  void initState() {
      super.initState();
      textController.text = '123' * 200;
      controller.addListener(() {
          print('offset:${controller.offer}');
      });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
          width: 200,
          color: Colors.grey,
          constraints: BoxConstraints(maxHeight:500),
          child:TextField(
              maxLines: null,
              controller: controller,
              scrollController: scrollController,
          )));
  }

将鼠标移动到输入框滑动滚轮打印如下:

offset:100.0
offset:200.0
offset:300.0
offset:400.0
offset:500.0
offset:524.0

原来每次滚动的偏移是100,最后一个偏移之所以不是,因为已经到底了;那问题就很明显了:当输入内容占用的高度小于等于100的话岂不就是滚轮滚动一下就到底了。

三、看一看

现在我们已经知道滚轮滚动一次的偏移是100,那么这个100到底是哪来的、怎么修改?既然ScrollController主要作用是控制滚动位置和监听滚动事件,我们就从这里开始看;滚动监听我们已经玩了,接下来就到ScrollContrller定义看看它到底是怎么控制滚动的。

Future<void> animateTo(
    double offset, {
    required Duration duration,
    required Curve curve,
}) async {
    ...
    await Future.wait<void>(<Future<void>>[
        for (int i = 0; i < _positions.length; i += 1) _positions[i].animateTo(offset, duration: duration, curve: curve),
    ]);
}

void jumpTo(double value) {
    ...
    for (final ScrollPosition position in List<ScrollPosition>.of(_positions)) {
        position.jumpTo(value);
    }
}

可以看出ScrollController是通过position来控制滚动的,接下来再看看TextField到底是怎么使用scrollController。在TextField定义文件中搜索发现如下代码:

text_field.dart
@override
Widget build(BuildContext context) {
    ...
    EditableText(
    scrollController: widget.scrollController,
    ...
}

editable_text.dart
class EditableTextState extends ... {
    ...
    ScrollController? _internalScrollController;
    ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());

    @override
    Widget build(BuildContext context) {
        ...
        Scrollable(
            controller: _scrollController,
        ...
    }
}

由上可以看出TextField内部是使用Scrollable来处理滑动手势、确定滑动偏移,而Scrollable也是继承于StatefulWidget,那么继续查看ScrollableState的源码:

class ScrollableState extends ... {
    ScrollPosition get position => _position!;
    ScrollPosition? _position;
    ScrollController get _effectiveScrollController => widget.controller ?? _fallbackScrollController!;
    @override
    void initState() {
        if (widget.controller == null) {
            _fallbackScrollController = ScrollController();
        }
    }
    
    @override
    Widget build(BuildContext context) {
        Widget result = _ScrollableScope(
            scrollable: this,
            position: position,
            child: Listener(
                onPointerSignal: _receivedPointerSignal,
                child: RawGestureDetector(
                ...
                )
        ...
        final ScrollableDetails details = ScrollableDetails(
            ...
            controller: _effectiveScrollController,
            ...
        )
        ...
    }
}

在这里我们可以看到几个点:

  1. ScrollableState持有了TextField的ScrollController
  2. ScrollableState有一个一定存在的_position
  3. Scrollable内部使用了Listener,在触摸发生时回调onPointerSignal

前面我们跟踪ScrollController的时候知道它是通过position来控制滚动的,既然如此再看看这个_position到底是怎么初始化的

void _updatePosition() {
    ...
    _position = _effectiveScrollController.createScrollPosition(_physics!, this, oldPositon)j
    _effectiveScrollController.attach(position);
}

这不就是调用TextField的ScrollController的createScrollPosition来创建position,再调用其attach方法将position加入到scrollController.positions中;此时position初始化虽然知道了,我们还是没看到它到底怎么控制滑动的啊,这时候就需要查看Listener的onPointerSignal回调了:

void _receivedPointerSignal(PointerSignalEvent event) {
    ...
    final double delta = _pointerSignalEventDelta(event);
    final double targetScrollOffset = _targetScrollOffsetForPointerScroll(delta);
    if (delta != 0 && targetScrollOffset != position.pixels) {
        GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
    }
    ...
}

void _handlePointerScroll(PointerEvent event) {
    final double delta = _pointerSignalEventDelta(event);
    final double targetScrollOffset = _targetScrollOffsetForPointerScroll(delta);
    if (delta != 0 && targetScrollOffset != position.pixels) {
        position.pointerScroll(delta);
    }
}

GestureBinding.instance.pointerSignalResolver.register()在干什么我不清楚,看名字像是在注册什么手势绑定,_handlePointerScroll这个名字就有点意思了处理触点滑动?position.pointerScroll(delta)在干啥?我也不知道,既然如此下个断点开启调试大法。

  1. 鼠标滚轮滚动一次,嘿断点来了:delta = 100,targetScrollOffset = 100
  2. 鼠标滚轮滚动第二次:delta = 100,targetScrollOffset = 200
  3. 鼠标滚轮滚动第三次:delta = 100,targetScrollOffset = 300

这么神奇的吗,position原来就是这样控制滚动的、100就是来自这里,索嘎赶紧瞧瞧

double _pointerSignalEventDelta(PointerScrollEvent event) {
    double delta = widget.axis == Axis.horizontal
        ? event.scrollDelta.dx
        : event.scrollDelta.dy;
    
    if (axisDirectionIsReversed(widget.axisDirection)) {
        delta *= -1;
    }
    return delta;
}

到这里已经很明确的可以看出这个100来自PointerScrollEvent是系统回调给我们的,感觉不咋好改,难道自己重写TextField...一直重写到Scrollable?算了算了太复杂。正好还有个pointerScroll没看,继续看看:ScrollPosition我们只有一个可用子类:ScrollPositionWithSingleContext

void pointerScroll(double delta) {
    ...
    didStartScroll();
    didUpdateScrollPositionBy(pixels - oldPixels);
    didEndScroll();
    ...
}

didStartScroll:开始滚动,didUpdateScrollPositionBy:更新滚动位置,didEndScroll:结束滚动。原来这才是position控制滚动位置的地方。

四、改一改

通过pointerScroll()方法我们知道ScrollPositionWithSingleContext并没有给我们提供修改delta的地方,那么我们只好给ScrollPositionWithSingleContext新增一个子类然后再重写pointerScroll()方法;前面我们也看到了position是在ScrollControllercreateScrollPosition方法中创建的,所以我们还需要给ScrollController新增个子类:

class CustomPositionWithSingleContext extends ScrollPositionWithSingleContext {
    CustomPositionWithSingleContext({
        required super.physics,
        required super.context,
        super.initialPixels,
        super.keepScrollOffset,
        super.oldPosition,
        super.debugLabel,
    });
    
    @override
    void pointerScroll(double delta) {
        // 每次滚动10
        delta = delta / delta.abs() * 10;
        super.pointerScroll(delta);
    }
}

class CustomScrollController extends ScrollController {
    @override
    ScrollPosition createScrollPosition(ScrollPhy6sics physics, ScrollContext context, ScrollPosition? oldPosition) {
        return CustomPositionWithSingleContext(
            physics: physics,
            context: context,
            initialPixels: initialScrollOffset,
            keepScrollOffset: keepScrollOffset,
            oldPosition: oldPosition,
            debugLabel: debugLabel,
        );
    }
}

最后只需要在创建TextField的位置将scrollController设置为CustomScrollController就好了。

转载自:https://juejin.cn/post/7389651543740776448
评论
请登录