在 Dart 中处理字素簇
Handling grapheme clusters in Dart
据我所知,Dart 不支持字素簇,尽管有人谈论支持它:
- Dart Strings should support Unicode grapheme cluster operations #34
- Minimal Unicode grapheme cluster support #49
在实现之前,我有哪些迭代字素集群的选项?例如,如果我有这样的字符串:
String family = '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}'; //
String myString = 'Let me introduce my $family to you.';
五码位家族表情符号后面有一个光标:
如何将光标向左移动一个用户感知的字符?
(在这种特殊情况下,我知道字素簇的大小,所以我可以做到,但我真正想问的是找到任意长的字素簇的长度。)
更新
我从 this article that Swift uses the system's 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 字符串原生(iOS Swift4+ 或 Android Java/Kotlin) 到 iterate/manupuliate 那里,然后发回结果。
- 对于 Swift4+,它就像你提到的文章一样开箱即用(不是 Swift3-,不是 ObjC)
- 对于 Java/Kotlin,将 Oracle 的
BreakIterator
替换为 ICU library,效果更好。除了导入语句外没有变化。
我建议使用原生操作(而不是在 Dart 上进行)的原因是因为 Unicode 有太多东西要处理,例如规范化、规范等价、ZWNJ、ZWJ、ZWSP 等
如果您需要一些示例代码,请留下评论。
2020 年更新
使用 Dart 团队的 characters 包。它现在是处理字素簇的官方方式。
使用text.characters
获取字素簇。用户 text.characters.iterator
将它们移到上方。我仍在研究如何将 CharacterRange
转换为 TextSelection
。当我有更多细节时,我会稍后更新这个答案。
注意:这是对我的旧答案的完全重写。详情见编辑历史。
据我所知,Dart 不支持字素簇,尽管有人谈论支持它:
- Dart Strings should support Unicode grapheme cluster operations #34
- Minimal Unicode grapheme cluster support #49
在实现之前,我有哪些迭代字素集群的选项?例如,如果我有这样的字符串:
String family = '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}'; //
String myString = 'Let me introduce my $family to you.';
五码位家族表情符号后面有一个光标:
如何将光标向左移动一个用户感知的字符?
(在这种特殊情况下,我知道字素簇的大小,所以我可以做到,但我真正想问的是找到任意长的字素簇的长度。)
更新
我从 this article that Swift uses the system's 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 字符串原生(iOS Swift4+ 或 Android Java/Kotlin) 到 iterate/manupuliate 那里,然后发回结果。
- 对于 Swift4+,它就像你提到的文章一样开箱即用(不是 Swift3-,不是 ObjC)
- 对于 Java/Kotlin,将 Oracle 的
BreakIterator
替换为 ICU library,效果更好。除了导入语句外没有变化。
我建议使用原生操作(而不是在 Dart 上进行)的原因是因为 Unicode 有太多东西要处理,例如规范化、规范等价、ZWNJ、ZWJ、ZWSP 等
如果您需要一些示例代码,请留下评论。
2020 年更新
使用 Dart 团队的 characters 包。它现在是处理字素簇的官方方式。
使用text.characters
获取字素簇。用户 text.characters.iterator
将它们移到上方。我仍在研究如何将 CharacterRange
转换为 TextSelection
。当我有更多细节时,我会稍后更新这个答案。
注意:这是对我的旧答案的完全重写。详情见编辑历史。