Skip to content

Commit f3f72ed

Browse files
Renzo-OlivaresRenzo Olivares
and
Renzo Olivares
authored
Add SelectionListener/SelectedContentRange (flutter#154202)
https://github.com/user-attachments/assets/59225cf7-5506-414e-87da-aa4d3227e7f6 Adds: * `SelectionListener`, allows a user to listen to selection changes under the subtree it wraps given their is an ancestor `SelectionArea` or `SelectableRegion`. These selection changes can be listened to through the `SelectionListenerNotifier` that is provided to a `SelectionListener`. * `SelectionListenerNotifier`, used with `SelectionListener`, allows a user listen to selection changes for the subtree of the `SelectionListener` it was provided to. Provides access to individual selection values through the `SelectionDetails` object `selection`. * `SelectableRegionSelectionStatusScope`, allows the user to listen to when a parent `SelectableRegion` is changing or finalizing the selection. * `SelectedContentRange`, provides information about the selection range under a `SelectionHandler` or `Selectable` through the `getSelection()` method. This includes a start and end offset relative to the `Selectable`s content. * `SelectionHandler.contentLength`, to describe the length of the content contained by a selectable. Original PR & Discussion: flutter#148998 Fixes: flutter#110594 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Renzo Olivares <[email protected]>
1 parent 4051a38 commit f3f72ed

File tree

14 files changed

+1664
-34
lines changed

14 files changed

+1664
-34
lines changed

examples/api/lib/material/selectable_region/selectable_region.0.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class _RenderSelectableAdapter extends RenderProxyBox with Selectable, Selection
9494
final ValueNotifier<SelectionGeometry> _geometry;
9595

9696
Color get selectionColor => _selectionColor;
97-
late Color _selectionColor;
97+
Color _selectionColor;
9898
set selectionColor(Color value) {
9999
if (_selectionColor == value) {
100100
return;
@@ -272,6 +272,20 @@ class _RenderSelectableAdapter extends RenderProxyBox with Selectable, Selection
272272
return value.hasSelection ? const SelectedContent(plainText: 'Custom Text') : null;
273273
}
274274

275+
@override
276+
SelectedContentRange? getSelection() {
277+
if (!value.hasSelection) {
278+
return null;
279+
}
280+
return const SelectedContentRange(
281+
startOffset: 0,
282+
endOffset: 1,
283+
);
284+
}
285+
286+
@override
287+
int get contentLength => 1;
288+
275289
LayerLink? _startHandle;
276290
LayerLink? _endHandle;
277291

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/foundation.dart';
6+
import 'package:flutter/material.dart';
7+
8+
/// Flutter code sample for [SelectionArea].
9+
10+
void main() => runApp(const SelectionAreaSelectionListenerExampleApp());
11+
12+
class SelectionAreaSelectionListenerExampleApp extends StatelessWidget {
13+
const SelectionAreaSelectionListenerExampleApp({super.key});
14+
15+
@override
16+
Widget build(BuildContext context) {
17+
return MaterialApp(
18+
title: 'Flutter Demo',
19+
theme: ThemeData(
20+
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
21+
useMaterial3: true,
22+
),
23+
home: const MyHomePage(title: 'Flutter Demo Home Page'),
24+
);
25+
}
26+
}
27+
28+
class MyHomePage extends StatefulWidget {
29+
const MyHomePage({super.key, required this.title});
30+
31+
final String title;
32+
33+
@override
34+
State<MyHomePage> createState() => _MyHomePageState();
35+
}
36+
37+
class _MyHomePageState extends State<MyHomePage> {
38+
final SelectionListenerNotifier _selectionNotifier = SelectionListenerNotifier();
39+
SelectableRegionSelectionStatus? _selectableRegionStatus;
40+
41+
void _handleOnSelectionStateChanged(SelectableRegionSelectionStatus status) {
42+
setState(() {
43+
_selectableRegionStatus = status;
44+
});
45+
}
46+
47+
@override
48+
void dispose() {
49+
_selectionNotifier.dispose();
50+
_selectableRegionStatus = null;
51+
super.dispose();
52+
}
53+
54+
@override
55+
Widget build(BuildContext context) {
56+
return Scaffold(
57+
appBar: AppBar(
58+
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
59+
title: Text(widget.title),
60+
),
61+
body: Center(
62+
child: Column(
63+
children: <Widget>[
64+
Column(
65+
crossAxisAlignment: CrossAxisAlignment.start,
66+
children: <Widget>[
67+
for (final (int? offset, String label) in <(int? offset, String label)>[
68+
(_selectionNotifier.registered ? _selectionNotifier.selection.range?.startOffset : null, 'StartOffset'),
69+
(_selectionNotifier.registered ? _selectionNotifier.selection.range?.endOffset : null, 'EndOffset'),
70+
])
71+
Text('Selection $label: $offset'),
72+
Text('Selection Status: ${_selectionNotifier.registered ? _selectionNotifier.selection.status : 'SelectionListenerNotifier not registered.'}'),
73+
Text('Selectable Region Status: $_selectableRegionStatus'),
74+
],
75+
),
76+
const SizedBox(height: 15.0,),
77+
SelectionArea(
78+
child: MySelectableText(
79+
selectionNotifier: _selectionNotifier,
80+
onChanged: _handleOnSelectionStateChanged,
81+
),
82+
),
83+
],
84+
),
85+
),
86+
);
87+
}
88+
}
89+
90+
class MySelectableText extends StatefulWidget {
91+
const MySelectableText({
92+
super.key,
93+
required this.selectionNotifier,
94+
required this.onChanged,
95+
});
96+
97+
final SelectionListenerNotifier selectionNotifier;
98+
final ValueChanged<SelectableRegionSelectionStatus> onChanged;
99+
100+
@override
101+
State<MySelectableText> createState() => _MySelectableTextState();
102+
}
103+
104+
class _MySelectableTextState extends State<MySelectableText> {
105+
ValueListenable<SelectableRegionSelectionStatus>? _selectableRegionScope;
106+
107+
void _handleOnSelectableRegionChanged() {
108+
if (_selectableRegionScope == null) {
109+
return;
110+
}
111+
widget.onChanged.call(_selectableRegionScope!.value);
112+
}
113+
114+
@override
115+
void didChangeDependencies() {
116+
super.didChangeDependencies();
117+
_selectableRegionScope?.removeListener(_handleOnSelectableRegionChanged);
118+
_selectableRegionScope = SelectableRegionSelectionStatusScope.maybeOf(context);
119+
_selectableRegionScope?.addListener(_handleOnSelectableRegionChanged);
120+
}
121+
122+
@override
123+
void dispose() {
124+
_selectableRegionScope?.removeListener(_handleOnSelectableRegionChanged);
125+
_selectableRegionScope = null;
126+
super.dispose();
127+
}
128+
129+
@override
130+
Widget build(BuildContext context) {
131+
return SelectionListener(
132+
selectionNotifier: widget.selectionNotifier,
133+
child: const Text('This is some text under a SelectionArea that can be selected.'),
134+
);
135+
}
136+
}

0 commit comments

Comments
 (0)