在使用Flutter 输入框TextField组件时,一直好奇组件是如何定位到光标位置,在学习了解TextField组件源码后,对这个问题有初步了解。
查看
TextField 源码 _CupertinoTextFieldState build方法
- return Semantics(
- enabled: enabled,
- onTap: !enabled || widget.readOnly ? null : () {
- if (!controller.selection.isValid) {
- controller.selection = TextSelection.collapsed(offset: controller.text.length);
- }
- _requestKeyboard();
- },
- onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
- child: IgnorePointer(
- ignoring: !enabled,
- child: Container(
- decoration: effectiveDecoration,
- color: !enabled && effectiveDecoration == null ? disabledColor : null,
- child: _selectionGestureDetectorBuilder.buildGestureDetector(
- behavior: HitTestBehavior.translucent,
- child: Align(
- alignment: Alignment(-1.0, _textAlignVertical.y),
- widthFactor: 1.0,
- heightFactor: 1.0,
- child: _addTextDependentAttachments(paddedEditable, textStyle, placeholderStyle),
- ),
- ),
- ),
- ),
- );
该build方法中使用了_selectionGestureDetectorBuilder.buildGestureDetector方法,这里是一个手势识别器。也就是这个方法里面监听处理鼠标点击(Web中使用)和点击手势。
- Widget buildGestureDetector({
- Key? key,
- HitTestBehavior? behavior,
- required Widget child,
- }) {
- return TextSelectionGestureDetector(
- key: key,
- onTapDown: onTapDown,
- onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
- onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
- onSecondaryTap: onSecondaryTap,
- onSecondaryTapDown: onSecondaryTapDown,
- onSingleTapUp: onSingleTapUp,
- onSingleTapCancel: onSingleTapCancel,
- onSingleLongTapStart: onSingleLongTapStart,
- onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
- onSingleLongTapEnd: onSingleLongTapEnd,
- onDoubleTapDown: onDoubleTapDown,
- onDragSelectionStart: onDragSelectionStart,
- onDragSelectionUpdate: onDragSelectionUpdate,
- onDragSelectionEnd: onDragSelectionEnd,
- behavior: behavior,
- child: child,
- );
- }
这里我们重点查看单击onSingleTapUp方法。
- void onSingleTapUp(TapUpDetails details) {
- if (_isShiftTapping) {
- _isShiftTapping = false;
- return;
- }
-
- if (delegate.selectionEnabled) {
- switch (defaultTargetPlatform) {
- case TargetPlatform.iOS:
- case TargetPlatform.macOS:
- switch (details.kind) {
- case PointerDeviceKind.mouse:
- case PointerDeviceKind.stylus:
- case PointerDeviceKind.invertedStylus:
- // Precise devices should place the cursor at a precise position.
- renderEditable.selectPosition(cause: SelectionChangedCause.tap);
- break;
- case PointerDeviceKind.touch:
- case PointerDeviceKind.unknown:
- // On macOS/iOS/iPadOS a touch tap places the cursor at the edge
- // of the word.
- renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
- break;
- }
- break;
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- case TargetPlatform.linux:
- case TargetPlatform.windows:
- renderEditable.selectPosition(cause: SelectionChangedCause.tap);
- break;
- }
- }
- }
重点方法 renderEditable.selectPosition(cause: SelectionChangedCause.tap);
- void selectPosition({ required SelectionChangedCause cause }) {
- selectPositionAt(from: _lastTapDownPosition!, cause: cause);
- }
-
- /// Select text between the global positions [from] and [to].
- ///
- /// [from] corresponds to the [TextSelection.baseOffset], and [to] corresponds
- /// to the [TextSelection.extentOffset].
- void selectPositionAt({ required Offset from, Offset? to, required SelectionChangedCause cause }) {
- assert(cause != null);
- assert(from != null);
- _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
- final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
- final TextPosition? toPosition = to == null
- ? null
- : _textPainter.getPositionForOffset(globalToLocal(to - _paintOffset));
-
- final int baseOffset = fromPosition.offset;
- final int extentOffset = toPosition?.offset ?? fromPosition.offset;
-
- final TextSelection newSelection = TextSelection(
- baseOffset: baseOffset,
- extentOffset: extentOffset,
- affinity: fromPosition.affinity,
- );
- _setSelection(newSelection, cause);
- }
selectPositionAt 这个方法中,通过点击位置计算判断光标应该显示在文本第几个字符。
细节:
final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
这里用到了TextPainer.getPositionForOffset方法
- /// Returns the position within the text for the given pixel offset.
- TextPosition getPositionForOffset(Offset offset) {
- assert(!_debugNeedsLayout);
- return _paragraph!.getPositionForOffset(offset);
- }
这个方法就是根据传入的offset计算在字符第几个字符中。
计算好后生成一个TextSelection对象,传递给 RenderEditable
- final TextSelection newSelection = TextSelection(
- baseOffset: baseOffset,
- extentOffset: extentOffset,
- affinity: fromPosition.affinity,
- );
class _Editable extends MultiChildRenderObjectWidget {}
_Editable 也就是输入框重点的RenderObject,主要负责绘制工作
- @override
- void updateRenderObject(BuildContext context, RenderEditable renderObject) {
- renderObject
- ///省略
- ..selection = value.selection
- ///省略
- }
这里的value.selection 也就是上面的TextSelection对象。