Raymond Ativie
Raymond Ativie

Reputation: 1826

Flutter: TextField or TextFormField label inside border using ThemeData

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:

Before active

Becomes

enter image description here This

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

Answers (3)

Ivan Timashov
Ivan Timashov

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

Erfan Eghterafi
Erfan Eghterafi

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();
  }
}

enter image description here

enter image description here

Upvotes: 4

Andrei R
Andrei R

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.

  1. Create variable for 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;
  1. Inside of 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;
    });
  });
}
  1. Create 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)),
      ),
    ),
  ),
),
  1. Don't forget call 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:

enter image description here

Upvotes: 9

Related Questions