Reputation: 1826
How can i make my TextField
label float to the top but stay inside the TextField
border (not at the edge of it) when my TextField
widget is active?
Example of what i'm going for:
Becomes
Is this possible using the inputDecorationTheme
on ThemeData
? or do i have to make a wrapper Widget to accomplish this?
Would very much prefer to control it from an app wide ThemeData
Upvotes: 5
Views: 9841
Reputation: 41
For those who might encounter a similar issue, here is my workaround. I created my implementation of InputBorder
(similar to OutlineInputBorder
or UnderlineInputBorder
). The key difference is that it positions the label inside the border, eliminating any gap on the border for the label.
TextField(
decoration: InputDecoration(
labelText: 'Floating label',
helperText: 'Helper text works fine',
contentPadding: EdgeInsets.all(12.0),
border: OutlinedInputBorder(),
),
);
An example image of a TextField
with the custom InputBorder
.
I hope this solution can be convenient as it doesn't introduce any additional wrappers and can be used quite universally. For example, you can use this InputBorder
implementation directly in your theme.
Here's the code of the custom border.
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
class OutlinedInputBorder extends InputBorder {
const OutlinedInputBorder({
super.borderSide = const BorderSide(),
this.borderRadius = const BorderRadius.all(Radius.circular(4.0)),
});
final BorderRadius borderRadius;
@override
bool get isOutline => false;
@override
OutlinedInputBorder copyWith({
BorderSide? borderSide,
BorderRadius? borderRadius,
}) {
return OutlinedInputBorder(
borderSide: borderSide ?? this.borderSide,
borderRadius: borderRadius ?? this.borderRadius,
);
}
@override
EdgeInsetsGeometry get dimensions {
return EdgeInsets.all(borderSide.width);
}
@override
OutlinedInputBorder scale(double t) {
return OutlinedInputBorder(
borderSide: borderSide.scale(t),
borderRadius: borderRadius * t,
);
}
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
if (a is OutlinedInputBorder) {
final OutlinedInputBorder outline = a;
return OutlinedInputBorder(
borderRadius: BorderRadius.lerp(outline.borderRadius, borderRadius, t)!,
borderSide: BorderSide.lerp(outline.borderSide, borderSide, t),
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
if (b is OutlinedInputBorder) {
final OutlinedInputBorder outline = b;
return OutlinedInputBorder(
borderRadius: BorderRadius.lerp(borderRadius, outline.borderRadius, t)!,
borderSide: BorderSide.lerp(borderSide, outline.borderSide, t),
);
}
return super.lerpTo(b, t);
}
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
return Path()
..addRRect(borderRadius
.resolve(textDirection)
.toRRect(rect)
.deflate(borderSide.width));
}
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
return Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
}
@override
void paintInterior(Canvas canvas, Rect rect, Paint paint,
{TextDirection? textDirection}) {
canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), paint);
}
@override
bool get preferPaintInterior => true;
@override
void paint(
Canvas canvas,
Rect rect, {
double? gapStart,
double gapExtent = 0.0,
double gapPercentage = 0.0,
TextDirection? textDirection,
}) {
final Paint paint = borderSide.toPaint();
final RRect outer = borderRadius.toRRect(rect);
final RRect center = outer.deflate(borderSide.width / 2.0);
canvas.drawRRect(center, paint);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is OutlinedInputBorder &&
other.borderSide == borderSide &&
other.borderRadius == borderRadius;
}
@override
int get hashCode => Object.hash(borderSide, borderRadius);
}
Here's also a gist.
There are relatively few differences from the OutlineInputBorder
implementation to be fair, and most of the code is simply copied. The main change is that the isOutline
getter returns false
(similar to UnderlineInputBorder
), as this value determines where the label will be positioned. Keep in mind that this value is taken into account for some other minor parameters, so you would possibly want to check how InputDecorator
handles it to ensure that my solution suits your needs. For example, you may want to specify custom contentPadding
value. I also modified the logic for drawing the border on the canvas to avoid adding any gap for the label, but there shouldn't be any unexpected issues here, as far as I understand.
Upvotes: 2
Reputation: 5585
class MyInputTextField extends StatefulWidget {
final String? title;
final String? helperText;
final bool isSecure;
final int maxLength;
final String? hint;
final TextInputType? inputType;
final String? initValue;
final Color? backColor;
final Widget? suffix;
final Widget? prefix;
final TextEditingController? textEditingController;
final String? Function(String? value)? validator;
final Function(String)? onTextChanged;
final Function(String)? onSaved;
List<TextInputFormatter>? inputFormatters;
static const int MAX_LENGTH = 500;
MyInputTextField({
this.title,
this.hint,
this.helperText,
this.inputType,
this.initValue = "",
this.isSecure = false,
this.textEditingController,
this.validator,
this.maxLength = MAX_LENGTH,
this.onTextChanged,
this.onSaved,
this.inputFormatters,
this.backColor,
this.suffix,
this.prefix,
});
@override
_MyInputTextFieldState createState() => _MyInputTextFieldState();
}
class _MyInputTextFieldState extends State<MyInputTextField> {
late bool _passwordVisibility;
late ThemeData theme;
FocusNode _focusNode = FocusNode();
Color _borderColor = getColors().primaryVariant;
double _borderSize = 1;
@override
void initState() {
super.initState();
_passwordVisibility = !widget.isSecure;
widget.textEditingController?.text = widget.initValue ?? "";
_focusNode.addListener(() {
setState(() {
_borderSize = _focusNode.hasFocus ? 1.7 : 1;
});
});
}
@override
void didChangeDependencies() {
theme = Theme.of(context);
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Container(
height: 55,
decoration: BoxDecoration(
border: Border.all(color: _borderColor, width: _borderSize),
borderRadius: BorderRadius.circular(AppDimens.radiusSmall),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: TextFormField(
focusNode: _focusNode,
controller: widget.textEditingController,
autocorrect: false,
obscureText: !_passwordVisibility,
keyboardType: widget.inputType,
cursorColor: Colors.white,
validator: (value) {
String? f = widget.validator?.call(value);
setState(() {
_borderColor = f != null ? getColors().redError : getColors().primaryVariant;
});
return f;
},
style: theme.textTheme.bodyText1,
maxLength: widget.maxLength,
inputFormatters: widget.inputFormatters,
maxLines: 1,
onChanged: (text) {
widget.onTextChanged?.call(text);
},
decoration: InputDecoration(
counterText: "",
hintStyle: theme.textTheme.subtitle1,
floatingLabelStyle: theme.textTheme.headline6?.copyWith(color: getColors().textSubtitle),
labelText: widget.title,
helperText: widget.helperText,
suffixIcon: getSuffixIcon(),
prefixIcon: widget.prefix,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
),
),
)
],
);
}
Widget? getSuffixIcon() {
return widget.isSecure ? getPasswordSuffixIcon() : widget.suffix;
}
Widget? getPasswordSuffixIcon() {
return IconButton(
hoverColor: Colors.transparent,
focusColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.zero,
icon: _passwordVisibility ? Icon(AppIcons.password_eye) : Icon(AppIcons.password_eye_blind),
color: Colors.white,
onPressed: () {
setState(() {
_passwordVisibility = !_passwordVisibility;
});
},
);
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
}
Upvotes: 4
Reputation: 1986
Unfortunately i don't know how to do it with default tools, but i do it with another way, may be it will be helpful for you.
FocusNode
and border color inside of your widget:// Use it to change color for border when textFiled in focus
FocusNode _focusNode = FocusNode();
// Color for border
Color _borderColor = Colors.grey;
initState
create listener for textField
, if textField
will be in focus, change border color to orange, otherwise change to grey:@override
void initState() {
super.initState();
// Change color for border if focus was changed
_focusNode.addListener(() {
setState(() {
_borderColor = _focusNode.hasFocus ? Colors.orange : Colors.grey;
});
});
}
Container
with border for textField
, add focusNode
and set decoration to textField
:Container(
decoration: BoxDecoration(
border: Border.all(color: _borderColor),
borderRadius: BorderRadius.circular(4),
),
child: TextField(
focusNode: _focusNode,
style: TextStyle(color: Colors.grey),
keyboardType: TextInputType.number,
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
labelText: "Amount",
prefixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
prefixIcon: Padding(
padding: EdgeInsets.symmetric(vertical: 18, horizontal: 8),
child: Text("₦", style: TextStyle(fontSize: 16, color: Colors.grey)),
),
),
),
),
dispose
for focusNode
:@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
Full code:
class TextFieldDesignPage extends StatefulWidget {
TextFieldDesignPage({Key? key}) : super(key: key);
@override
_TextFieldDesignPageState createState() => _TextFieldDesignPageState();
}
class _TextFieldDesignPageState extends State<TextFieldDesignPage> {
// Use it to change color for border when textFiled in focus
FocusNode _focusNode = FocusNode();
// Color for border
Color _borderColor = Colors.grey;
@override
void initState() {
super.initState();
// Change color for border if focus was changed
_focusNode.addListener(() {
setState(() {
_borderColor = _focusNode.hasFocus ? Colors.orange : Colors.grey;
});
});
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
margin: EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: _borderColor),
borderRadius: BorderRadius.circular(4),
),
child: TextField(
focusNode: _focusNode,
style: TextStyle(color: Colors.grey),
keyboardType: TextInputType.number,
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
labelText: "Amount",
prefixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
prefixIcon: Padding(
padding: EdgeInsets.symmetric(vertical: 18, horizontal: 8),
child: Text("₦", style: TextStyle(fontSize: 16, color: Colors.grey)),
),
),
),
),
),
);
}
}
Result:
Upvotes: 9