feat: Add hint (Widget) property to InputDecoration (#161424)

<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->

Adds a new property to InputDecoration `hintTextWidget`

*Fixes*: #161130

With the introduction of ~hintTextWidget~ hint We should be able to
animate hintText



https://github.com/user-attachments/assets/7955a835-5f60-4451-8ede-b5e5f0457046


https://github.com/user-attachments/assets/55d7f021-cc8f-471e-a1d8-e601262ff640


<details>
<summary>sample code</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  String _searchText = 'Popular Picks';
  List<String> wordsToShow = [
    'Popular Picks',
    'Trending',
    'New Arrivals',
    'Best Sellers',
    'Top Rated',
  ];

  int index = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextField(
                cursorHeight: 16,
                decoration: InputDecoration(
                  maintainHintHeight: true,
                  hintTextWidget: Row(
                    children: [
                      const Text(
                        "Search for ",
                        style: TextStyle(fontSize: 16),
                      ),
                      SlidingText(
                          onCompleted: () {
                            index = (index + 1) % wordsToShow.length;
                            setState(() {
                              _searchText = wordsToShow[index];
                            });
                          },
                          word: _searchText,
                          interval: 1500,
                          isDelay: true),
                    ],
                  ),
                  border: OutlineInputBorder(),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class SlidingText extends StatefulWidget {
  final String word;
  final int interval;
  final bool isDelay;
  final Function onCompleted;

  const SlidingText({
    required this.word,
    required this.interval,
    required this.isDelay,
    required this.onCompleted,
    Key? key,
  }) : super(key: key);

  @override
  _SlidingTextState createState() => _SlidingTextState();
}

class _SlidingTextState extends State<SlidingText>
    with SingleTickerProviderStateMixin {
  late AnimationController _animController;
  late Animation<Offset> _slideAnimation;
  late Animation<double> _fadeAnimation;

  @override
  void initState() {
    super.initState();

    _animController = AnimationController(
      duration: Duration(milliseconds: widget.interval),
      vsync: this,
    );

    _slideAnimation = Tween<Offset>(
      begin: Offset(0, 0.5),
      end: Offset(0, -0.6),
    ).animate(
      CurvedAnimation(
        parent: _animController,
        curve: Curves.easeInOut,
      ),
    );
    _fadeAnimation = TweenSequence([
      TweenSequenceItem(
        tween: Tween<double>(begin: 0.0, end: 1.0).chain(
          CurveTween(curve: Curves.easeIn),
        ),
        weight: 50, // First half of the animation
      ),
      TweenSequenceItem(
        tween: Tween<double>(begin: 1.0, end: 0.0).chain(
          CurveTween(curve: Curves.easeOut),
        ),
        weight: 50, // Second half of the animation
      ),
    ]).animate(_animController);
    // add interval

    _animController.addStatusListener((status) async {
      if (status == AnimationStatus.completed) {
        await Future.delayed(Duration(milliseconds: 500));
        widget.onCompleted();
      }
    });
    _animController.forward();
  }

  @override
  void didUpdateWidget(covariant SlidingText oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.word != widget.word) {
      _animController.reset();
      _animController.forward();
    }
  }

  @override
  void dispose() {
    _animController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animController,
      builder: (context, child) {
        return SlideTransition(
          position: _slideAnimation,
          child: FadeTransition(
            opacity: _fadeAnimation,
            child: Text(
              widget.word,
              style: const TextStyle(fontSize: 16),
            ),
          ),
        );
      },
    );
  }
}

```
</details> 

## 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.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Mahesh Jamdade 2025-01-23 18:11:53 -05:00 committed by GitHub
parent bfe31d607c
commit 787afad5b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 52 additions and 12 deletions

View File

@ -2249,31 +2249,35 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
final String? hintText = decoration.hintText;
final bool maintainHintHeight = decoration.maintainHintHeight;
Widget? hint;
if (hintText != null) {
if (decoration.hint != null || hintText != null) {
final Widget hintWidget =
decoration.hint ??
Text(
hintText!,
style: hintStyle,
textDirection: decoration.hintTextDirection,
overflow:
hintStyle.overflow ??
(decoration.hintMaxLines == null ? null : TextOverflow.ellipsis),
textAlign: textAlign,
maxLines: decoration.hintMaxLines,
);
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,
child: hintWidget,
)
: AnimatedSwitcher(
duration: decoration.hintFadeDuration ?? _kHintFadeTransitionDuration,
transitionBuilder: _buildTransition,
child: showHint ? hintTextWidget : const SizedBox.shrink(),
child: showHint ? hintWidget : const SizedBox.shrink(),
);
}
InputBorder? border;
if (!decoration.enabled) {
border = _hasError ? decoration.errorBorder : decoration.disabledBorder;
@ -2695,6 +2699,7 @@ class InputDecoration {
this.helperStyle,
this.helperMaxLines,
this.hintText,
this.hint,
this.hintStyle,
this.hintTextDirection,
this.hintMaxLines,
@ -2742,6 +2747,10 @@ class InputDecoration {
!(label != null && labelText != null),
'Declaring both label and labelText is not supported.',
),
assert(
hint == null || hintText == null,
'Declaring both hint and hintText is not supported.',
),
assert(
!(helper != null && helperText != null),
'Declaring both helper and helperText is not supported.',
@ -2781,6 +2790,7 @@ class InputDecoration {
)
FloatingLabelAlignment? floatingLabelAlignment,
this.hintStyle,
this.hint,
this.hintTextDirection,
this.hintMaxLines,
this.hintFadeDuration,
@ -3019,6 +3029,11 @@ class InputDecoration {
/// or (b) the input has the focus.
final String? hintText;
/// The widget to use in place of the [hintText].
///
/// Either [hintText] or [hint] can be specified, but not both.
final Widget? hint;
/// The style to use for the [hintText].
///
/// If [hintStyle] is a [WidgetStateTextStyle], then the effective
@ -3743,6 +3758,7 @@ class InputDecoration {
TextStyle? helperStyle,
int? helperMaxLines,
String? hintText,
Widget? hint,
TextStyle? hintStyle,
TextDirection? hintTextDirection,
Duration? hintFadeDuration,
@ -3799,6 +3815,7 @@ class InputDecoration {
helperStyle: helperStyle ?? this.helperStyle,
helperMaxLines: helperMaxLines ?? this.helperMaxLines,
hintText: hintText ?? this.hintText,
hint: hint ?? this.hint,
hintStyle: hintStyle ?? this.hintStyle,
hintTextDirection: hintTextDirection ?? this.hintTextDirection,
hintMaxLines: hintMaxLines ?? this.hintMaxLines,
@ -3908,6 +3925,7 @@ class InputDecoration {
other.helperStyle == helperStyle &&
other.helperMaxLines == helperMaxLines &&
other.hintText == hintText &&
other.hint == hint &&
other.hintStyle == hintStyle &&
other.hintTextDirection == hintTextDirection &&
other.hintMaxLines == hintMaxLines &&
@ -3967,6 +3985,7 @@ class InputDecoration {
helperStyle,
helperMaxLines,
hintText,
hint,
hintStyle,
hintTextDirection,
hintMaxLines,
@ -4026,6 +4045,7 @@ class InputDecoration {
if (helperText != null) 'helperText: "$helperText"',
if (helperMaxLines != null) 'helperMaxLines: "$helperMaxLines"',
if (hintText != null) 'hintText: "$hintText"',
if (hint != null) 'hint: $hint',
if (hintMaxLines != null) 'hintMaxLines: "$hintMaxLines"',
if (hintFadeDuration != null) 'hintFadeDuration: "$hintFadeDuration"',
if (!maintainHintHeight) 'maintainHintHeight: false',

View File

@ -4684,6 +4684,26 @@ void main() {
});
});
testWidgets('InputDecorator throws Assertion Error when hint and hintText are provided', (
WidgetTester tester,
) async {
expect(() {
buildInputDecorator(
decoration: InputDecoration(
hintText: 'Enter text here',
hint: const Text('Enter text here', style: TextStyle(fontSize: 20.0)),
),
);
}, throwsAssertionError);
});
testWidgets('InputDecorator shows hint widget', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(decoration: const InputDecoration(hint: Text('hint'))),
);
expect(find.text('hint'), findsOneWidget);
});
testWidgets('hint style overflow works', (WidgetTester tester) async {
final String hintText = 'hint text' * 20;
const TextStyle hintStyle = TextStyle(fontSize: 14.0, overflow: TextOverflow.fade);