Fix broken text field with set hint and min and max lines(#153183) (#153235)

Fix https://github.com/flutter/flutter/issues/153183
Fix https://github.com/flutter/flutter/issues/52008

Pre-launch Checklist
This commit is contained in:
zhengzeqin 2024-09-27 06:08:05 +08:00 committed by GitHub
parent feb9c59284
commit 7f663fc1d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 155 additions and 8 deletions

View File

@ -2163,20 +2163,37 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
final TextStyle hintStyle = _getInlineHintStyle(themeData, defaults);
final String? hintText = decoration.hintText;
final Widget? hint = hintText == null ? null : AnimatedOpacity(
opacity: (isEmpty && !_hasInlineLabel) ? 1.0 : 0.0,
duration: decoration.hintFadeDuration ?? _kHintFadeTransitionDuration,
curve: _kTransitionCurve,
child: Text(
final bool maintainHintHeight = decoration.maintainHintHeight;
Widget? hint;
if (hintText != null) {
final bool showHint = isEmpty && !_hasInlineLabel;
final Text hintTextWidget = Text(
hintText,
style: hintStyle,
textDirection: decoration.hintTextDirection,
overflow: hintStyle.overflow ?? (decoration.hintMaxLines == null ? null : TextOverflow.ellipsis),
textAlign: textAlign,
maxLines: decoration.hintMaxLines,
),
);
);
hint = maintainHintHeight ? AnimatedOpacity(
opacity: showHint ? 1.0 : 0.0,
duration: decoration.hintFadeDuration ?? _kHintFadeTransitionDuration,
curve: _kTransitionCurve,
child: hintTextWidget,
) : AnimatedSwitcher(
duration: decoration.hintFadeDuration ?? _kHintFadeTransitionDuration,
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: _kTransitionCurve,
),
child: child,
);
},
child: showHint ? hintTextWidget : const SizedBox.shrink(),
);
}
InputBorder? border;
if (!decoration.enabled) {
border = _hasError ? decoration.errorBorder : decoration.disabledBorder;
@ -2574,6 +2591,7 @@ class InputDecoration {
this.hintTextDirection,
this.hintMaxLines,
this.hintFadeDuration,
this.maintainHintHeight = true,
this.error,
this.errorText,
this.errorStyle,
@ -2643,6 +2661,7 @@ class InputDecoration {
this.hintTextDirection,
this.hintMaxLines,
this.hintFadeDuration,
this.maintainHintHeight = true,
this.filled = false,
this.fillColor,
this.focusColor,
@ -2904,6 +2923,15 @@ class InputDecoration {
/// If [InputDecorationTheme.hintFadeDuration] is null defaults to 20ms.
final Duration? hintFadeDuration;
/// Whether the input field's height should always be greater than or equal to
/// the height of the [hintText], even if the [hintText] is not visible.
///
/// The [InputDecorator] widget ignores [hintText] during layout when
/// it's not visible, if this flag is set to false.
///
/// Defaults to true.
final bool maintainHintHeight;
/// Optional widget that appears below the [InputDecorator.child] and the border.
///
/// If non-null, the border's color animates to red and the [helperText] is not shown.
@ -3578,6 +3606,7 @@ class InputDecoration {
TextDirection? hintTextDirection,
Duration? hintFadeDuration,
int? hintMaxLines,
bool? maintainHintHeight,
Widget? error,
String? errorText,
TextStyle? errorStyle,
@ -3633,6 +3662,7 @@ class InputDecoration {
hintTextDirection: hintTextDirection ?? this.hintTextDirection,
hintMaxLines: hintMaxLines ?? this.hintMaxLines,
hintFadeDuration: hintFadeDuration ?? this.hintFadeDuration,
maintainHintHeight: maintainHintHeight ?? this.maintainHintHeight,
error: error ?? this.error,
errorText: errorText ?? this.errorText,
errorStyle: errorStyle ?? this.errorStyle,
@ -3741,6 +3771,7 @@ class InputDecoration {
&& other.hintTextDirection == hintTextDirection
&& other.hintMaxLines == hintMaxLines
&& other.hintFadeDuration == hintFadeDuration
&& other.maintainHintHeight == maintainHintHeight
&& other.error == error
&& other.errorText == errorText
&& other.errorStyle == errorStyle
@ -3799,6 +3830,7 @@ class InputDecoration {
hintTextDirection,
hintMaxLines,
hintFadeDuration,
maintainHintHeight,
error,
errorText,
errorStyle,
@ -3855,6 +3887,7 @@ class InputDecoration {
if (hintText != null) 'hintText: "$hintText"',
if (hintMaxLines != null) 'hintMaxLines: "$hintMaxLines"',
if (hintFadeDuration != null) 'hintFadeDuration: "$hintFadeDuration"',
if (!maintainHintHeight) 'maintainHintHeight: false',
if (error != null) 'error: "$error"',
if (errorText != null) 'errorText: "$errorText"',
if (errorStyle != null) 'errorStyle: "$errorStyle"',

View File

@ -4505,6 +4505,120 @@ void main() {
final Text hintTextWidget = tester.widget(hintTextFinder);
expect(hintTextWidget.style!.overflow, decoration.hintStyle!.overflow);
});
testWidgets('Widget height collapses from hint height when maintainHintHeight is false', (WidgetTester tester) async {
final String hintText = 'hint' * 20;
final InputDecoration decoration = InputDecoration(
hintText: hintText,
hintMaxLines: 3,
maintainHintHeight: false,
);
await tester.pumpWidget(
buildInputDecorator(
decoration: decoration,
),
);
expect(tester.getSize(find.byType(InputDecorator)).height, 48.0);
});
testWidgets('Widget height stays at hint height by default', (WidgetTester tester) async {
final String hintText = 'hint' * 20;
final InputDecoration decoration = InputDecoration(
hintMaxLines: 3,
hintText: hintText,
);
await tester.pumpWidget(
buildInputDecorator(
decoration: decoration,
),
);
final double hintHeight = tester.getSize(find.text(hintText)).height;
final double inputHeight = tester.getSize(find.byType(InputDecorator)).height;
expect(inputHeight, hintHeight + 16.0);
});
testWidgets('hintFadeDuration applies to hint fade-in when maintainHintHeight is false', (WidgetTester tester) async {
const InputDecoration decoration = InputDecoration(
hintText: hintText,
hintMaxLines: 3,
hintFadeDuration: Duration(milliseconds: 120),
maintainHintHeight: false,
);
// Build once with empty content.
await tester.pumpWidget(
buildInputDecorator(
decoration: decoration,
),
);
// Hint is not exist.
expect(find.text(hintText), findsNothing);
// Rebuild with empty content.
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
decoration: decoration,
),
);
// The hint's opacity animates from 0.0 to 1.0.
// The animation's default duration is 20ms.
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getHintOpacity(tester);
expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0));
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity100ms = getHintOpacity(tester);
expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0));
await tester.pump(const Duration(milliseconds: 20));
final double hintOpacity120ms = getHintOpacity(tester);
expect(hintOpacity120ms, 1.0);
});
testWidgets('hintFadeDuration applies to hint fade-out when maintainHintHeight is false', (WidgetTester tester) async {
const InputDecoration decoration = InputDecoration(
hintText: hintText,
hintMaxLines: 3,
hintFadeDuration: Duration(milliseconds: 120),
maintainHintHeight: false,
);
// Build once with empty content.
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
decoration: decoration,
),
);
// Hint is visible (opacity 1.0).
expect(getHintOpacity(tester), 1.0);
// Rebuild with non-empty content.
await tester.pumpWidget(
buildInputDecorator(
decoration: decoration,
),
);
// The hint's opacity animates from 1.0 to 0.0.
// The animation's default duration is 20ms.
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getHintOpacity(tester);
expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0));
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity100ms = getHintOpacity(tester);
expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms));
await tester.pump(const Duration(milliseconds: 20));
final double hintOpacity120ms = getHintOpacity(tester);
expect(hintOpacity120ms, 0);
await tester.pump(const Duration(milliseconds: 1));
// The hintText replaced with SizeBox.
expect(find.text(hintText), findsNothing);
});
});
group('Material3 - InputDecoration helper/counter/error', () {