flutter输入框使用的是TextField,可以通过设置它的属性给输入框和内部文字设置不同的样式,之前看了一个日记APP,是这样的:
可以给输入的文字分别设置样式。
于是就想用flutter去实现这样的效果。查了一下资料发现,TextField中有controller属性,需要传入TextEditingController,它可以去监听text的一些变化以及设置TextField中光标的位置等,最重要的是,可以自己写子类继承TextEditingController重写buildTextSpan方法,该方法返回TextSpan组件,flutter中的富文本组件RichText就是使用TextSpan来实现的。TextSpan中有children,我们只需要在buildTextSpan方法中拼接children,给children中的元素配置不同的TextStyle就可以实现这样的效果。
如这里写了一个子类重写了buildTextSpan方法,在这个方法中,通过每次text的变化,更新文本中的样式段数组,通过样式段数组组成TextSpan来绘制输入框文本。
class RichTextEditingController extends TextEditingController {
TextStyle _curStyle = TextStyle();
void setCurStyle(TextStyle style) {
_curStyle = style;
}
TextSegments configs = TextSegments();
@override
TextSpan buildTextSpan(
{required BuildContext context,
TextStyle? style,
required bool withComposing}) {
assert(!value.composing.isValid ||
!withComposing ||
value.isComposingRangeValid);
//给TextSegments配置数据更新replacements
configs.config(
selectRange: value.selection,
editingRange: value.composing,
text: value.text,
style: _curStyle);
List<InlineSpan> children = [];
//拼接
for (TextSegment element in configs.replacements) {
TextSpan span = TextSpan(text: element.text, style: element.style);
children.add(span);
}
print(
"selectRange: {${value.selection.start},${value.selection.end}}, text: ${text},{${configs.replacements}}");
return TextSpan(children: children);
}
}
TextSegments中的replacements是TextSegment数组,TextSegment中记录了每个文本样式段的文本信息和文本坐标范围:
class TextSegment {
TextRange range;
TextStyle style;
String text;
TextSegment({required this.range, required this.style, required this.text});
//自己在不在选择的区域中
bool inRange(TextRange selectRange) {
if (text.length <= 1) return false;
return rangeContain(range, selectRange.start) ||
rangeContain(range, selectRange.end);
}
省略的代码.....
}
TextSegments通过text的改变对内部的样式段数组进行增删改操作:
class TextSegments {
List<TextSegment> replacements = [];
String oldText = "";
//上一次光标选中的区域
TextRange selectRange = TextRange(start: -1, end: -1);
TextRange editingRange = TextRange(start: -1, end: -1);
void config(
{required TextRange selectRange,
required TextRange editingRange,
required String text,
required TextStyle style}) {
if (oldText == text) {
return;
}
//删除
if (text.length < oldText.length) {
delete(selectRange, editingRange, text);
}
//直接append到最后面
else if (selectRange.start > oldText.length || replacements.length == 0) {
append(selectRange, editingRange, text, style);
}
//从中间插入
else {
insert(selectRange, editingRange, text, style);
}
oldText = text;
}
.....下面省略代码.....
}
对replacements进行删除和插入的时候,一定要重新更新插入和删除之后的元素的位置信息TextRange。如删除的时候:
void _deleteWithStart(int start) {
int idx = -1;
for (var i = 0; i < replacements.length; i++) {
TextSegment element = replacements[i];
if (element.removeWithStart(start)) {
idx = i;
} else if (idx != -1) {
//更新后面元素的位置信息
element.range = TextRange(
start: element.range.start - 1, end: element.range.end - 1);
}
}
if (idx != -1 && replacements[idx].isEmpty()) {
replacements.removeAt(idx);
}
}
插入的时候:
void insert(TextRange selectRange, TextRange editingRange, String text,
TextStyle style) {
int insertIndex = -1;
....省略代码,找到需要插入的位置insertIndex....
//找到需要将添加的文字插入到replacements的某个元素中
for(){
... insertIndex = ...
}
//找到需要将添加的文字插入到replacements的某个元素后面
if(insertIndex = -1) {
for(){
... insertIndex = ...
}
}
//都没找见,直接append到最后面
if (insertIndex == -1) {
append(selectRange, editingRange, text, style);
return;
}
//将添加的文字加入进来
TextRange insertR = TextRange(
start: selectRange.start,
end: selectRange.start + (text.length - oldText.length));
TextSegment newT = TextSegment(
range: insertR,
style: style,
text: text.substring(insertR.start, insertR.end));
list.insert(insertIndex + 1, newT);
//插入的range后面的元素,range后移
for (var i = insertIndex + 2; i < replacements.length; i++) {
TextSegment element = replacements[i];
element.range = TextRange(
start: element.range.start + length, end: element.range.end + length);
}
replacements = list;
_connect(); //将相同style切相邻的元素进行合并
}
最后简单的实现效果:
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END