Adaptive TextField (#68918)
This commit is contained in:
parent
9e5e763ebe
commit
d3a8b03574
@ -25,6 +25,8 @@ import 'theme.dart';
|
||||
|
||||
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization, SmartQuotesType, SmartDashesType;
|
||||
|
||||
enum _TextFieldType { material, adaptive }
|
||||
|
||||
/// Signature for the [TextField.buildCounter] callback.
|
||||
typedef InputCounterWidgetBuilder = Widget? Function(
|
||||
/// The build context for the TextField.
|
||||
@ -380,7 +382,8 @@ class TextField extends StatefulWidget {
|
||||
this.scrollPhysics,
|
||||
this.autofillHints,
|
||||
this.restorationId,
|
||||
}) : assert(textAlign != null),
|
||||
}) : _textFieldType = _TextFieldType.material,
|
||||
assert(textAlign != null),
|
||||
assert(readOnly != null),
|
||||
assert(autofocus != null),
|
||||
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
|
||||
@ -427,6 +430,119 @@ class TextField extends StatefulWidget {
|
||||
)),
|
||||
super(key: key);
|
||||
|
||||
/// Creates a [CupertinoTextField] if the target platform is iOS, creates a
|
||||
/// material design text field otherwise.
|
||||
///
|
||||
/// To retain the standard look of [CupertinoTextField], this constructor only
|
||||
/// uses the [decoration] property's [decoration.hintText],
|
||||
/// [decoration.hintStyle], [decoration.suffix], and [decoration.prefix] for
|
||||
/// the iOS platform, and all else is ignored in support of the default iOS
|
||||
/// look. For instance, the [decoration.border] cannot override the default
|
||||
/// iOS-style border.
|
||||
///
|
||||
/// The target platform is based on the current [Theme]: [ThemeData.platform].
|
||||
const TextField.adaptive({
|
||||
Key? key,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.decoration = const InputDecoration(),
|
||||
TextInputType? keyboardType,
|
||||
this.textInputAction,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.style,
|
||||
this.strutStyle,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.textAlignVertical,
|
||||
this.textDirection,
|
||||
this.readOnly = false,
|
||||
ToolbarOptions? toolbarOptions,
|
||||
this.showCursor,
|
||||
this.autofocus = false,
|
||||
this.obscuringCharacter = '•',
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
SmartDashesType? smartDashesType,
|
||||
SmartQuotesType? smartQuotesType,
|
||||
this.enableSuggestions = true,
|
||||
this.maxLines = 1,
|
||||
this.minLines,
|
||||
this.expands = false,
|
||||
this.maxLength,
|
||||
this.maxLengthEnforced = true,
|
||||
this.onChanged,
|
||||
this.onEditingComplete,
|
||||
this.onSubmitted,
|
||||
this.onAppPrivateCommand,
|
||||
this.inputFormatters,
|
||||
this.enabled,
|
||||
this.cursorWidth = 2.0,
|
||||
this.cursorHeight,
|
||||
this.cursorRadius,
|
||||
this.cursorColor,
|
||||
this.selectionControls,
|
||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection = true,
|
||||
this.onTap,
|
||||
this.mouseCursor,
|
||||
this.buildCounter,
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
this.autofillHints,
|
||||
this.restorationId,
|
||||
}) : _textFieldType = _TextFieldType.adaptive,
|
||||
assert(textAlign != null),
|
||||
assert(readOnly != null),
|
||||
assert(autofocus != null),
|
||||
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
|
||||
assert(obscureText != null),
|
||||
assert(autocorrect != null),
|
||||
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||
assert(enableSuggestions != null),
|
||||
assert(enableInteractiveSelection != null),
|
||||
assert(maxLengthEnforced != null),
|
||||
assert(scrollPadding != null),
|
||||
assert(dragStartBehavior != null),
|
||||
assert(selectionHeightStyle != null),
|
||||
assert(selectionWidthStyle != null),
|
||||
assert(maxLines == null || maxLines > 0),
|
||||
assert(minLines == null || minLines > 0),
|
||||
assert(
|
||||
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||
"minLines can't be greater than maxLines",
|
||||
),
|
||||
assert(expands != null),
|
||||
assert(
|
||||
!expands || (maxLines == null && minLines == null),
|
||||
'minLines and maxLines must be null when expands is true.',
|
||||
),
|
||||
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
|
||||
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
|
||||
// Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
|
||||
assert(!identical(textInputAction, TextInputAction.newline) ||
|
||||
maxLines == 1 ||
|
||||
!identical(keyboardType, TextInputType.text),
|
||||
'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.'),
|
||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||
toolbarOptions = toolbarOptions ?? (obscureText ?
|
||||
const ToolbarOptions(
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
) :
|
||||
const ToolbarOptions(
|
||||
copy: true,
|
||||
cut: true,
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
)),
|
||||
super(key: key);
|
||||
|
||||
final _TextFieldType _textFieldType;
|
||||
|
||||
/// Controls the text being edited.
|
||||
///
|
||||
/// If null, this widget will create its own [TextEditingController].
|
||||
@ -1076,8 +1192,60 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget _buildCupertinoTextField(BuildContext context) {
|
||||
return CupertinoTextField(
|
||||
key: widget.key,
|
||||
controller: widget.controller,
|
||||
focusNode: widget.focusNode,
|
||||
placeholder: widget.decoration?.hintText,
|
||||
placeholderStyle: widget.decoration?.hintStyle ?? const TextStyle(fontWeight: FontWeight.w400, color: CupertinoColors.placeholderText),
|
||||
prefix: widget.decoration?.prefix,
|
||||
suffix: widget.decoration?.suffix,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.textInputAction,
|
||||
textCapitalization: widget.textCapitalization,
|
||||
style: widget.style,
|
||||
strutStyle: widget.strutStyle,
|
||||
textAlign: widget.textAlign,
|
||||
textAlignVertical: widget.textAlignVertical,
|
||||
readOnly: widget.readOnly,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
showCursor: widget.showCursor,
|
||||
autofocus: widget.autofocus,
|
||||
obscuringCharacter: widget.obscuringCharacter,
|
||||
obscureText: widget.obscureText,
|
||||
autocorrect: widget.autocorrect,
|
||||
smartDashesType: widget.smartDashesType,
|
||||
smartQuotesType: widget.smartQuotesType,
|
||||
enableSuggestions: widget.enableSuggestions,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
expands: widget.expands,
|
||||
maxLength: widget.maxLength,
|
||||
maxLengthEnforced: widget.maxLengthEnforced,
|
||||
onChanged: widget.onChanged,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
enabled: widget.enabled,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
cursorHeight: widget.cursorHeight,
|
||||
cursorColor: widget.cursorColor,
|
||||
selectionHeightStyle: widget.selectionHeightStyle,
|
||||
selectionWidthStyle: widget.selectionWidthStyle,
|
||||
keyboardAppearance: widget.keyboardAppearance,
|
||||
scrollPadding: widget.scrollPadding,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
onTap: widget.onTap,
|
||||
scrollController: widget.scrollController,
|
||||
scrollPhysics: widget.scrollPhysics,
|
||||
autofillHints: widget.autofillHints,
|
||||
restorationId: widget.restorationId,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMaterialTextField(BuildContext context) {
|
||||
assert(debugCheckHasMaterial(context));
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
@ -1255,4 +1423,27 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (widget._textFieldType) {
|
||||
case _TextFieldType.material:
|
||||
return _buildMaterialTextField(context);
|
||||
|
||||
case _TextFieldType.adaptive: {
|
||||
final ThemeData theme = Theme.of(context)!;
|
||||
assert(theme.platform != null);
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return _buildCupertinoTextField(context);
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return _buildMaterialTextField(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8528,4 +8528,205 @@ void main() {
|
||||
expect(inputWidth, wideWidth);
|
||||
expect(cursorRight, inputWidth - kCaretGap);
|
||||
});
|
||||
|
||||
testWidgets('Adaptive TextField displays CupertinoTextField in iOS',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Material(
|
||||
child: TextField.adaptive(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(CupertinoTextField), findsOneWidget);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform> {
|
||||
TargetPlatform.iOS,
|
||||
TargetPlatform.macOS,
|
||||
})
|
||||
);
|
||||
|
||||
testWidgets('Adaptive TextField does not display CupertinoTextField in non-iOS',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Material(
|
||||
child: TextField.adaptive(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(CupertinoTextField), findsNothing);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform> {
|
||||
TargetPlatform.android,
|
||||
TargetPlatform.fuchsia,
|
||||
TargetPlatform.windows,
|
||||
TargetPlatform.linux,
|
||||
}),
|
||||
);
|
||||
|
||||
testWidgets('Adaptive TextField in iOS with specified hintText',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Material(
|
||||
child: TextField.adaptive(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Hint',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Hint'), findsOneWidget);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform> {
|
||||
TargetPlatform.iOS,
|
||||
TargetPlatform.macOS,
|
||||
})
|
||||
);
|
||||
|
||||
testWidgets('Adaptive TextField in iOS cannot override iOS-specific decoration border',
|
||||
(WidgetTester tester) async {
|
||||
final BorderRadius borderRadius = BorderRadius.circular(0);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Material(
|
||||
child: TextField.adaptive(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Hint',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final CupertinoTextField textField = tester.widget(find.byType(CupertinoTextField));
|
||||
expect(textField.decoration!.borderRadius != borderRadius, isTrue);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform> {
|
||||
TargetPlatform.iOS,
|
||||
TargetPlatform.macOS,
|
||||
})
|
||||
);
|
||||
|
||||
testWidgets('Adaptive TextField in non-iOS can override decoration border',
|
||||
(WidgetTester tester) async {
|
||||
final OutlineInputBorder border = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(0),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Material(
|
||||
child: TextField.adaptive(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Hint',
|
||||
border: border,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.decoration!.border, border);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform> {
|
||||
TargetPlatform.android,
|
||||
TargetPlatform.fuchsia,
|
||||
TargetPlatform.windows,
|
||||
TargetPlatform.linux,
|
||||
})
|
||||
);
|
||||
|
||||
testWidgets('Adaptive TextField in iOS with specified hintStyle',
|
||||
(WidgetTester tester) async {
|
||||
final TextStyle hintStyle = TextStyle(
|
||||
inherit: false,
|
||||
color: Colors.pink[500],
|
||||
fontSize: 10.0,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Material(
|
||||
child: TextField.adaptive(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Hint',
|
||||
hintStyle: hintStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Text hintText = tester.widget(find.text('Hint'));
|
||||
expect(hintText.style, hintStyle);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform> {
|
||||
TargetPlatform.iOS,
|
||||
TargetPlatform.macOS,
|
||||
})
|
||||
);
|
||||
|
||||
testWidgets('Adaptive TextField in iOS with custom text style',
|
||||
(WidgetTester tester) async {
|
||||
final TextStyle style = TextStyle(
|
||||
color: Colors.pink[500],
|
||||
fontSize: 2.0,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
overlay(
|
||||
child: TextField.adaptive(
|
||||
controller: TextEditingController(text: 'Text'),
|
||||
style: style,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableText text = tester.widget(find.text('Text'));
|
||||
expect(text.style.color, style.color);
|
||||
expect(text.style.fontSize, style.fontSize);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform> {
|
||||
TargetPlatform.iOS,
|
||||
TargetPlatform.macOS,
|
||||
})
|
||||
);
|
||||
|
||||
testWidgets('Adaptive TextField in iOS with suffix',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
overlay(
|
||||
child: TextField.adaptive(
|
||||
controller: TextEditingController(text: 'Text'),
|
||||
decoration: const InputDecoration(
|
||||
suffix: Icon(Icons.phone),
|
||||
prefix: Icon(Icons.message),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byIcon(Icons.phone), findsOneWidget);
|
||||
expect(find.byIcon(Icons.message), findsOneWidget);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform> {
|
||||
TargetPlatform.iOS,
|
||||
TargetPlatform.macOS,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user