
Chào mấy đứa GenZ mê code! Hôm nay, anh Creyt sẽ dẫn mấy đứa đi "mổ xẻ" một thứ nghe có vẻ khô khan nhưng lại là "linh hồn" của trải nghiệm người dùng khi tương tác với văn bản: TextSelectionControls trong Flutter.
1. TextSelectionControls là gì và để làm gì?
"Tưởng tượng mà xem, khi mấy đứa "quẹt" ngón tay trên màn hình điện thoại để chọn một đoạn văn bản, rồi bỗng dưng hiện ra cái thanh công cụ "Copy, Cut, Paste" huyền thoại, kèm theo hai cái "chấm tròn" hay "thanh đứng" nhỏ xíu ở hai đầu đoạn văn bản để mình kéo ra kéo vào ấy. Đấy! Tất cả những thứ đó, từ cái thanh công cụ đến mấy cái "tay cầm" bé xinh kia, đều là do TextSelectionControls "điều khiển" và "vẽ" ra đó!"
Nói một cách "học thuật" hơn, TextSelectionControls trong Flutter là một lớp trừu tượng (abstract class) chịu trách nhiệm cung cấp các thành phần giao diện người dùng (UI) và hành vi liên quan đến việc chọn văn bản. Cụ thể, nó quản lý:
- Selection Handles: Các điểm neo (thường là hình tròn hoặc thanh) ở đầu và cuối vùng chọn văn bản, cho phép người dùng điều chỉnh phạm vi chọn.
- Selection Toolbar: Thanh công cụ popup chứa các hành động như Copy, Cut, Paste, Select All, Share, v.v., xuất hiện khi văn bản được chọn.
Mục đích chính của nó là cho phép các developer như chúng ta tùy chỉnh hoàn toàn giao diện và hành vi của quá trình chọn văn bản. Thay vì cứ dùng cái mặc định "na ná" nhau của hệ điều hành, mấy đứa có thể "phù phép" để nó mang đậm dấu ấn riêng của app mình, thậm chí thêm thắt các chức năng "độc quyền" nữa!
"Nó giống như cái remote điều khiển tivi vậy đó, nhưng thay vì chỉ có nút chuyển kênh và tăng giảm âm lượng, mấy đứa có thể biến nó thành một cái joystick game, thêm nút "hack", nút "buff" tùy ý, miễn sao người dùng thấy sướng là được!"
2. Code Ví Dụ Minh Hoạ Rõ Ràng
Để mấy đứa dễ hình dung, anh Creyt sẽ chỉ cho cách tạo một bộ TextSelectionControls "chất chơi" riêng, đổi màu, đổi icon, thêm nút "Highlight" nữa cho nó "ngầu"!
Chúng ta sẽ kế thừa từ MaterialTextSelectionControls (để tận dụng các logic mặc định của Material Design) và override các phương thức cần thiết.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; // For TextSelectionHandleType
// 1. Tạo một TextSelectionControls tùy chỉnh của riêng anh Creyt
class MyCustomTextSelectionControls extends MaterialTextSelectionControls {
/// Override phương thức buildToolbar để tùy chỉnh thanh công cụ.
@override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
Offset? lastTapDownPosition,
TextSelectionDelegate delegate,
ValueNotifier<bool> textRectsExist,
) {
// Đây là nơi anh em mình "phù phép" cái thanh toolbar mặc định.
// Thay vì trả về cái toolbar mặc định, mình sẽ tùy biến nó một chút.
// Chúng ta sẽ dùng TextSelectionToolbar để giữ nguyên vị trí và hiệu ứng mặc định,
// nhưng thay đổi nội dung bên trong.
return TextSelectionToolbar(
anchor: globalEditableRegion.center, // Vị trí neo cho toolbar
children: <Widget>[
// Nút Copy với icon và màu sắc "Creyt-style"
MaterialButton(
onPressed: () => delegate.copySelection(SelectionChangedCause.toolbar),
child: const Icon(Icons.copy_all, color: Colors.purple, size: 20),
minWidth: 40, // Giảm kích thước nút
height: 40,
padding: EdgeInsets.zero,
),
// Nút Paste với icon và màu sắc "Creyt-style"
MaterialButton(
onPressed: () => delegate.pasteSelection(SelectionChangedCause.toolbar),
child: const Icon(Icons.paste_sharp, color: Colors.green, size: 20),
minWidth: 40,
height: 40,
padding: EdgeInsets.zero,
),
// Nút Cut với icon và màu sắc "Creyt-style"
MaterialButton(
onPressed: () => delegate.cutSelection(SelectionChangedCause.toolbar),
child: const Icon(Icons.content_cut_sharp, color: Colors.red, size: 20),
minWidth: 40,
height: 40,
padding: EdgeInsets.zero,
),
// Thêm một nút tùy chỉnh "Highlight" - Đây mới là điểm nhấn!
MaterialButton(
onPressed: () {
// Logic xử lý khi nhấn Highlight
final String selectedText = delegate.textEditingValue.text.substring(
delegate.textEditingValue.selection.start,
delegate.textEditingValue.selection.end,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anh Creyt đã highlight: "$selectedText"')),
);
delegate.hideToolbar(); // Ẩn toolbar sau khi xử lý
},
child: const Icon(Icons.highlight, color: Colors.blue, size: 20),
minWidth: 40,
height: 40,
padding: EdgeInsets.zero,
),
],
);
}
/// Override phương thức buildHandle để tùy chỉnh các tay cầm (handles).
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) {
// Đây là nơi anh em mình "phù phép" mấy cái tay cầm (handles).
// Mặc định thì nó là hình tròn, giờ mình thử đổi màu và thêm icon nhẹ nhàng.
final Color handleColor = Theme.of(context).primaryColor; // Lấy màu chủ đạo của app
return SizedBox(
width: 24, // Kích thước handle
height: 24,
child: Center(
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: handleColor.withOpacity(0.8), // Màu handle tùy chỉnh
shape: BoxShape.circle, // Giữ hình tròn
),
child: Icon(
// Đổi icon tùy theo loại handle (trái/phải)
type == TextSelectionHandleType.left ? Icons.arrow_back_ios_new : Icons.arrow_forward_ios_new,
size: 12,
color: Colors.white,
),
),
),
);
}
}
// 2. Ứng dụng TextSelectionControls tùy chỉnh vào MaterialApp
class TextSelectionControlsExampleApp extends StatelessWidget {
const TextSelectionControlsExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Custom Text Selection',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.deepPurple,
// Đây là chỗ quan trọng để áp dụng TextSelectionControls tùy chỉnh
textSelectionTheme: TextSelectionThemeData(
selectionControls: MyCustomTextSelectionControls(), // Áp dụng controls của mình
selectionColor: Colors.deepPurple.withOpacity(0.3), // Màu nền khi chọn text
cursorColor: Colors.deepPurple, // Màu con trỏ
),
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TextSelectionControls Demo của anh Creyt'),
),
body: const Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
decoration: InputDecoration(
labelText: 'Thử gõ và chọn văn bản ở đây!',
border: OutlineInputBorder(),
),
maxLines: 5,
controller: TextEditingController(
text: 'Chào mấy đứa GenZ mê code! Đây là một đoạn văn bản ví dụ để mấy đứa ',
),
),
SizedBox(height: 20),
SelectableText(
'Có thể thử chọn, copy, paste, và xem cái thanh công cụ ' +
'tùy chỉnh của anh Creyt nó hoạt động như thế nào. ' +
'Hãy tự mình trải nghiệm và cảm nhận sự khác biệt nhé! ' +
'Flutter là một framework UI mạnh mẽ cho phép bạn xây dựng ứng dụng ' +
'đa nền tảng từ một codebase duy nhất. Học Flutter không khó, ' +
'chỉ cần có đam mê và một người thầy "chất" như anh Creyt là okela!',
style: TextStyle(fontSize: 16),
),
],
),
),
);
}
}
void main() {
runApp(const TextSelectionControlsExampleApp());
}
Trong ví dụ trên:
MyCustomTextSelectionControlskế thừaMaterialTextSelectionControlsđể có sẵn các logic cơ bản.- Anh Creyt override
buildToolbarđể tạo ra mộtTextSelectionToolbarvới cácMaterialButtontùy chỉnh, thêm nút "Highlight" "độc quyền" và đổi icon. - Anh Creyt override
buildHandleđể thay đổi màu sắc và thêm icon vào các tay cầm chọn văn bản. - Cuối cùng, áp dụng
MyCustomTextSelectionControlsnày vàotextSelectionThemecủaMaterialAppđể nó có hiệu lực trên toàn bộ ứng dụng.

3. Mẹo (Best Practices) từ anh Creyt
"Trong lập trình, không phải cái gì tùy biến cũng là tốt, quan trọng là tùy biến đúng lúc, đúng chỗ!"
- Khi nào thì "đụng chạm": Chỉ nên tùy biến
TextSelectionControlskhi thực sự cần thiết, ví dụ để phù hợp với branding của ứng dụng (màu sắc, font chữ, icon), hoặc khi cần thêm các chức năng đặc biệt (như nút "Dịch", "Tìm kiếm trong từ điển", "Gửi nhanh qua Zalo"...). Đừng tùy biến chỉ vì muốn khác biệt mà không có lý do chính đáng, dễ làm người dùng bối rối. - Giữ sự nhất quán: Nếu đã tùy biến, hãy đảm bảo nó nhất quán trên toàn bộ ứng dụng. Người dùng ghét sự "nửa vời" hoặc mỗi chỗ một kiểu. Điều này giúp trải nghiệm người dùng mượt mà và dễ đoán.
- Cân nhắc hiệu năng: Việc vẽ các widget phức tạp cho toolbar và handles có thể ảnh hưởng nhỏ đến hiệu năng, đặc biệt trên các thiết bị cũ hoặc khi có quá nhiều văn bản. Giữ cho UI đơn giản, hiệu quả và tránh các animation quá "nặng đô" cho các thành phần này.
- Accessibility (Khả năng tiếp cận): Đảm bảo các nút tùy chỉnh vẫn dễ sử dụng cho mọi người, bao gồm cả những người dùng có nhu cầu đặc biệt. Kích thước nút, độ tương phản màu sắc, và mô tả ngữ nghĩa (semantic labels) là những yếu tố quan trọng cần lưu ý.
- Kế thừa từ cái có sẵn: Thay vì viết lại từ đầu, hãy kế thừa từ
MaterialTextSelectionControlshoặcCupertinoTextSelectionControlsvà chỉ override những phần mình muốn thay đổi. "Đừng cố gắng tự chế bánh xe khi đã có cái bánh xe chất lượng cao rồi, chỉ cần sơn phết lại thôi là đủ "chất" rồi!"
4. Ví dụ thực tế các ứng dụng/website đã ứng dụng
"Mấy đứa thấy đó, mấy cái app "xịn xò" thường không chỉ dừng lại ở chức năng cơ bản đâu, họ luôn tìm cách tối ưu từng chút một để người dùng mê mẩn!"
- Ứng dụng soạn thảo văn bản chuyên nghiệp (Notion, Google Docs, Obsidian): Các ứng dụng này thường có thanh chọn văn bản "siêu cấp" với vô vàn tùy chọn không chỉ Copy/Paste mà còn định dạng (in đậm, in nghiêng, gạch chân), thêm link, comment, chuyển đổi heading, v.v.
- Ứng dụng học ngoại ngữ (Duolingo, Elsa Speak): Khi người dùng chọn một từ hoặc cụm từ, thanh công cụ có thể hiện thêm nút "Dịch", "Tra từ điển", "Nghe phát âm", giúp việc học trở nên tiện lợi hơn rất nhiều.
- Ứng dụng đọc sách điện tử (Kindle, Google Books): Khi chọn một đoạn văn bản trong sách, người dùng thường thấy các tùy chọn như "Highlight" (đánh dấu), "Ghi chú", "Tìm kiếm trên mạng", "Chia sẻ đoạn trích".
- Các trình duyệt web tùy chỉnh (Brave, Opera): Một số trình duyệt có thể thêm nút "Mở trong tab mới", "Chia sẻ nhanh" cho các đường link được chọn, hoặc "Tìm kiếm hình ảnh" khi chọn một hình ảnh.
5. Thử nghiệm và Hướng dẫn nên dùng cho case nào
"Anh Creyt đã từng "nghịch" đủ kiểu với cái này rồi, và đây là kinh nghiệm xương máu để mấy đứa không bị "lạc trôi"!"
Nên dùng TextSelectionControls tùy chỉnh khi:
- Branding mạnh mẽ: Khi ứng dụng của mấy đứa có một bộ nhận diện thương hiệu (brand identity) rất mạnh và muốn mọi chi tiết UI, dù là nhỏ nhất, cũng phải "thuần" brand, từ màu sắc, hình dạng của handles đến icon trên toolbar.
- Thêm chức năng độc đáo: Khi cần bổ sung các hành động đặc thù mà các nút mặc định không có. Ví dụ như nút "Highlight" trong ví dụ của anh Creyt, hoặc "Dịch", "Tra từ điển", "Chia sẻ nhanh"...
- Cải thiện trải nghiệm người dùng trong các ứng dụng chuyên biệt: Ví dụ, một trình soạn thảo code có thể thêm nút "Refactor", "Format code" ngay trên thanh chọn văn bản để tăng năng suất cho developer.
- Cải thiện khả năng tiếp cận (Accessibility): Đôi khi, các controls mặc định có thể quá nhỏ hoặc khó nhìn đối với một số người dùng. Tùy chỉnh có thể giúp tạo ra các controls lớn hơn, tương phản tốt hơn, hoặc có biểu tượng dễ hiểu hơn.
Không nên dùng TextSelectionControls tùy chỉnh khi:
- Ứng dụng đơn giản, không có yêu cầu đặc biệt: Nếu ứng dụng chỉ cần các chức năng Copy/Paste cơ bản và không có yêu cầu về branding hay tính năng đặc thù, việc tùy chỉnh chỉ làm tốn thời gian và có thể gây ra lỗi không đáng có.
- Thời gian phát triển gấp rút: Việc tùy chỉnh UI luôn tốn thời gian và công sức kiểm thử. Nếu dự án đang "chạy deadline", hãy ưu tiên các chức năng cốt lõi trước.
- Tùy chỉnh mà không có kế hoạch rõ ràng: Việc tùy tiện thay đổi mà không có mục tiêu cụ thể dễ dẫn đến giao diện "lộn xộn", khó dùng và làm người dùng cảm thấy khó chịu. "Tùy biến mà không có chiến lược thì khác gì tự bắn vào chân mình đâu mấy đứa!"
"Tóm lại, TextSelectionControls là một công cụ mạnh mẽ, nhưng hãy sử dụng nó một cách khôn ngoan. Hãy tự hỏi: 'Cái này có thật sự làm cho người dùng sướng hơn không, hay chỉ làm cho mình vui thôi?' Chúc mấy đứa code vui!"
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é!