Flutter自定义输入框滑动偏移(PC端)
一、前言
- 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,
...
)
...
}
}
在这里我们可以看到几个点:
- ScrollableState持有了TextField的ScrollController
- ScrollableState有一个一定存在的_position
- 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)
在干啥?我也不知道,既然如此下个断点开启调试大法。
- 鼠标滚轮滚动一次,嘿断点来了:delta = 100,targetScrollOffset = 100
- 鼠标滚轮滚动第二次:delta = 100,targetScrollOffset = 200
- 鼠标滚轮滚动第三次: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是在ScrollController
的createScrollPosition
方法中创建的,所以我们还需要给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