Điểm Neo Chữ: Giải Mã TextSelectionPoint trong Flutter
Flutter

Điểm Neo Chữ: Giải Mã TextSelectionPoint trong Flutter

Author

Admin System

@root

Ngày xuất bản

22 Mar, 2026

Lượt xem

5 Lượt

"TextSelectionPoint"

Chào các "dev-er" tương lai, hôm nay anh Creyt sẽ "bung lụa" một khái niệm nghe thì "hack não" nhưng thực ra lại "easy game" nếu các em chịu khó nghe anh "chém gió" tí. Từ khóa hôm nay là TextSelectionPoint – cái thứ mà nghe thôi đã thấy "căng đét" rồi đúng không? Đừng lo, anh sẽ biến nó thành câu chuyện "quẹt" chữ mà ai cũng hiểu.

1. TextSelectionPoint là gì? (Kiểu Gen Z)

Nói một cách "ngôn tình" Gen Z, TextSelectionPoint chính là "cái điểm neo" của mỗi cú "quẹt" chọn chữ của các em trên màn hình. Tưởng tượng các em đang dùng ngón tay "vuốt" để chọn một đoạn text. Cái điểm mà ngón tay các em bắt đầukết thúc chính là hai TextSelectionPoint đó.

Nhưng mà, nó không chỉ đơn thuần là một vị trí (index) đâu nha. Nó còn có một "cái thần thái" riêng, gọi là TextAffinity. Cứ như bạn đứng ở vạch đích, nhưng có thể là "mép trong" hay "mép ngoài" vạch vậy. Cụ thể:

  • index: Đây là vị trí của ký tự trong chuỗi văn bản. Kiểu như bạn đang đứng ở ký tự thứ 5, thứ 10 gì đó.
  • affinity: Cái này mới "ảo diệu" nè. Nó cho biết "điểm neo" của bạn đang "hút" về phía nào của ký tự đó. Có hai loại:
    • TextAffinity.upstream: Nghĩa là điểm đó đang "ngả" về phía trước ký tự (bên trái nếu là văn bản LTR). Cứ như nó đang "nương tựa" vào ký tự đằng trước vậy.
    • TextAffinity.downstream: Nghĩa là điểm đó đang "ngả" về phía sau ký tự (bên phải nếu là văn bản LTR). Nó đang "ôm ấp" ký tự hiện tại.

Hiểu đơn giản, TextSelectionPoint là một cặp bài trùng (index, affinity) giúp Flutter biết chính xác "cái ranh giới" của vùng chọn chữ của bạn là ở đâu, không lệch đi đâu một ly.

2. Để làm gì? (Why it matters)

"Anh Creyt ơi, em dùng Text với TextField mặc định vẫn chọn chữ ầm ầm mà có thấy dùng cái này đâu?" – Đúng rồi, vì Flutter nó "tự động hóa" cho mình rồi. Nhưng nếu các em muốn "flex" trình độ, muốn "custom" một cái widget hiển thị văn bản riêng, hoặc muốn tạo ra những hiệu ứng chọn chữ "độc lạ Bình Dương" thì TextSelectionPoint chính là "chìa khóa vàng" đó. Nó giúp các em:

  • Kiểm soát chính xác vùng chọn: Không chỉ biết bắt đầu ở ký tự nào, mà còn biết "mép nào" của ký tự đó.
  • Tạo widget text "đỉnh của chóp": Khi xây dựng các trình soạn thảo code, trình chỉnh sửa rich text, hay bất kỳ widget nào cần tương tác sâu với text selection, các em sẽ cần đến nó để vẽ vùng chọn, xử lý copy/paste, v.v.
  • Xử lý các trường hợp "khó nhằn": Ví dụ, khi chọn chữ ở cuối dòng, đầu dòng, hoặc khi có các ký tự đặc biệt, affinity sẽ giúp phân biệt rõ ràng.
Illustration

3. Code Ví Dụ Minh Họa Rõ Ràng

Để các em dễ hình dung, anh Creyt sẽ làm một ví dụ đơn giản. Chúng ta sẽ tạo một đoạn văn bản và giả lập một vùng chọn, sau đó "soi" xem các TextSelectionPoint của vùng chọn đó trông như thế nào nhé.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TextSelectionPoint Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const TextSelectionPointScreen(),
    );
  }
}

class TextSelectionPointScreen extends StatefulWidget {
  const TextSelectionPointScreen({super.key});

  @override
  State<TextSelectionPointScreen> createState() => _TextSelectionPointScreenState();
}

class _TextSelectionPointScreenState extends State<TextSelectionPointScreen> {
  final String _text = "Hôm nay anh Creyt dạy TextSelectionPoint rất dễ hiểu.";
  TextSelection? _currentSelection;
  String _selectionInfo = "Chưa có vùng chọn nào.";

  @override
  void initState() {
    super.initState();
    // Giả lập một vùng chọn ban đầu
    _currentSelection = const TextSelection(
      baseOffset: 12, // Bắt đầu từ chữ 'Creyt'
      extentOffset: 25, // Kết thúc ở chữ 'TextSelectionPoint'
      affinity: TextAffinity.downstream,
      isDirectional: true,
    );
    _updateSelectionInfo();
  }

  void _updateSelectionInfo() {
    if (_currentSelection == null) {
      setState(() {
        _selectionInfo = "Chưa có vùng chọn nào.";
      });
      return;
    }

    // Lấy TextSelectionPoint từ TextSelection
    final TextSelectionPoint basePoint = _currentSelection!.base;
    final TextSelectionPoint extentPoint = _currentSelection!.extent;

    // In thông tin chi tiết của từng điểm neo
    setState(() {
      _selectionInfo = '''
      Vùng chọn hiện tại: "${_currentSelection!.textInside(_text)}"
      
      Điểm BẮT ĐẦU (Base Point):
      - Index: ${basePoint.index} (Ký tự: '${_text[basePoint.index]}')
      - Affinity: ${basePoint.affinity}
      
      Điểm KẾT THÚC (Extent Point):
      - Index: ${extentPoint.index} (Ký tự: '${_text[extentPoint.index - 1]}')
      - Affinity: ${extentPoint.affinity}
      
      Giải thích: 
      Base Point index ${basePoint.index} là vị trí của chữ '${_text[basePoint.index]}'.
      Extent Point index ${extentPoint.index} là vị trí SAU chữ '${_text[extentPoint.index - 1]}'
      (vì extentOffset thường trỏ đến vị trí *sau* ký tự cuối cùng được chọn).
      ''';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TextSelectionPoint trong Flutter'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Đoạn văn bản gốc:',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
            ),
            const SizedBox(height: 8),
            SelectableText(
              _text,
              style: const TextStyle(fontSize: 16),
              // Khi người dùng tự chọn, chúng ta cập nhật _currentSelection
              onSelectionChanged: (selection, cause) {
                setState(() {
                  _currentSelection = selection;
                  _updateSelectionInfo();
                });
              },
            ),
            const Divider(height: 32),
            const Text(
              'Thông tin chi tiết về TextSelectionPoint:',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
            ),
            const SizedBox(height: 8),
            Container(
              padding: const EdgeInsets.all(12),
              color: Colors.grey[200],
              width: double.infinity,
              child: Text(
                _selectionInfo,
                style: const TextStyle(fontFamily: 'monospace', fontSize: 14),
              ),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _currentSelection = const TextSelection(
                    baseOffset: 0,
                    extentOffset: 5, // Chọn chữ "Hôm n"
                    affinity: TextAffinity.downstream,
                    isDirectional: true,
                  );
                  _updateSelectionInfo();
                });
              },
              child: const Text('Chọn "Hôm n"'),
            ),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _currentSelection = const TextSelection(
                    baseOffset: 30,
                    extentOffset: 34, // Chọn chữ "rất"
                    affinity: TextAffinity.upstream,
                    isDirectional: true,
                  );
                  _updateSelectionInfo();
                });
              },
              child: const Text('Chọn "rất" (affinity upstream)'),
            ),
          ],
        ),
      ),
    );
  }
}

Trong ví dụ này, anh Creyt dùng SelectableText để các em có thể tự "quẹt" chọn và thấy thông tin thay đổi. Ngoài ra, anh còn giả lập các vùng chọn cố định để các em thấy rõ cách basePointextentPoint (là các TextSelectionPoint) được trích xuất và hiển thị thông tin indexaffinity của chúng.

Gợi Ý Đọc Tiếp
MaterialStateProperty: Nút "Mood Ring" của Flutter!

9 Lượt xem

Lưu ý: baseOffsetextentOffset trong TextSelection cũng chính là index của baseextent TextSelectionPoint đó. base là điểm bắt đầu vùng chọn, extent là điểm kết thúc. Khi baseOffset < extentOffset thì base là điểm đầu tiên theo thứ tự văn bản, extent là điểm cuối cùng. Ngược lại, nếu bạn chọn từ phải sang trái, extent có thể có index nhỏ hơn base.

4. Mẹo Nhỏ từ Creyt (Best Practices)

  • "Thần thái" của affinity là quan trọng: Đừng bao giờ quên TextAffinity. Nó giúp phân biệt các trường hợp "râu ria" như chọn giữa hai ký tự, hoặc chọn ở vị trí xuống dòng. Cứ nghĩ nó là "nam châm" hút điểm neo về phía nào của ký tự đó.
  • Đừng tự "phát minh lại bánh xe": Nếu TextTextField của Flutter đã đáp ứng đủ nhu cầu, cứ dùng chúng. Chỉ khi nào các em cần "custom" sâu, "chơi trội" thì mới cần đụng đến TextSelectionPoint.
  • Test "tới bến" các trường hợp biên: Văn bản rỗng, văn bản chỉ có một ký tự, văn bản nhiều dòng, văn bản có ký tự đặc biệt (emoji, ký tự unicode phức tạp). Đây là những "chiêu" giúp các em "lên trình" debug và hiểu sâu hơn.
  • Debug bằng cách print: Khi "bí", cứ print indexaffinity của các TextSelectionPoint ra console. Nó sẽ "mách nước" cho các em rất nhiều đó.

5. Ứng Dụng Thực Tế (App Examples)

Cứ tưởng tượng bất kỳ ứng dụng nào mà các em thấy người ta "quẹt quẹt" chọn chữ "xịn sò" thì chắc chắn có bóng dáng của TextSelectionPoint (hoặc các khái niệm tương tự trong các framework khác) ở đó:

  • Trình soạn thảo code (VS Code, Android Studio, Xcode): Các em có thể chọn từng dòng, từng khối code, highlight syntax. Để làm được điều đó, họ phải biết chính xác từng điểm bắt đầu và kết thúc của vùng chọn.
  • Ứng dụng ghi chú, soạn thảo văn bản (Notion, Google Docs, Medium): Các app này cho phép chọn text, bôi đậm, in nghiêng, highlight màu mè. Tất cả đều dựa trên việc xác định chính xác vùng text được chọn.
  • Các app đọc sách, báo: Chức năng highlight một đoạn văn để ghi chú, chia sẻ cũng là một ví dụ điển hình.
  • Ứng dụng dịch thuật: Khi bạn chọn một đoạn text để dịch, ứng dụng cần biết chính xác đoạn nào cần dịch.

6. Thử Nghiệm & Nên Dùng Khi Nào

Khi nào nên "triển" TextSelectionPoint?

  • Xây dựng widget text "cây nhà lá vườn": Nếu bạn muốn tự tay tạo một widget hiển thị văn bản từ đầu (thường là dùng CustomPainter hoặc các widget cấp thấp hơn) và cần hỗ trợ chọn chữ.
  • Tạo hiệu ứng chọn chữ "độc lạ": Ví dụ, bạn muốn khi người dùng chọn một từ, nó tự động chọn cả câu chứa từ đó, hoặc muốn có hiệu ứng animation khi chọn.
  • Tạo trình soạn thảo "rich text" phức tạp: Cần kiểm soát từng milimet của vùng chọn để áp dụng định dạng, chèn đối tượng, v.v.
  • Xử lý văn bản đa hướng (Bi-directional text): Trong các ngôn ngữ như Ả Rập, Hebrew, hướng chữ có thể thay đổi. TextAffinity trở nên cực kỳ quan trọng để xác định đúng ranh giới vùng chọn.

Thử nghiệm "sương sương" tại nhà:

Anh Creyt thách các em thử tạo một TextSelection với cùng một index nhưng thay đổi affinity (ví dụ: baseOffset: 5, affinity: TextAffinity.upstreambaseOffset: 5, affinity: TextAffinity.downstream) và quan sát xem vùng chọn của nó có "xê dịch" tí nào không, đặc biệt là ở các vị trí có ký tự xuống dòng hoặc khoảng trắng. Các em sẽ thấy sự khác biệt "nhẹ nhàng" nhưng lại rất quan trọng đó.

Tóm lại, TextSelectionPoint là một "mảnh ghép" quan trọng trong việc làm chủ text selection trong Flutter, đặc biệt khi các em muốn "nâng tầm" ứng dụng của mình lên một đẳng cấp khác. Cứ "nghịch" nhiều vào, rồi các em sẽ thấy nó "dễ như ăn kẹo" thôi! Chúc các em code vui vẻ!

Thuộc Series: Flutter

Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

#tech #cyberpunk #laravel
Chỉnh sửa bài viết

Bình luận (0)

Vui lòng Đăng Nhập để Bình luận

Hỗ trợ Markdown cơ bản
Nguyễn Văn A
1 ngày trước

Tính năng này đỉnh quá ad ơi, chờ mãi mới thấy một blog Tiếng Việt có UI/UX xịn như vầy!