Từ affinity có nghĩa là thu hút. Điều này đề cập đến phần nào của text mà con trỏ (hoặc dấu mũ đôi khi được gọi) được thu hút đến. Để giải thích điều đó, trước tiên cần nói về vị trí text, được đại diện bởi class TextPosition.
Text position
TextPosition có thuộc tính offset được sử dụng cho vị trí con trỏ hoặc text selection.
Left-to-right text (Text từ trái sang phải)
Lấy ví dụ từ Hello. Trong hình ảnh bên dưới, bạn có thể thấy vị trí của con trỏ đối với mỗi offset của vị trí text:
Text tiếng Anh được viết từ trái sang phải, nhưng các ngôn ngữ như tiếng Ả Rập và tiếng Do Thái được viết từ phải sang trái.
Right-to-left text (Text từ phải sang trái)
Nếu bạn sử dụng từ He trong tiếng Do Thái, sự khác biệt sẽ đi theo hướng ngược lại với tiếng Anh. Đó là, họ bắt đầu ở bên phải và di chuyển sang trái:
Line wrapping
Bây giờ, giả sử bạn có text 0123456789. Nếu bạn đặt offset vị trí text thành 9
như thế này (full code ở cuối bài viết):
TextPosition(offset: 9),
bạn sẽ nhận được kết quả sau:
Đó chính là thứ tự từ trái sang phải mà bạn đã thấy với Hello.
Tất cả điều này đều rất hay, nhưng điều gì sẽ xảy ra nếu bạn không có đủ chỗ để xếp text trên một dòng và ký tự cuối cùng đưa xuống dòng tiếp theo như vậy:
Bây giờ nếu bạn đặt vị trí text ở offset 9
, con trỏ sẽ đi đâu? Nó đi sau số 8 trên dòng đầu tiên hay trước số 9 trên dòng thứ hai?
Chà, hóa ra mặc định là đi trước số 9 ở đầu dòng thứ hai:
Nhưng nếu bạn muốn đặt con trỏ sau số 8 trên dòng đầu tiên thì sao? Sẽ thực sự khó chịu nếu bạn đang cố gắng nhập một thứ gì đó và bạn không thể làm được điều đó. Đây là nơi xuất hiện text affinity.
Downstream text affinity
Text affinity có thể upstream hoặc downstream. Vì text tiếng Anh (và các số như 0123456789) chảy như sông từ trái sang phải, downstream có nghĩa là ký tự tiếp theo bên phải. Khi con trỏ ở giữa 8 và 9, ký tự downstream là 9. Và vì downstream là mặc định, đó là lý do tại sao con trỏ ở dòng thứ hai:
Upstream text affinity
Tuy nhiên, cũng có thể đặt affinity thành upstream như sau:
TextPosition(offset: 9, affinity: TextAffinity.upstream),
Bây giờ khi con trỏ dao động giữa 8 và 9, upstream có nghĩa là nó đi ngược lại hướng tự nhiên (trong trường hợp này là từ phải sang trái) và chọn 8 như vậy:
Tất nhiên, nếu tất cả text có thể nằm gọn trên một dòng thì không thành vấn đề nếu affinity là upstream hay downstream. Con trỏ kết thúc ở cùng một nơi theo một trong hai cách:
Bidirectional text (Text hai chiều)
Một tình huống khác mà bạn cần text affinity là chọn đúng vị trí con trỏ trong text hai chiều. Như đã đề cập, text tiếng Anh được viết từ trái sang phải (LTR) trong khi các ngôn ngữ như tiếng Ả Rập và tiếng Do Thái được viết từ phải sang trái (RTL). Văn bản hai chiều xảy ra khi text LTR và RTL được sử dụng cùng nhau trong cùng một string.
Lấy string sau làm ví dụ:
const text = 'Helloשלום';
Hãy thử từ từ chọn phần bên trong dấu ngoặc kép và để ý cách trình duyệt của bạn xử lý văn bản hai chiều. Đôi khi có thể là một thách thức để có được lựa chọn chính xác.
Downstream affinity
Nếu bạn lặp qua tất cả các offset vị trí text bằng cách sử dụng affinity của TextAffinity.downsream
(mặc định), thì bạn sẽ nhận được các vị trí con trỏ sau:
Bạn nghĩ rằng bạn có thể chọn offset 5
để đi đến phần cuối của Hello, nhưng điều đó không đúng vì affinity là downstream. Khi bạn ở phía bên phải của Hello, bạn phải đi upstream để đến o. Thay vào đó, downstream có nghĩa là con trỏ có affinity với (tức là sự hấp dẫn đối với) phần đầu của שלום, nằm ở phía bên phải của từ tiếng Do Thái đó.
Upstream affinity
Nếu bạn đặt affinity thành upstream và lặp qua string, bạn sẽ nhận được kết quả sau:
Sự khác biệt duy nhất là ở offset 5
, nơi con trỏ có upstream affinity đối với ký tự cuối cùng của Hello. Có vẻ như offset 9
giống nhau, nhưng ở đó con trỏ có upstream affinity đối với ký tự cuối cùng của שלום.
Bidirectional text comparison
Một lần nữa, điều này là downstream cho offset 5
:
Nó ở đầu שלום.
Và điều này là upstream cho offset 5
:
Nó nằm ở cuối Hello.
Nếu điều đó dường như vẫn khó hiểu khiến bạn phải suy nghĩ (như đối với tôi), thì sẽ hữu ích khi nghĩ về nó theo thứ tự unit code:
const text = 'Helloשלום';
final codeUnits = text.codeUnits;
for (var i = 0; i < codeUnits.length; i++) {
final char = String.fromCharCode(codeUnits[i]);
print('$i: $char');
}
Kết quả:
0: H
1: e
2: l
3: l
4: o
5: ש
6: ל
7: ו
8: ם
Như bạn thấy, ש of שלום xuất hiện ngay sau o của Hello khi quan sát thứ tự code unit UTF-16 chứ không phải thứ tự hiển thị cuối cùng. Nghĩa là, ש là downstream từ o, và o là upstream từ ש.
Lưu ý về text direction
Bài viết này đã nói khá nhiều về văn bản hai chiều, vì vậy cũng nên đề cập đến TextDirection
. Enum này có hai giá trị:
TextDirection.ltr
(left-to-right)TextDirection.rtl
(right-to-left)
Mọi người có thể được tha thứ vì nghĩ rằng văn bản Hello dưới dạng LTR sẽ được render olleH dưới dạng văn bản RTL, nhưng đó không phải là ý nghĩa của hướng văn bản. TextDirection kiểm soát thứ tự của các khối văn bản trong văn bản hai chiều, nghĩa là trong một string có cả ký tự LTR và RTL.
Trong string sau, Hello đứng thứ nhất và שלום là thứ hai. Cả hai được ngăn cách bởi một khoảng trắng:
const text = 'Hello שלום';
Trong môi trường hướng văn bản LTR, đầu tiên ở bên trái và cuối cùng ở bên phải:
Nhưng nếu bạn thay đổi hướng văn bản bằng cách chỉ định TextDirection.rtl
thay thế, thì đầu tiên ở bên phải và cuối cùng ở bên trái.
Như bạn có thể thấy, thứ tự ký tự của Hello hoặc שלום không thay đổi, mà là thứ tự của toàn bộ các khối đã thay đổi liên quan đến nhau. Điều thú vị là khoảng cách ở giữa vẫn còn.
Bạn có thể tìm hiểu chi tiết hơn về điều này trong câu trả lời Stack Overflow của tôi tại đây.
Kết luận
Tóm lại, text affinity có mục đích phân biệt vị trí văn bản trong hai trường hợp sau:
- Ở cuối soft line wrap.
- Boundary giữa văn bản hai chiều từ trái sang phải và từ phải sang trái.
Khi bạn lập trình Flutter, bạn gần như không phải lo lắng về text affinity vì TextPainter thực hiện tất cả công việc cho bạn. Tuy nhiên, nếu bạn đang tạo text editor tùy chỉnh, thì text affinity là một khái niệm thực sự hữu ích.
Tôi không sử dụng nhiều code trong phần giải thích ở trên, vì vậy vui lòng xem full code bên dưới.
Bạn có thể thử code bằng cách tạo một dự án Flutter mới và thay thế main.dart như sau:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: HomeWidget(),
backgroundColor: Colors.white,
),
);
}
}
class HomeWidget extends StatefulWidget {
@override
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(100.0),
child: CustomPaint(
size: Size(300, 300),
painter: LineWrappingAffinity(),
// painter: BidirectionalAffinity(), // <-- try this too
),
);
}
}
class LineWrappingAffinity extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// text
const text = '0123456789';
final textSpan = const TextSpan(
text: text,
style: TextStyle(fontSize: 50, color: Colors.black),
);
// draw text
final TextPainter textPainter = TextPainter()
..textDirection = TextDirection.ltr
..text = textSpan
..layout(maxWidth: 280);
textPainter.paint(canvas, Offset(0, 0));
// draw cursor
final cursorPaint = Paint()
..color = Colors.red
..style = PaintingStyle.fill
..strokeWidth = 1;
var caretOffset = textPainter.getOffsetForCaret(
TextPosition(offset: 9, affinity: TextAffinity.upstream),
Rect.zero,
);
final dx = caretOffset.dx;
final dy = caretOffset.dy;
final rect = Rect.fromLTRB(dx, dy, dx + 3, dy + textPainter.height / 2);
canvas.drawRect(rect, cursorPaint);
}
@override
bool shouldRepaint(CustomPainter old) {
return false;
}
}
class BidirectionalAffinity extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// text
const text = 'Helloשלום';
final textSpan = const TextSpan(
text: text,
style: TextStyle(fontSize: 50, color: Colors.black),
);
// painters
final TextPainter textPainter = TextPainter()
..textDirection = TextDirection.ltr
..text = textSpan
..layout();
final cursorPaint = Paint()
..color = Colors.red
..style = PaintingStyle.fill
..strokeWidth = 1;
// loop through each caret offset
var dy = 0.0;
for (var offset = 0; offset <= text.length; offset++) {
// draw text
textPainter.paint(canvas, Offset(0, dy));
// draw cursor
var caretOffset = textPainter.getOffsetForCaret(
TextPosition(offset: offset, affinity: TextAffinity.upstream),
Rect.zero,
);
final dx = caretOffset.dx;
final rect = Rect.fromLTRB(dx, dy, dx + 3, dy + textPainter.height);
canvas.drawRect(rect, cursorPaint);
dy += textPainter.height;
}
}
@override
bool shouldRepaint(CustomPainter old) {
return false;
}
}
Bài viết được dịch từ đây.
Kieu Hoa
Khi mình yêu cuộc đời, cuộc đời cũng sẽ yêu mình đắm say
Bài viết liên quan
Tự học Dart: Các Dart Operators (toán tử) bạn cần biết
Sep 02, 2023 • 18 min read
Flutter cơ bản: Điều cần biết khi lập trình ứng dụng đầu tiên
Aug 23, 2023 • 13 min read
Dart là gì? Giới thiệu cơ bản về ngôn ngữ lập trình Dart
Aug 21, 2023 • 11 min read
Flutter là gì? Vì sao nên học công cụ lập trình Flutter?
Aug 19, 2023 • 11 min read
Flutter cơ bản: Widget Tree, Element Tree & Render Tree
Aug 19, 2023 • 10 min read
So sánh StatelessWidget và StatefulWidget
Jul 17, 2022 • 12 min read