
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ừ TextSelectionControls và override 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
MyCustomTextSelectionControlskế 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àutertiarycủaColorSchemeđể 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ácTextSelectionToolbarTextButtonhoặc bất kỳ widget nào khác vào danh sáchchildrencủaTextSelectionToolbar. Ở đây anh Creyt đã thêm nútSearch 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ằngCustomPaintvà_TextSelectionHandlePainter.- Cuối cùng, mình dùng
SelectionAreawidget và truyềnMyCustomTextSelectionControlsvào thuộc tínhselectionControlsđể áp dụng các tùy chỉnh này cho cácSelectableTextbên trong nó. Đối vớiTextField, mình có thể truyền trực tiếp vàoselectionControlscủaTextField.

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.
TextSelectionOverlaycó 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ó
Copymà còn có các tùy chọnBold,Italic,Highlight,Comment, hoặc thậm chí làTurn into block. Đó chính là tùy biếnTextSelectionControlsđó! - 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, hayShare 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
buildToolbarphá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útTra 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útShare 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é!