TextSelectionOverlay: Nâng cấp trải nghiệm chọn văn bản trong Flutter
Flutter

TextSelectionOverlay: Nâng cấp trải nghiệm chọn văn bản trong Flutter

Author

Admin System

@root

Ngày xuất bản

22 Mar, 2026

Lượt xem

3 Lượt

"TextSelectionOverlay"

Này mấy đứa, hôm nay mình cùng nhau “mổ xẻ” một cái thứ mà nhìn thì nhỏ xíu, nhưng lại có võ cực kỳ trong việc “nâng tầm” trải nghiệm người dùng trong app Flutter của mình. Đó chính là TextSelectionOverlay.

1. TextSelectionOverlay là gì mà nghe “ngầu” vậy anh Creyt?

Thực ra, TextSelectionOverlay nó giống như cái “bộ đồ nghề” mà các em thấy mỗi khi mình nhấn giữ vào một đoạn văn bản trên điện thoại để chọn chữ, copy, paste, hay cắt ấy. Nhớ không? Cái mà nó hiện ra hai cái “tay cầm” (handle) để mình kéo qua kéo lại, rồi cái thanh menu nhỏ nhỏ phía trên (toolbar) có chữ Copy, Paste, Cut ấy. Đấy, nguyên cái “combo” đó, chính là TextSelectionOverlay.

Nói một cách Genz hơn thì nó là cái “vibe” khi user tương tác với text. Thay vì chỉ là mấy cái màu mè, nút bấm mặc định nhàm chán, mình có thể “tút tát” nó lại cho thật “gu” của app mình, hoặc thậm chí là thêm mấy tính năng “độc lạ” mà app khác không có. Giống như các em đi quán cafe, cùng là cà phê thôi, nhưng quán nào có decor đẹp, menu sáng tạo, có “chất” riêng thì mình thích hơn đúng không? App mình cũng vậy, cái TextSelectionOverlay chính là một phần của cái “chất” đó!

Để làm gì ư? Đơn giản là để app của mình trông chuyên nghiệp hơn, “ăn nhập” hơn với branding tổng thể, hoặc cung cấp các chức năng đặc biệt ngay tại chỗ mà người dùng cần, tăng tính tiện lợi và “đã” mắt hơn khi sử dụng.

2. Code Ví Dụ Minh Hoạ: “Tút tát” TextSelectionOverlay

Ví dụ 1: Thay đổi màu sắc cơ bản với TextSelectionTheme (Dễ mà hiệu quả)

Đây là cách “nhẹ đô” nhất để thay đổi màu của các thành phần trong TextSelectionOverlay như handle, cursor, và màu nền khi chọn chữ. Mình sẽ dùng TextSelectionThemeData trong ThemeData hoặc TextSelectionTheme widget.

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: 'Text Selection Demo',
      theme: ThemeData(
        // Đây là cách mình 'tút tát' màu cho TextSelectionOverlay
        textSelectionTheme: const TextSelectionThemeData(
          cursorColor: Colors.deepPurple, // Màu con trỏ nhấp nháy
          selectionColor: Colors.deepPurpleAccent.withOpacity(0.3), // Màu nền khi chọn chữ
          selectionHandleColor: Colors.deepPurple, // Màu của 'tay cầm' (handle)
        ),
        // Thêm màu nền cho Scaffold để dễ nhìn hơn
        scaffoldBackgroundColor: Colors.grey[100],
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TextSelectionOverlay Customization'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: const Center(
        child: Padding(
          padding: EdgeInsets.all(20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'Chào mừng đến với lớp học của anh Creyt!',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 20),
              SelectableText(
                'Đây là một đoạn văn bản mà các bạn có thể nhấn giữ để chọn. Thử xem các màu sắc của handle và vùng chọn đã thay đổi chưa nhé! Thấy xịn hơn chưa?',
                style: TextStyle(fontSize: 18),
                textAlign: TextAlign.justify,
              ),
              SizedBox(height: 40),
              TextField(
                decoration: InputDecoration(
                  labelText: 'Thử gõ và chọn ở đây nữa nè',
                  border: OutlineInputBorder(),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Giải thích: Đơn giản là mình bọc toàn bộ app trong MaterialApp và dùng ThemeData để định nghĩa textSelectionTheme. Các thuộc tính như cursorColor, selectionColor, selectionHandleColor sẽ giúp mình đổi màu cho con trỏ, vùng chọn và các handle. Nó sẽ ảnh hưởng đến tất cả các SelectableText, TextField, TextFormField trong app.

Ví dụ 2: Tùy biến sâu hơn với TextSelectionControls (Dân chơi hệ pro)

Khi các em muốn thay đổi hình dáng của handle, hoặc thêm/bớt các nút trong thanh toolbar (menu copy/paste), thì lúc này TextSelectionControls là “vũ khí” tối thượng. Mình sẽ tạo một class kế thừa từ TextSelectionControlsoverride các phương thức cần thiết.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Text Selection Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Custom TextSelectionOverlay'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'Chào mừng đến với lớp học của anh Creyt!',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 20),
              // Dùng SelectionArea để áp dụng custom controls
              SelectionArea(
                selectionControls: MyCustomTextSelectionControls(context),
                child: const SelectableText(
                  'Đây là một đoạn văn bản mà các bạn có thể nhấn giữ để chọn. Hãy để ý xem handle đã đổi màu thành Teal và thanh menu có thêm nút "Search Google" chưa nhé!',
                  style: TextStyle(fontSize: 18),
                  textAlign: TextAlign.justify,
                ),
              ),
              const SizedBox(height: 40),
              TextField(
                decoration: const InputDecoration(
                  labelText: 'Thử gõ và chọn ở đây nữa nè',
                  border: OutlineInputBorder(),
                ),
                // TextField cũng có thể dùng chung controls nếu không được override
                // Hoặc bạn có thể bọc nó trong SelectionArea riêng với controls khác
                selectionControls: MyCustomTextSelectionControls(context),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// Đây là class 'dân chơi hệ pro' của mình
class MyCustomTextSelectionControls extends MaterialTextSelectionControls {
  MyCustomTextSelectionControls(this.context);

  final BuildContext context;

  /// Override màu của handle để nó 'ăn nhập' với màu app
  @override
  Color getHandleColor(TextSelectionThemeData data) {
    return Theme.of(context).colorScheme.tertiary; // Màu teal từ ThemeData
  }

  /// Override cái 'bộ đồ nghề' (toolbar) khi chọn chữ
  @override
  Widget buildToolbar(
      BuildContext context,
      Rect globalEditableRegion,
      double textLineHeight,
      Offset selectionMidpoint,
      List<TextSelectionPoint> endpoints,
      TextSelectionDelegate delegate,
      ValueListenable<bool> hideToolbar,
      ) {
    final List<Widget> customButtons = [
      // Nút 'Copy' mặc định
      TextSelectionToolbarTextButton(
        padding: TextSelectionToolbarTextButton.get </* 'copy' */ TextSelectionToolbarTextButton.get , // copy
        onPressed: () => delegate.copySelection(SelectionChangedCause.toolbar),
        child: const Text('Copy'),
      ),
      // Nút 'Paste' mặc định
      TextSelectionToolbarTextButton(
        padding: TextSelectionToolbarTextButton.get , // paste
        onPressed: () => delegate.pasteText(SelectionChangedCause.toolbar),
        child: const Text('Paste'),
      ),
      // Thêm nút 'Search Google' của riêng mình
      TextSelectionToolbarTextButton(
        padding: TextSelectionToolbarTextButton.get , // custom
        onPressed: () {
          // Implement logic to search Google with selected text
          final String selectedText = delegate.textEditingValue.text.substring(
            delegate.textEditingValue.selection.start,
            delegate.textEditingValue.selection.end,
          );
          print('Searching Google for: $selectedText');
          // Mấy đứa có thể mở link web ở đây nè
          // launchUrl(Uri.parse('https://www.google.com/search?q=$selectedText'));
          delegate.hideToolbar(); // Ẩn toolbar sau khi bấm
        },
        child: const Text('Search Google'),
      ),
    ];

    return TextSelectionToolbar(
      anchor: globalEditableRegion.center + Offset(0, -textLineHeight / 2),
      children: customButtons,
    );
  }

  /// Override hình dáng handle
  @override
  Widget buildHandle(
      BuildContext context, TextSelectionHandleType type, double textLineHeight) {
    // Đổi màu handle thành màu accent của app, và làm nó nhỏ hơn chút
    final Color handleColor = getHandleColor(Theme.of(context).textSelectionTheme);
    return SizedBox(
      width: 20.0,
      height: 20.0,
      child: CustomPaint(
        painter: _TextSelectionHandlePainter(handleColor),
      ),
    );
  }
}

// CustomPainter để vẽ cái handle hình tròn nhỏ xinh
class _TextSelectionHandlePainter extends CustomPainter {
  _TextSelectionHandlePainter(this.color);

  final Color color;

  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()..color = color;
    canvas.drawCircle(Offset(size.width / 2, size.height / 2), size.width / 2, paint);
  }

  @override
  bool shouldRepaint(_TextSelectionHandlePainter oldPainter) {
    return color != oldPainter.color;
  }
}

Giải thích:

  • Mình tạo MyCustomTextSelectionControls kế thừa từ MaterialTextSelectionControls để tận dụng các logic mặc định mà Flutter cung cấp.
  • getHandleColor: Dùng để định nghĩa màu cho handle. Anh Creyt đã đổi nó thành màu tertiary của ColorScheme để nó “ăn rơ” hơn với theme.
  • buildToolbar: Đây là nơi mình “chế biến” cái thanh menu. Mình có thể thêm/bớt các TextSelectionToolbarTextButton hoặc bất kỳ widget nào khác vào danh sách children của TextSelectionToolbar. Ở đây anh Creyt đã thêm nút Search Google.
  • buildHandle: Cái này cho phép mình vẽ lại hoàn toàn hình dáng của handle. Anh Creyt đã vẽ một cái hình tròn nhỏ xinh bằng CustomPaint_TextSelectionHandlePainter.
  • Cuối cùng, mình dùng SelectionArea widget và truyền MyCustomTextSelectionControls vào thuộc tính selectionControls để áp dụng các tùy chỉnh này cho các SelectableText bên trong nó. Đối với TextField, mình có thể truyền trực tiếp vào selectionControls của TextField.
Illustration

3. Mẹo Vặt Của Creyt (Best Practices) Để Ghi Nhớ Và Dùng Thực Tế

  • Đừng “làm quá”: Tùy biến là tốt, nhưng đừng làm nó quá khác lạ đến mức người dùng không nhận ra đây là chức năng chọn văn bản nữa. Giữ cho nó trực quan và dễ hiểu.
  • Đồng bộ UI/UX: Màu sắc, hình dáng của handle và toolbar nên “ăn nhập” với tổng thể thiết kế của app. Đừng để nó lạc quẻ như “áo gấm đi đêm” nhé.
  • Đảm bảo Accessibility: Nhớ rằng không phải ai cũng có ngón tay thon thả hay thị lực tốt. Handle nên đủ lớn để dễ chạm, màu sắc phải có độ tương phản tốt để dễ nhìn. Flutter đã làm rất tốt điều này với các widget mặc định, khi tùy biến mình cần cân nhắc.
  • Thử nghiệm đa nền tảng: Android và iOS có những “gu” thiết kế riêng. TextSelectionOverlay có thể trông hơi khác nhau. Hãy test trên cả hai để đảm bảo trải nghiệm mượt mà nhất.
  • Chỉ tùy biến khi cần: Nếu app của em chỉ cần chức năng copy/paste cơ bản, thì cứ để mặc định cho Flutter lo. Đừng “cố đấm ăn xôi” tùy biến chỉ vì muốn “khác người” mà không có mục đích rõ ràng.

4. Ứng Dụng Thực Tế: Ai Đã Dùng Rồi?

Các em để ý các app sau, họ tận dụng TextSelectionOverlay rất xịn sò:

  • Notion / Medium: Khi các em chọn một đoạn văn bản trong các ứng dụng ghi chú hoặc đọc bài viết này, thanh toolbar không chỉ có Copy mà còn có các tùy chọn Bold, Italic, Highlight, Comment, hoặc thậm chí là Turn into block. Đó chính là tùy biến TextSelectionControls đó!
  • Google Docs / Microsoft Word: Đây là “ông tổ” của việc tùy biến thanh chọn văn bản. Các em có thể thấy vô vàn tùy chọn định dạng, comment, dịch thuật khi chọn một đoạn text.
  • Ứng dụng đọc sách (Kindle, Google Books): Khi chọn một từ hoặc đoạn văn, các app này thường hiện ra các tùy chọn như Highlight, Note, Search Dictionary, Translate, hay Share Quote. Cực kỳ tiện lợi cho “mọt sách” đúng không?

5. Thử Nghiệm Và Nên Dùng Cho Case Nào?

Vậy khi nào thì mình nên “động tay” vào TextSelectionOverlay?

  • App có Branding mạnh: Nếu app của em có một bộ màu sắc, font chữ riêng biệt, việc tùy biến handle và toolbar theo branding sẽ giúp app trông “chuyên nghiệp” và “có gu” hơn rất nhiều.
  • Cần thêm chức năng độc đáo: Đây là lúc buildToolbar phát huy tác dụng. Ví dụ, trong một app từ điển, khi chọn một từ, em có thể thêm nút Tra từ, Thêm vào danh sách học. Hoặc trong một app mạng xã hội, có thể có nút Share Quote để chia sẻ nhanh đoạn văn bản đó lên story.
  • Cải thiện trải nghiệm người dùng (UX) và Accessibility: Nếu handle mặc định quá nhỏ hoặc khó nhìn trên một số thiết bị, mình có thể thay đổi kích thước, màu sắc để nó dễ tương tác hơn. Hoặc nếu app của em hướng đến người dùng có nhu cầu đặc biệt, việc tùy biến này là cực kỳ quan trọng.

Nhớ nhé, TextSelectionOverlay không chỉ là một chi tiết nhỏ, nó là một phần quan trọng tạo nên sự tinh tế và khác biệt cho app của các em. Hãy tận dụng nó để “phù phép” cho app của mình trở nên “xịn sò” hơn trong mắt người dùng! Anh Creyt tin mấy đứa làm được!

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!