extended_text_field 相关文章git
上一篇关于extended_text_field的文章主要介绍下用法,这篇文章介绍下,实现的过程。github
关于怎么在文字里面加入图片,在这篇文章里面我就再也不介绍了,有兴趣的同窗能够先看一下Extended Text,原理是一毛同样的。ide
我写的好多组件都是对官方组件的扩展,因此对官方源码必定要读懂,知道它是作什么用的,才能在这个基础上扩展本身的功能。svg
除了工具类,其余都是从官方那边copy过来,而后进行修改的。
咱们先打开extended_editable_text.dart
能够看到它是继承这个TextInputClient的,而TextInputClient是一个抽象类,而TextInputConnection是键盘的通讯的关键先生,它将键盘的动做反馈给TextInputClient,咱们顺便来看看它的实现。
class TextInputConnection {
TextInputConnection._(this._client)
: assert(_client != null),
_id = _nextId++;
static int _nextId = 1;
final int _id;
final TextInputClient _client;
/// Whether this connection is currently interacting with the text input control.
bool get attached => _clientHandler._currentConnection == this;
/// Requests that the text input control become visible.
void show() {
assert(attached);
SystemChannels.textInput.invokeMethod<void>('TextInput.show');
}
/// Requests that the text input control change its internal state to match the given state.
void setEditingState(TextEditingValue value) {
assert(attached);
SystemChannels.textInput.invokeMethod<void>(
'TextInput.setEditingState',
value.toJSON(),
);
}
/// Stop interacting with the text input control.
///
/// After calling this method, the text input control might disappear if no
/// other client attaches to it within this animation frame.
void close() {
if (attached) {
SystemChannels.textInput.invokeMethod<void>('TextInput.clearClient');
_clientHandler
.._currentConnection = null
.._scheduleHide();
}
assert(!attached);
}
}
复制代码
能够看到3里面的几个方法都有调用 SystemChannels.textInput.invokeMethod
这种代码是否是很熟悉,methodchannel,用过的人都知道,能够跟原生进行交互,那么就很简单了。
text field会在点击的时候得到焦点,而且打开键盘的连接,这样就能够接受到键盘的响应,那么原生反馈Flutter是在哪里呢,是在_TextInputClientHandler _clientHandler这个里面. 咱们也看看_TextInputClientHandler里面的代码
class _TextInputClientHandler {
_TextInputClientHandler() {
SystemChannels.textInput.setMethodCallHandler(_handleTextInputInvocation);
}
TextInputConnection _currentConnection;
Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
if (_currentConnection == null)
return;
final String method = methodCall.method;
final List<dynamic> args = methodCall.arguments;
final int client = args[0];
// The incoming message was for a different client.
if (client != _currentConnection._id)
return;
switch (method) {
case 'TextInputClient.updateEditingState':
_currentConnection._client.updateEditingValue(TextEditingValue.fromJSON(args[1]));
break;
case 'TextInputClient.performAction':
_currentConnection._client.performAction(_toTextInputAction(args[1]));
break;
case 'TextInputClient.updateFloatingCursor':
_currentConnection._client.updateFloatingCursor(_toTextPoint(_toTextCursorAction(args[1]), args[2]));
break;
default:
throw MissingPluginException();
}
}
bool _hidePending = false;
void _scheduleHide() {
if (_hidePending)
return;
_hidePending = true;
// Schedule a deferred task that hides the text input. If someone else
// shows the keyboard during this update cycle, then the task will do
// nothing.
scheduleMicrotask(() {
_hidePending = false;
if (_currentConnection == null)
SystemChannels.textInput.invokeMethod<void>('TextInput.hide');
});
}
}
final _TextInputClientHandler _clientHandler = _TextInputClientHandler();
复制代码
又是跟methodchannel一毛同样,能够监听原生的回调,其实啊,SystemChannels.textInput就是一个methodchannel
从上面代码咱们看到。若是进行了键盘输入,那么原生会通知flutter去updateEditingValue,而且把这个时候的数值转递过来
case 'TextInputClient.updateEditingState':
_currentConnection._client.updateEditingValue(TextEditingValue.fromJSON(args[1]));
break;
复制代码
这个值是结构是TextEditingValue,它包括了文本,光标(选中)位置,以及composing(个人理解是,好比中文输入的时候是字母,而后下面有下划线,只有当输入完毕选择的时候才会显示成中文)
/// The current text being edited.
final String text;
/// The range of text that is currently selected.
final TextSelection selection;
/// The range of text that is still being composed.
final TextRange composing;
复制代码
如今咱们知道flutter的输入框跟键盘是怎么进行交互的了,总结一下,
/// Requests that this client update its editing state to the given value.
void updateEditingValue(TextEditingValue value);
/// Requests that this client perform the given action.
void performAction(TextInputAction action);
/// Updates the floating cursor position and state.
void updateFloatingCursor(RawFloatingCursorPoint point);
复制代码
/// Requests that the text input control change its internal state to match the given state.
void setEditingState(TextEditingValue value)
/// Requests that the text input control become visible.
void show()
/// Stop interacting with the text input control.
///
/// After calling this method, the text input control might disappear if no
/// other client attaches to it within this animation frame.
void close()
复制代码
接下来咱们移动到buildTextSpan 方法
/// Builds [TextSpan] from current editing value.
///
/// By default makes text in composing range appear as underlined.
/// Descendants can override this method to customize appearance of text.
TextSpan buildTextSpan(BuildContext context)
复制代码
能够看到这里是将TextEditingValue转换为了TextSpan,那么咱们的机会是否是就来了,咱们能够在这里经过SpecialTextSpanBuilder,把TextEditingValue的值转换为咱们想要的特殊的TextSpan.
TextSpan buildTextSpan(BuildContext context) {
if (!widget.obscureText && _value.composing.isValid) {
final TextStyle composingStyle = widget.style.merge(
const TextStyle(decoration: TextDecoration.underline),
);
var beforeText = _value.composing.textBefore(_value.text);
var insideText = _value.composing.textInside(_value.text);
var afterText = _value.composing.textAfter(_value.text);
if (supportSpecialText) {
var before = widget.specialTextSpanBuilder
.build(beforeText, textStyle: widget.style);
var after = widget.specialTextSpanBuilder
.build(afterText, textStyle: widget.style);
List<TextSpan> children = List<TextSpan>();
if (before != null && before.children != null) {
_createImageConfiguration(<TextSpan>[before], context);
before.children.forEach((sp) {
children.add(sp);
});
} else {
children.add(TextSpan(text: beforeText));
}
children.add(TextSpan(
style: composingStyle,
text: insideText,
));
if (after != null && after.children != null) {
_createImageConfiguration(<TextSpan>[after], context);
after.children.forEach((sp) {
children.add(sp);
});
} else {
children.add(TextSpan(text: afterText));
}
return TextSpan(style: widget.style, children: children);
}
return TextSpan(style: widget.style, children: <TextSpan>[
TextSpan(text: beforeText),
TextSpan(
style: composingStyle,
text: insideText,
),
TextSpan(text: afterText),
]);
}
String text = _value.text;
if (widget.obscureText) {
text = RenderEditable.obscuringCharacter * text.length;
final int o =
_obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null;
if (o != null && o >= 0 && o < text.length)
text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1));
}
if (supportSpecialText) {
var specialTextSpan =
widget.specialTextSpanBuilder?.build(text, textStyle: widget.style);
if (specialTextSpan != null) {
_createImageConfiguration(<TextSpan>[specialTextSpan], context);
return specialTextSpan;
}
}
return TextSpan(style: widget.style, text: text);
}
复制代码
根据官方的源码,我对各类状况进行了处理,而且经过SpecialTextSpanBuilder将文本转换了咱们想要的TextSpan,为绘制作好准备。
拿到TextSpan,那么下一步,咱们就要准备去绘制文字了,咱们去看看 extended_render_editable.dart
大概看了下源码,就感受跟extended text 里面的extended_render_paragraph差异不大,区别是输入框增长了对光标,以及选中背景的绘制。
那么套路都是同样,找到_paintContents方法,咱们将在这里绘制图片以及一些特殊文本。
源码的绘制顺序是 选中背景,光标,文本(固然根据平台不一样,光标和文本顺序也不一样),
修改以后 绘制顺序为 选中背景,特殊文本(图片等),光标,文本(固然根据平台不一样,光标和文本顺序也不一样)
移动到_paintSpecialText方法中,跟Extended Text同样,支持图片和自定义背景2种特殊文本,区别只是我只遍历children,不会再到children的children里面去找特殊文本了
void _paintSpecialText(PaintingContext context, Offset offset) {
if (!handleSpecialText) return;
final Canvas canvas = context.canvas;
canvas.save();
///move to extended text
canvas.translate(offset.dx, offset.dy);
///we have move the canvas, so rect top left should be (0,0)
final Rect rect = Offset(0.0, 0.0) & size;
_paintSpecialTextChildren(text.children, canvas, rect);
canvas.restore();
}
void _paintSpecialTextChildren(
List<TextSpan> textSpans, Canvas canvas, Rect rect,
{int textOffset: 0}) {
if (textSpans == null) return;
for (TextSpan ts in textSpans) {
Offset topLeftOffset = getOffsetForCaret(
TextPosition(offset: textOffset),
rect,
);
//skip invalid or overflow
if (topLeftOffset == null ||
(textOffset != 0 && topLeftOffset == Offset.zero)) {
return;
}
if (ts is ImageSpan) {
///imageSpanTransparentPlaceholder \u200B has no width, and we define image width by
///use letterSpacing,so the actual top-left offset of image should be subtract letterSpacing(width)/2.0
Offset imageSpanOffset = topLeftOffset -
Offset(getImageSpanCorrectPosition(ts, textDirection), 0.0);
if (!ts.paint(canvas, imageSpanOffset)) {
//image not ready
ts.resolveImage(
listener: (ImageInfo imageInfo, bool synchronousCall) {
if (synchronousCall)
ts.paint(canvas, imageSpanOffset);
else {
if (owner == null || !owner.debugDoingPaint) {
markNeedsPaint();
}
}
});
}
} else if (ts is BackgroundTextSpan) {
var painter = ts.layout(_textPainter);
Rect textRect = topLeftOffset & painter.size;
Offset endOffset;
if (textRect.right > rect.right) {
int endTextOffset = textOffset + ts.toPlainText().length;
endOffset = _findEndOffset(rect, endTextOffset);
}
ts.paint(canvas, topLeftOffset, rect,
endOffset: endOffset, wholeTextPainter: _textPainter);
}
// else if (ts.children != null) {
// _paintSpecialTextChildren(ts.children, canvas, rect,
// textOffset: textOffset);
//
}
textOffset += ts.toPlainText().length;
}
}
复制代码
咱们处理了关联,绘制,最后咱们须要处理光标以及交互。
咱们把眼光移动到extended_text_selection.dart
ExtendedTextSelectionOverlay
跟它的名字同样,它是OverlayEntry,主要是负责显示那个 好比(copy,paste,select all)这种菜单的。
眼光再次移动到 extended_text_field.dart
这个里面定义不少交互,它们有的用来移动光标,有的用来选中文本,有的用来选中整个word。
child: IgnorePointer(
ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
child: TextSelectionGestureDetector(
onTapDown: _handleTapDown,
onForcePressStart:
forcePressEnabled ? _handleForcePressStarted : null,
onSingleTapUp: _handleSingleTapUp,
onSingleTapCancel: _handleSingleTapCancel,
onSingleLongTapStart: _handleSingleLongTapStart,
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
onSingleLongTapEnd: _handleSingleLongTapEnd,
onDoubleTapDown: _handleDoubleTapDown,
onDragSelectionStart: _handleMouseDragSelectionStart,
onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
behavior: HitTestBehavior.translucent,
child: child,
),
),
复制代码
关键的点来了,由于咱们把文本转换为了特殊TextSpan,致使其实绘制的文字跟实际文本是不同的,好比对于图片,以前它是"[1]"文本,但在绘制的时候它其实只是"",一个空的占位符号。
再详细点的例子就是,好比我点击在一个表情的后面,对于TextPainter来讲,它告诉你的位置1,可是对于真实文原本说,它的位置应该是3.
咱们使用的真实值以及键盘的值是用TextEditingValue 来保存的,而咱们绘画文本是用TextSpan以及TextPainter来进行计算的,因此咱们须要给他们2者之间来一个转换,让咱们把目光移动到extended_text_field_utils.dart
在这个里面,我写了双方进行转换的方法,他们是如下方法
TextPosition convertTextInputPostionToTextPainterPostion(
TextSpan text, TextPosition textPosition)
TextSelection convertTextInputSelectionToTextPainterSelection(
TextSpan text, TextSelection selection)
TextPosition convertTextPainterPostionToTextInputPostion(
TextSpan text, TextPosition textPosition)
TextSelection convertTextPainterSelectionToTextInputSelection(
TextSpan text, TextSelection selection)
复制代码
其实道理很简单,就是双方文字的差别就是这个光标表示方法的差别,就像上面的例子,"[1]" 和 ""之间差距是2,这就会致使它们表示的光标位置差距也是2,根据这个原理咱们就能够把它们进行互相的转换了。
感兴趣的同窗能够去看看代码,若是有更优化的解放,请告诉我一下,谢谢。
由于ImageSpan的作法是使用\u200B(ZERO WIDTH SPACE,就是宽带为0的空白),而使用letterSpacing看成宽度,因此经过 TextPainter计算出来的位置,是在letterSpacing的中间,图片绘画的地方应该要向前移动width / 2.0。也就是说若是光标在图片前,要向前移动width / 2.0。若是光标在图片以后,要向后移动width / 2.0。 对于选中背景也是一样的道理。
// zmt
double imageTextSpanWidth = 0.0;
Offset imageSpanEndCaretOffset;
if (handleSpecialText) {
var textSpan = text.getSpanForPosition(textPosition);
if (textSpan != null) {
if (textSpan is ImageSpan) {
if (textInputPosition.offset >= textSpan.start &&
textInputPosition.offset < textSpan.end) {
imageTextSpanWidth -=
getImageSpanCorrectPosition(textSpan, textDirection);
} else if (textInputPosition.offset == textSpan.end) {
///_textPainter.getOffsetForCaret is not right.
imageSpanEndCaretOffset = _textPainter.getOffsetForCaret(
TextPosition(
offset: textPosition.offset - 1,
affinity: textPosition.affinity),
effectiveOffset & size,
) +
Offset(
getImageSpanCorrectPosition(textSpan, textDirection), 0.0);
}
}
} else {
//handle image text span is last one, textPainter will get wrong offset
//last one
textSpan = text.children?.last;
if (textSpan != null && textSpan is ImageSpan) {
imageSpanEndCaretOffset = _textPainter.getOffsetForCaret(
TextPosition(
offset: textPosition.offset - 1,
affinity: textPosition.affinity),
effectiveOffset & size,
) +
Offset(getImageSpanCorrectPosition(textSpan, textDirection), 0.0);
}
}
}
final Offset caretOffset = (imageSpanEndCaretOffset ??
_textPainter.getOffsetForCaret(textPosition, _caretPrototype) +
Offset(imageTextSpanWidth, 0.0)) +
effectiveOffset;
复制代码
由于支持手动输入也要转换特殊文本,因此存在这种状况。
我先输入了[],再把光标移动到中间,输入1,这个时候会转换为表情1,可是光标没有停留在表情以后,若是你这个时候再输入,它就会在1后面增长。对于这种状况,咱们要作一下处理。
///correct caret Offset
///make sure caret is not in image span
TextEditingValue correctCaretOffset(TextEditingValue value, TextSpan textSpan,
TextInputConnection textInputConnection) {
if (value.selection.isValid && value.selection.isCollapsed) {
int caretOffset = value.selection.extentOffset;
var imageSpans = textSpan.children.where((x) => x is ImageSpan);
//correct caret Offset
//make sure caret is not in image span
for (ImageSpan ts in imageSpans) {
if (caretOffset > ts.start && caretOffset < ts.end) {
//move caretOffset to end
caretOffset = ts.end;
break;
}
}
///tell textInput caretOffset is changed.
if (caretOffset != value.selection.baseOffset) {
value = value.copyWith(
selection: value.selection
.copyWith(baseOffset: caretOffset, extentOffset: caretOffset));
textInputConnection?.setEditingState(value);
}
}
return value;
}
复制代码
当光标位置处于表情文字中间的时候,咱们把光标移动到表情的后面去,而且通知键盘,光标位置变化了。这样咱们再继续输入的时候,就没有问题了。
TextPainter的getFullHeightForCaret 在低版本上面不支持,若是你是适合的版本建议打开下面的注释,这样光标的高度会更舒服。
///zmt
///1.5.7
///under lower version of flutter, getFullHeightForCaret is not support
///
// Override the height to take the full height of the glyph at the TextPosition
// when not on iOS. iOS has special handling that creates a taller caret.
// TODO(garyq): See the TODO for _getCaretPrototype.
// if (defaultTargetPlatform != TargetPlatform.iOS &&
// _textPainter.getFullHeightForCaret(textPosition, _caretPrototype) !=
// null) {
// caretRect = Rect.fromLTWH(
// caretRect.left,
// // Offset by _kCaretHeightOffset to counteract the same value added in
// // _getCaretPrototype. This prevents this from scaling poorly for small
// // font sizes.
// caretRect.top - _kCaretHeightOffset,
// caretRect.width,
// _textPainter.getFullHeightForCaret(textPosition, _caretPrototype),
// );
// }
复制代码
当这5个都介绍完毕的时候,咱们就讲的差很少了,为了方便你们查看我修改的地方,你只须要搜索 zmt ,就能快速找到我为支持扩展功能而添加的代码了。
最后放上 extended_text_field,若是你有什么不明白或者对这个方案有什么改进的地方,请告诉我,欢迎加入Flutter Candies,一块儿生产可爱的Flutter 小糖果(QQ群:181398081)
最最后放上Flutter Candies全家桶,真香。
custom flutter candies(widgets) for you to easily build flutter app, enjoy it.