Skip to content

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

Merged
auto-submit[bot] merged 29 commits into
flutter:masterfrom
maheshj01:hint-widget-161130
Jan 24, 2025
Merged

auto-submit[bot] merged 29 commits into
flutter:masterfrom
maheshj01:hint-widget-161130

Conversation

@maheshj01

@maheshj01 maheshj01 commented Jan 10, 2025 •

Copy link
Copy Markdown
Member

Adds a new property to InputDecoration hintTextWidget hint

Fixes: #161130

With the introduction of hintTextWidget hint We should be able to pass a custom widget. This will allow creating such animated experiences

Screen.Recording.2025-01-10.at.09.50.49.mov
Screen.Recording.2025-01-10.at.13.41.28.mov
sample code
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,
                  hint: 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),
            ),
          ),
        );
      },
    );
  }
}

Pre-launch Checklist

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

@flutter-dashboard

Copy link
Copy Markdown

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!).

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group.

@github-actions github-actions Bot added framework flutter/packages/flutter repository. See also f: labels. p: material_ui material_ui package in flutter/packages labels Jan 10, 2025
@maheshj01

Copy link
Copy Markdown
Member Author

cc: @justinmc

@bleroux

bleroux commented Jan 10, 2025

Copy link
Copy Markdown
Contributor

Hey @maheshj01! 👋
This property should be named 'hint' similarly to the other InputDecoration properties (label/labelText, helper/helperText, etc).

@maheshj01

maheshj01 commented Jan 10, 2025 •

Copy link
Copy Markdown
Member Author

@bleroux renamed.
Do we need any tests for this PR?

@maheshj01 maheshj01 changed the title feat: Add hintTextWidget property feat: Add hint (Widget) property to InputDecoration Jan 10, 2025

@bleroux bleroux left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, tests are required for this PR.
I see a minimum of two tests:

  • one which uses the new fields and check that the corresponding widget is used.
  • another which check that defining both hint and hintText triggers the assert.

Comment thread packages/flutter/lib/src/material/input_decorator.dart Outdated
Comment thread packages/flutter/lib/src/material/input_decorator.dart Outdated
Comment thread packages/flutter/lib/src/material/input_decorator.dart Outdated
@maheshj01
maheshj01 requested a review from matanlurey as a code owner January 15, 2025 14:05
@github-actions github-actions Bot added a: tests "flutter test", flutter_test, or one of our tests a: text input Entering text in a text field or keyboard related problems c: contributor-productivity Team-specific productivity, code health, technical debt. platform-android Android applications specifically platform-ios iOS applications specifically tool Affects the "flutter" command-line tool. See also t: labels. engine flutter/engine related. See also e: labels. a: animation Animation APIs a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) a: internationalization Supporting other languages or locales. (aka i18n) f: scrolling Viewports, list views, slivers, etc. p: cupertino_ui cupertino_ui package in flutter/packages platform-windows Building on or for Windows specifically d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: routes Navigator, Router, and related APIs. labels Jan 15, 2025

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few minor points here.

if (decoration.hint != null || hintText != null) {
final Widget hintWidget;
hintWidget =
decoration.hint ??

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch @bdlukaa. A separate PR sounds good to me. @bleroux Do we have an issue for those?

Comment on lines +2253 to +2254
final Widget hintWidget;
hintWidget =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be one line I think.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dart format makes it two liner, I suspect the analyzer test would fail making it one line

@bleroux bleroux Jan 23, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Justin suggestion was related to merging the declaration and the initialization on the same line:

Suggested change
final Widget hintWidget;
hintWidget =
final Widget hintWidget =

@maheshj01 maheshj01 Jan 23, 2025 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated it with one line now

/// or (b) the input has the focus.
final String? hintText;

/// The widget to use inplace of the [hintText]. Either [hintText] or

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inplace => in place

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the second sentence should be its own paragraph. From the style guide:

"The first paragraph of any dartdoc section must be a short self-contained sentence that explains the purpose and meaning of the item being documented. "

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more small formatting thing in your docs here that needs to be fixed. Sorry, this should be part of the autoformatter or something.

'Declaring both label and labelText is not supported.',
),
assert(
!(hintText != null && hint != null),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Another option is hintText == null || hint == null, either way is fine though.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Comment on lines +3032 to +3033
/// The widget to use in place of the [hintText].
/// Either [hintText] or [hint] can be specified, but not both.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need an empty line of /// in between these two lines.

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@justinmc justinmc added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 23, 2025
@auto-submit
auto-submit Bot added this pull request to the merge queue Jan 23, 2025
Merged via the queue into flutter:master with commit 787afad Jan 24, 2025
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jan 24, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 24, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 24, 2025
Wasmund1 pushed a commit to Wasmund1/flutter that referenced this pull request Jan 24, 2025
<!--
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*: flutter#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
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 24, 2025
@Imgkl

Imgkl commented May 2, 2025

Copy link
Copy Markdown

Has this reached stable? Not able to find. @maheshj01

@TahaTesser

Copy link
Copy Markdown
Contributor

Has this reached stable? Not able to find. @maheshj01

This isn't currently available on the latest Flutter Stable channel. will probably in the next major release.

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels. p: material_ui material_ui package in flutter/packages team-text-input Owned by Text Input team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants