概要
通过SelectionArea实现一个可复用的文本的全选、复制功能的widget组件
实现思路
目标明确,为了以后的易用性、可扩展性,单独抽出来一个widget类,功能内聚。
- 对外提供一个方法wrap,接受一个子widget(Text文本)、是否全选变量selectAll、是否复制变量copy,通过这两个变量控制是否显示全选、复制功能
- 当前文本是否全选变量selectAllEnable,只有当selectAll=true&&selectAllEnable=true时,显示全选功能
源码分析
1.SelectionArea源码:
const SelectionArea({
super.key,
this.focusNode,
this.selectionControls,
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.magnifierConfiguration,
this.onSelectionChanged,
required this.child,
});
分析一下我们需要的参数child必转、onSelectionChanged选中文本回调、contextMenuBuilder 菜单列表。
2.onSelectionChanged源码:
final ValueChanged<SelectedContent?>? onSelectionChanged;
回调回来一个可空的SelectedContent
3.contextMenuBuilder源码:
static Widget _defaultContextMenuBuilder(BuildContext context, SelectableRegionState selectableRegionState) {
return AdaptiveTextSelectionToolbar.selectableRegion(
selectableRegionState: selectableRegionState,
);
}
(1)、讲一下SelectableRegionState参数:
//1.实现TextSelectionDelegate 接口的全选、复制
class SelectableRegionState extends State<SelectableRegion> with TextSelectionDelegate implements SelectionRegistrar {
//2._SelectAllAction、_CopySelectionAction放Map里准备初始化
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
...
};
void selectAll([SelectionChangedCause? cause]) {
_clearSelection();
_selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent());
if (cause == SelectionChangedCause.toolbar) {
_showToolbar();
_showHandles();
}
_updateSelectedContentIfNeeded();
}
(
'Use `contextMenuBuilder` instead. '
'This feature was deprecated after v3.3.0-0.5.pre.',
)
void copySelection(SelectionChangedCause cause) {
_copy();
_clearSelection();
}
Widget build(BuildContext context) {
assert(debugCheckHasOverlay(context));
Widget result = SelectionContainer(
registrar: this,
delegate: _selectionDelegate,
child: widget.child,
);
if (kIsWeb) {
result = PlatformSelectableRegionContextMenu(
child: result,
);
}
return CompositedTransformTarget(
link: _toolbarLayerLink,
child: RawGestureDetector(
gestures: _gestureRecognizers,
behavior: HitTestBehavior.translucent,
excludeFromSemantics: true,
child: Actions(
actions: _actions,//初始化_actions如:全选、复制
child: Focus(
includeSemantics: false,
focusNode: widget.focusNode,
child: result,
),
),
),
);
}
}
- TextSelectionDelegate属于text_input类
mixin TextSelectionDelegate {
...
//全选
void selectAll(SelectionChangedCause cause);
//复制
void copySelection(SelectionChangedCause cause);
}
- _SelectAllAction、_CopySelectionAction
class _SelectAllAction extends _NonOverrideAction<SelectAllTextIntent> {
_SelectAllAction(this.state);
final SelectableRegionState state;
//invokeAction回调到SelectableRegionState的selectAll实现全选
void invokeAction(SelectAllTextIntent intent, [BuildContext? context]) {
state.selectAll(SelectionChangedCause.keyboard);
}
}
class _CopySelectionAction extends _NonOverrideAction<CopySelectionTextIntent> {
_CopySelectionAction(this.state);
final SelectableRegionState state;
//invokeAction回调到SelectableRegionState的_copy实现复制
void invokeAction(CopySelectionTextIntent intent, [BuildContext? context]) {
state._copy();
}
}
(2)、_defaultContextMenuBuilder返回的是一个widget
我们最终返回AdaptiveTextSelectionToolbar.buttonItems:
const AdaptiveTextSelectionToolbar.buttonItems({
super.key,
required this.buttonItems,
required this.anchors,
}) : children = null;
buttonItems是ContextMenuButtonItem菜单集合,anchors是 contextMenuAnchors。
具体实现
main.dart代码
import 'package:flutter/material.dart';
import 'package:selection_area/ib_selection_area.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'SelectionArea实现文本全选复制功能',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'SelectionArea实现文本全选复制功能'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final String content =
"5G的设想最早在2008年就由美国提出来了,但是当时4G还没有建设好,因此各国真正坐下来讨论5G的标准其实是在2012年前后。\n注:主管标准制定的机构叫做3GPP(第三代合作伙伴计划),它成立于1998年,最初只是为各国协调3G通信标准的组织,后来就负责其历代移动通信标准的制定了。今天世界上主要的通信厂家,包括中国的华为等企业,都在其中。 标准的制定过程和我们编写软件,或者写书很相像。参与者各自提供自己的想法,大家讨论、修改,形成共同的意见。由于技术不断发展,大家的想法也在改变,运营商的需求也不断增加,因此总有新的东西加进来。于是,主管标准制定的机构也就是3GPP,在某一个阶段,就必须冻结所有的需求,然后发布一个版本,叫做一个Release,中文常常把它简写成R。\n关于5G的标准,目前大家讨论的是15版(3GPP-R15)。在这个标准中,将5G的建设分为了两步走,这两步走得都很艰辛。第一步经过78次开会,无数的讨价还价和妥协,最后在去年底总算是确定下来了。第二步,在2019年6月份才确定。注:第一步是所谓的非独立组网模式NSA,即采用现有4G作为核心网,4G为主,5G为辅,对应的标准则是3GPP-R15-NSA,这是设想的前期做法。第二步是独立组网模式SA,5G作为核心网,只有5G基站工作,对应的标准是3GPP-R15-SA。这部分标准2019年6月份在高通所在地圣地亚哥才正式确定。";
get _listView => ListView(
padding:
const EdgeInsets.only(left: 12, right: 12, top: 15, bottom: 15),
children: [_textContent],
);
get _textContent => IBSelectionArea.wrap(Text(
content,
style: const TextStyle(fontSize: 16),
));
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: _listView,
);
}
}
ib_selection_area.dart代码
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
///文本全选、复制
class IBSelectionArea {
static Widget wrap(Text child, {bool selectAll = true, bool copy = true}) {
//选择的文本
var selectedText = '';
return SelectionArea(
child: child,
onSelectionChanged: (SelectedContent? selectContent) =>
selectedText = selectContent?.plainText ?? "",
contextMenuBuilder:
(BuildContext context, SelectableRegionState selectableRegionState) {
bool selectAllEnable = false;
//如果还有可选的内容则展示全选菜单
if (selectedText.length < (child.data?.length ?? 0)) {
selectAllEnable = true;
}
final List<ContextMenuButtonItem> buttonItems = [
if (selectAll && selectAllEnable)
ContextMenuButtonItem(
label: '全选',
onPressed: () {
selectableRegionState
.selectAll(SelectionChangedCause.toolbar);
}),
if (copy)
ContextMenuButtonItem(
label: '复制',
onPressed: () {
selectableRegionState
.copySelection(SelectionChangedCause.toolbar);
})
];
return AdaptiveTextSelectionToolbar.buttonItems(
buttonItems: buttonItems,
anchors: selectableRegionState.contextMenuAnchors,
);
},
);
}
}