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:
parent
bfe31d607c
commit
787afad5b4
@ -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',
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user