Skip to content

Commit e0ad129

Browse files
authored
[framework] Add textField OCR support for framework side (#96637)
iOS OCR keyboard input support.
1 parent 2f7614a commit e0ad129

File tree

97 files changed

+868
-46
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+868
-46
lines changed

packages/flutter/lib/services.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export 'src/services/hardware_keyboard.dart';
2525
export 'src/services/keyboard_inserted_content.dart';
2626
export 'src/services/keyboard_key.g.dart';
2727
export 'src/services/keyboard_maps.g.dart';
28+
export 'src/services/live_text.dart';
2829
export 'src/services/message_codec.dart';
2930
export 'src/services/message_codecs.dart';
3031
export 'src/services/mouse_cursor.dart';

packages/flutter/lib/src/cupertino/adaptive_text_selection_toolbar.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget {
9494
required VoidCallback? onCut,
9595
required VoidCallback? onPaste,
9696
required VoidCallback? onSelectAll,
97+
required VoidCallback? onLiveTextInput,
9798
required this.anchors,
9899
}) : children = null,
99100
buttonItems = EditableText.getEditableButtonItems(
@@ -102,6 +103,7 @@ class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget {
102103
onCut: onCut,
103104
onPaste: onPaste,
104105
onSelectAll: onSelectAll,
106+
onLiveTextInput: onLiveTextInput
105107
);
106108

107109
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the

packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:math';
6+
57
import 'package:flutter/widgets.dart';
68

79
import 'button.dart';
@@ -103,6 +105,7 @@ class CupertinoTextSelectionToolbarButton extends StatefulWidget {
103105
return localizations.pasteButtonLabel;
104106
case ContextMenuButtonType.selectAll:
105107
return localizations.selectAllButtonLabel;
108+
case ContextMenuButtonType.liveTextInput:
106109
case ContextMenuButtonType.delete:
107110
case ContextMenuButtonType.custom:
108111
return '';
@@ -131,6 +134,7 @@ class _CupertinoTextSelectionToolbarButtonState extends State<CupertinoTextSelec
131134

132135
@override
133136
Widget build(BuildContext context) {
137+
final Widget content = _getContentWidget(context);
134138
final Widget child = CupertinoButton(
135139
color: isPressed
136140
? _kToolbarPressedColor.resolveFrom(context)
@@ -145,15 +149,7 @@ class _CupertinoTextSelectionToolbarButtonState extends State<CupertinoTextSelec
145149
// There's no foreground fade on iOS toolbar anymore, just the background
146150
// is darkened.
147151
pressedOpacity: 1.0,
148-
child: widget.child ?? Text(
149-
widget.text ?? CupertinoTextSelectionToolbarButton.getButtonLabel(context, widget.buttonItem!),
150-
overflow: TextOverflow.ellipsis,
151-
style: _kToolbarButtonFontStyle.copyWith(
152-
color: widget.onPressed != null
153-
? _kToolbarTextColor.resolveFrom(context)
154-
: CupertinoColors.inactiveGray,
155-
),
156-
),
152+
child: content,
157153
);
158154

159155
if (widget.onPressed != null) {
@@ -170,4 +166,85 @@ class _CupertinoTextSelectionToolbarButtonState extends State<CupertinoTextSelec
170166
return child;
171167
}
172168
}
169+
170+
Widget _getContentWidget(BuildContext context) {
171+
if (widget.child != null) {
172+
return widget.child!;
173+
}
174+
final Widget textWidget = Text(
175+
widget.text ?? CupertinoTextSelectionToolbarButton.getButtonLabel(context, widget.buttonItem!),
176+
overflow: TextOverflow.ellipsis,
177+
style: _kToolbarButtonFontStyle.copyWith(
178+
color: widget.onPressed != null
179+
? _kToolbarTextColor.resolveFrom(context)
180+
: CupertinoColors.inactiveGray,
181+
),
182+
);
183+
if (widget.buttonItem == null) {
184+
return textWidget;
185+
}
186+
switch (widget.buttonItem!.type) {
187+
case ContextMenuButtonType.cut:
188+
case ContextMenuButtonType.copy:
189+
case ContextMenuButtonType.paste:
190+
case ContextMenuButtonType.selectAll:
191+
case ContextMenuButtonType.delete:
192+
case ContextMenuButtonType.custom:
193+
return textWidget;
194+
case ContextMenuButtonType.liveTextInput:
195+
return SizedBox(
196+
width: 13.0,
197+
height: 13.0,
198+
child: CustomPaint(
199+
painter: _LiveTextIconPainter(color: _kToolbarTextColor.resolveFrom(context)),
200+
),
201+
);
202+
}
203+
}
204+
}
205+
206+
class _LiveTextIconPainter extends CustomPainter {
207+
_LiveTextIconPainter({required this.color});
208+
209+
final Color color;
210+
211+
final Paint _painter = Paint()
212+
..strokeCap = StrokeCap.round
213+
..strokeJoin = StrokeJoin.round
214+
..strokeWidth = 1.0
215+
..style = PaintingStyle.stroke;
216+
217+
@override
218+
void paint(Canvas canvas, Size size) {
219+
_painter.color = color;
220+
canvas.save();
221+
canvas.translate(size.width / 2.0, size.height / 2.0);
222+
223+
final Offset origin = Offset(-size.width / 2.0, -size.height / 2.0);
224+
// Path for the one corner.
225+
final Path path = Path()
226+
..moveTo(origin.dx, origin.dy + 3.5)
227+
..lineTo(origin.dx, origin.dy + 1.0)
228+
..arcToPoint(Offset(origin.dx + 1.0, origin.dy), radius: const Radius.circular(1))
229+
..lineTo(origin.dx + 3.5, origin.dy);
230+
231+
// Rotate to draw corner four times.
232+
final Matrix4 rotationMatrix = Matrix4.identity()..rotateZ(pi / 2.0);
233+
for (int i = 0; i < 4; i += 1) {
234+
canvas.drawPath(path, _painter);
235+
canvas.transform(rotationMatrix.storage);
236+
}
237+
238+
// Draw three lines.
239+
canvas.drawLine(const Offset(-3.0, -3.0), const Offset(3.0, -3.0), _painter);
240+
canvas.drawLine(const Offset(-3.0, 0.0), const Offset(3.0, 0.0), _painter);
241+
canvas.drawLine(const Offset(-3.0, 3.0), const Offset(1.0, 3.0), _painter);
242+
243+
canvas.restore();
244+
}
245+
246+
@override
247+
bool shouldRepaint(covariant _LiveTextIconPainter oldDelegate) {
248+
return oldDelegate.color != color;
249+
}
173250
}

packages/flutter/lib/src/material/adaptive_text_selection_toolbar.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget {
103103
required VoidCallback? onCut,
104104
required VoidCallback? onPaste,
105105
required VoidCallback? onSelectAll,
106+
required VoidCallback? onLiveTextInput,
106107
required this.anchors,
107108
}) : children = null,
108109
buttonItems = EditableText.getEditableButtonItems(
@@ -111,6 +112,7 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget {
111112
onCut: onCut,
112113
onPaste: onPaste,
113114
onSelectAll: onSelectAll,
115+
onLiveTextInput: onLiveTextInput
114116
);
115117

116118
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
@@ -213,6 +215,8 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget {
213215
return localizations.selectAllButtonLabel;
214216
case ContextMenuButtonType.delete:
215217
return localizations.deleteButtonTooltip.toUpperCase();
218+
case ContextMenuButtonType.liveTextInput:
219+
return localizations.scanTextButtonLabel;
216220
case ContextMenuButtonType.custom:
217221
return '';
218222
}
@@ -242,9 +246,8 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget {
242246
switch (Theme.of(context).platform) {
243247
case TargetPlatform.iOS:
244248
return buttonItems.map((ContextMenuButtonItem buttonItem) {
245-
return CupertinoTextSelectionToolbarButton.text(
246-
onPressed: buttonItem.onPressed,
247-
text: getButtonLabel(context, buttonItem),
249+
return CupertinoTextSelectionToolbarButton.buttonItem(
250+
buttonItem: buttonItem,
248251
);
249252
});
250253
case TargetPlatform.fuchsia:

packages/flutter/lib/src/material/material_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ abstract class MaterialLocalizations {
103103
/// Label for "cut" edit buttons and menu items.
104104
String get cutButtonLabel;
105105

106+
/// Label for "scan text" OCR edit buttons and menu items.
107+
String get scanTextButtonLabel;
108+
106109
/// Label for OK buttons and menu items.
107110
String get okButtonLabel;
108111

@@ -1159,6 +1162,9 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
11591162
@override
11601163
String get cutButtonLabel => 'Cut';
11611164

1165+
@override
1166+
String get scanTextButtonLabel => 'Scan text';
1167+
11621168
@override
11631169
String get okButtonLabel => 'OK';
11641170

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 'system_channels.dart';
6+
7+
/// Utility methods for interacting with the system's Live Text.
8+
///
9+
/// For example, the Live Text input feature of iOS turns the keyboard into a camera view for
10+
/// directly inserting text obtained through OCR into the active field.
11+
///
12+
/// See also:
13+
/// * <https://developer.apple.com/documentation/uikit/uiresponder/3778577-capturetextfromcamera>
14+
/// * <https://support.apple.com/guide/iphone/use-live-text-iphcf0b71b0e/ios>
15+
class LiveText {
16+
// This class is not meant to be instantiated or extended; this constructor
17+
// prevents instantiation and extension.
18+
LiveText._();
19+
20+
/// Returns true if the Live Text input feature is available on the current device.
21+
static Future<bool> isLiveTextInputAvailable() async {
22+
final bool supportLiveTextInput =
23+
await SystemChannels.platform.invokeMethod('LiveText.isLiveTextInputAvailable') ?? false;
24+
return supportLiveTextInput;
25+
}
26+
27+
/// Start Live Text input.
28+
///
29+
/// If any [TextInputConnection] is currently active, calling this method will tell the text field
30+
/// to start Live Text input. If the current device doesn't support Live Text input,
31+
/// nothing will happen.
32+
static void startLiveTextInput() {
33+
SystemChannels.textInput.invokeMethod('TextInput.startLiveTextInput');
34+
}
35+
}

packages/flutter/lib/src/services/text_input.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,13 @@ mixin TextSelectionDelegate {
10501050
/// Whether select all is enabled, must not be null.
10511051
bool get selectAllEnabled => true;
10521052

1053+
/// Whether Live Text input is enabled.
1054+
///
1055+
/// See also:
1056+
/// * [LiveText], where the availability of Live Text input can be obtained.
1057+
/// * [LiveTextInputStatusNotifier], where the status of Live Text can be listened to.
1058+
bool get liveTextInputEnabled => false;
1059+
10531060
/// Cut current selection to [Clipboard].
10541061
///
10551062
/// If and only if [cause] is [SelectionChangedCause.toolbar], the toolbar

packages/flutter/lib/src/widgets/context_menu_button_item.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ enum ContextMenuButtonType {
2626
/// A button that deletes the current text selection.
2727
delete,
2828

29+
/// A button for starting Live Text input.
30+
///
31+
/// See also:
32+
/// * [LiveText], where the availability of Live Text input can be obtained.
33+
/// * [LiveTextInputStatusNotifier], where the status of Live Text can be listened to.
34+
liveTextInput,
35+
2936
/// Anything other than the default button types.
3037
custom,
3138
}

0 commit comments

Comments
 (0)