内容简介:上一篇关于关于怎么在文字里面加入图片,在这篇文章里面我就不再介绍了,有兴趣的同学可以先看一下Extended Text,原理是一毛一样的。
extended_text_field 相关文章
上一篇关于 extended_text_field 的文章主要介绍下用法,这篇文章介绍下,实现的过程。
过程
文字中插入图片
关于怎么在文字里面加入图片,在这篇文章里面我就不再介绍了,有兴趣的同学可以先看一下Extended Text,原理是一毛一样的。
键盘与输入框的关联
我写的好多组件都是对官方组件的扩展,所以对官方源码一定要读懂,知道它是做什么用的,才能在这个基础上扩展自己的功能。
除了 工具 类,其他都是从官方那边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的输入框跟键盘是怎么进行交互的了,总结一下,
- 键盘通过TextInputConnection,执行3个方法传递变化给输入框
/// 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); 复制代码
- 输入框通过TextInputConnection,也可以把TextEditingValue传递给键盘,
/// 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; } 复制代码
当光标位置处于表情文字中间的时候,我们把光标移动到表情的后面去,并且通知键盘,光标位置变化了。这样我们再继续输入的时候,就没有问题了。
- getFullHeightForCaret api在低版本不支持
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.
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Flutter 支持图片以及特殊文字的输入框(一)使用方法
- canvas压缩图片以及卡片制作
- 网页图片等比例缩小实现方案总结以及最佳实践
- 详解Apache配置图片防盗链以及隐藏版本信息
- itchat 保存好友信息以及生成好友头像图片墙
- 图片上传姿势以及你不知道的 Typed Arrays
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
互联网浪尖上的女性
田玉翠 / 人民出版社 / 2017-1 / 68.00
二十三个真实、前沿的女性创业者实例,带你走进“她世界”洞悉“她经济”。《互联网浪尖上的女性》不仅仅关于创业,更是关于女性如何追逐自己的梦想,让人生更丰满、更精彩。一起来看看 《互联网浪尖上的女性》 这本书的介绍吧!