, October 02, 2022

0 kết quả được tìm thấy

Tìm hiểu TextAffinity trong Flutter

  • Đăng bởi  Kieu Hoa
  •  Nov 27, 2021

  •   9 min reads
Tìm hiểu TextAffinity trong Flutter

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.

Bài viết liên quan

Flutter Tutorial 2022: Giới thiệu Flutter Travel App

Trong series này, chúng ta sẽ cùng nhau thực hiện ứng dụng Travel App với một bản UI Design vô cùng đẹp mắt, thu hút có sẵn....

Flutter Tutorial 2022: Giới thiệu Flutter Travel App
So sánh StatelessWidget và StatefulWidget

Trong bài viết này, các bạn sẽ có cái nhìn tổng quát về hai loại widget lớn nhất là StatelessWidget và StatefulWidget....

So sánh StatelessWidget và StatefulWidget
Tổng hợp các Shortcuts, Extensions & Settings trong VSCode khi lập trình Flutter

Trong bài viết này, mình sẽ liệt kê cho bạn các shortcuts, extensions, setting mà bạn nên sử dụng để lập trình Flutter hàng ngày....

Tổng hợp các Shortcuts, Extensions & Settings trong VSCode khi lập trình Flutter
[Phần 1] Những extension cần thiết khi làm việc với Flutter trên VS Code

VS Code là 1 text editor tuyệt vời được phát triển bởi Mircosoft. Đây là một trong các công cụ được developer trên toàn thế giới yêu thích vì sự tiện lợi, dễ dùng và nó chứa hàng ngàn, thậm chí trăm ngàn các extension phục vụ cho bạn trong quá trình bạn phát triển phần mềm....

[Phần 1] Những extension cần thiết khi làm việc với Flutter trên VS Code
Fix lỗi Flutter 3 không thể build app trên iOS

Cách fix lỗi Flutter 3 không thể build và run được app trên iOS với video hướng dẫn chia tiết...

Fix lỗi Flutter 3 không thể build app trên iOS
You've successfully subscribed to 200Lab Blog
Great! Next, complete checkout for full access to 200Lab Blog
Xin chào mừng bạn đã quay trở lại
OK! Tài khoản của bạn đã kích hoạt thành công.
Success! Your billing info is updated.
Billing info update failed.
Your link has expired.