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;
|
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization, SmartQuotesType, SmartDashesType;
|
||||||
|
|
||||||
|
enum _TextFieldType { material, adaptive }
|
||||||
|
|
||||||
/// Signature for the [TextField.buildCounter] callback.
|
/// Signature for the [TextField.buildCounter] callback.
|
||||||
typedef InputCounterWidgetBuilder = Widget? Function(
|
typedef InputCounterWidgetBuilder = Widget? Function(
|
||||||
/// The build context for the TextField.
|
/// The build context for the TextField.
|
||||||
@ -380,7 +382,8 @@ class TextField extends StatefulWidget {
|
|||||||
this.scrollPhysics,
|
this.scrollPhysics,
|
||||||
this.autofillHints,
|
this.autofillHints,
|
||||||
this.restorationId,
|
this.restorationId,
|
||||||
}) : assert(textAlign != null),
|
}) : _textFieldType = _TextFieldType.material,
|
||||||
|
assert(textAlign != null),
|
||||||
assert(readOnly != null),
|
assert(readOnly != null),
|
||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
|
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
|
||||||
@ -427,6 +430,119 @@ class TextField extends StatefulWidget {
|
|||||||
)),
|
)),
|
||||||
super(key: key);
|
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.
|
/// Controls the text being edited.
|
||||||
///
|
///
|
||||||
/// If null, this widget will create its own [TextEditingController].
|
/// If null, this widget will create its own [TextEditingController].
|
||||||
@ -1076,8 +1192,60 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Widget _buildCupertinoTextField(BuildContext context) {
|
||||||
Widget build(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(debugCheckHasMaterial(context));
|
||||||
assert(debugCheckHasMaterialLocalizations(context));
|
assert(debugCheckHasMaterialLocalizations(context));
|
||||||
assert(debugCheckHasDirectionality(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(inputWidth, wideWidth);
|
||||||
expect(cursorRight, inputWidth - kCaretGap);
|
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