在使用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对象。