据我所知,Dart不支持字形集群,尽管有人支持它:
在实施之前,我有哪些选择来迭代字形集群?例如,如果我有这样的字符串:
String family = '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}'; // 👨👩👧
String myString = 'Let me introduce my $family to you.';
并且在五码点家族表情符号之后有一个光标:
如何将光标移动到左侧的一个用户感知字符?
(在这种特殊情况下,我知道字形集群的大小,所以我可以做到,但我真正要问的是找到一个任意长的字形集群的长度。)
更新
我从this article看到,Swift使用系统的ICU库。在Flutter中可能有类似的东西。
对于那些想要玩上面的例子的人来说,这是一个演示项目。按钮将光标向右或向左移动。它目前需要按8次按钮才能将光标移动到家族表情符号上。
main.dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Grapheme cluster testing')),
body: BodyWidget(),
),
);
}
}
class BodyWidget extends StatefulWidget {
@override
_BodyWidgetState createState() => _BodyWidgetState();
}
class _BodyWidgetState extends State<BodyWidget> {
TextEditingController controller = TextEditingController(
text: 'Let me introduce my \u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467} to you.'
);
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
controller: controller,
),
Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: Text('<<'),
onPressed: () {
_moveCursorLeft();
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: Text('>>'),
onPressed: () {
_moveCursorRight();
},
),
),
],
)
],
);
}
void _moveCursorLeft() {
int currentCursorPosition = controller.selection.start;
if (currentCursorPosition == 0)
return;
int newPosition = currentCursorPosition - 1;
controller.selection = TextSelection(baseOffset: newPosition, extentOffset: newPosition);
}
void _moveCursorRight() {
int currentCursorPosition = controller.selection.end;
if (currentCursorPosition == controller.text.length)
return;
int newPosition = currentCursorPosition + 1;
controller.selection = TextSelection(baseOffset: newPosition, extentOffset: newPosition);
}
}
更新:使用https://pub.dartlang.org/packages/icu
示例代码:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:icu/icu.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Grapheme cluster testing')),
body: BodyWidget(),
),
);
}
}
class BodyWidget extends StatefulWidget {
@override
_BodyWidgetState createState() => _BodyWidgetState();
}
class _BodyWidgetState extends State<BodyWidget> {
final ICUString icuText = ICUString('Let me introduce my \u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467} to you.\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}');
TextEditingController controller;
_BodyWidgetState() {
controller = TextEditingController(
text: icuText.toString()
);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
controller: controller,
),
Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: Text('<<'),
onPressed: () async {
await _moveCursorLeft();
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: Text('>>'),
onPressed: () async {
await _moveCursorRight();
},
),
),
],
)
],
);
}
void _moveCursorLeft() async {
int currentCursorPosition = controller.selection.start;
if (currentCursorPosition == 0)
return;
int newPosition = await icuText.previousGraphemePosition(currentCursorPosition);
controller.selection = TextSelection(baseOffset: newPosition, extentOffset: newPosition);
}
void _moveCursorRight() async {
int currentCursorPosition = controller.selection.end;
if (currentCursorPosition == controller.text.length)
return;
int newPosition = await icuText.nextGraphemePosition(currentCursorPosition);
controller.selection = TextSelection(baseOffset: newPosition, extentOffset: newPosition);
}
}
原始答案:
在Dart / Flutter完全实现ICU之前,我认为最好的办法是使用PlatformChannel传递Unicode字符串native(iOS Swift4 +或Android Java / Kotlin)来迭代/ manupuliate,然后发回结果。
BreakIterator
替换Oracle的ICU library,效果要好得多。除了import语句之外没有任何变化。我建议使用本机操作(而不是在Dart上执行)的原因是因为Unicode有太多东西需要处理,例如规范化,规范等价,ZWNJ,ZWJ,ZWSP等。
如果您需要一些示例代码,请记下来。
来自TextPainter类的源代码提供了一些如何找到字形集群的线索。具体而言,通过使用零宽度连接器来连接代码点来创建长字形集群,因此您可以使用此知识来搜索字形集群的末尾。
// Unicode value for a zero width joiner character.
static const int _zwjUtf16 = 0x200d;
// Get the Rect of the cursor (in logical pixels) based off the near edge
// of the character upstream from the given string offset.
// TODO(garyq): Use actual extended grapheme cluster length instead of
// an increasing cluster length amount to achieve deterministic performance.
Rect _getRectFromUpstream(int offset, Rect caretPrototype) {
final String flattenedText = _text.toPlainText();
final int prevCodeUnit = _text.codeUnitAt(max(0, offset - 1));
if (prevCodeUnit == null)
return null;
// Check for multi-code-unit glyphs such as emojis or zero width joiner
final bool needsSearch = _isUtf16Surrogate(prevCodeUnit) || _text.codeUnitAt(offset) == _zwjUtf16;
int graphemeClusterLength = needsSearch ? 2 : 1;
List<TextBox> boxes = <TextBox>[];
while (boxes.isEmpty && flattenedText != null) {
final int prevRuneOffset = offset - graphemeClusterLength;
boxes = _paragraph.getBoxesForRange(prevRuneOffset, offset);
// When the range does not include a full cluster, no boxes will be returned.
if (boxes.isEmpty) {
// When we are at the beginning of the line, a non-surrogate position will
// return empty boxes. We break and try from downstream instead.
if (!needsSearch)
break; // Only perform one iteration if no search is required.
if (prevRuneOffset < -flattenedText.length)
break; // Stop iterating when beyond the max length of the text.
// Multiply by two to log(n) time cover the entire text span. This allows
// faster discovery of very long clusters and reduces the possibility
// of certain large clusters taking much longer than others, which can
// cause jank.
graphemeClusterLength *= 2;
continue;
}
final TextBox box = boxes.first;
// If the upstream character is a newline, cursor is at start of next line
const int NEWLINE_CODE_UNIT = 10;
if (prevCodeUnit == NEWLINE_CODE_UNIT) {
return Rect.fromLTRB(_emptyOffset.dx, box.bottom, _emptyOffset.dx, box.bottom + box.bottom - box.top);
}
final double caretEnd = box.end;
final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
return Rect.fromLTRB(min(dx, width), box.top, min(dx, width), box.bottom);
}
return null;
}
此外,here is a file在Flutter的libtxt引擎的minikin库中处理Grapheme集群。我不确定它是否可以直接访问,但它可能对参考有用。