Flutter 验证码输入框

  在 Flutter 作的一个项目中,要用到一个验证码输入框,在原生应用中很常见,但 Flutter 中资料比较少,就本身简单写个。
  UI 设计效果以下: git

验证码输入框
  分析一下,这个须要自定义一个输入框,输入框自动获焦,而且输入一位密码的时候,输入框就填入一位,且光标自动移到下一位框中,这就须要单独绘制了,系统默认的输入框没办法直接实现。

实现效果

实现思路比较简单,直接看代码就会懂了。github

支持属性

属性名 做用
autoFocus 是否获焦
codeLength 验证码长度
decoration 下划线样式
inputFormatter 输入文本校验
keyboardType 键盘类型
focusNode 焦点
textInputAction 用于控制键盘动做

主要源码

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_common_utils/lcfarm_size.dart';
import 'package:kappa_app/utils/lcfarm_color.dart';

/// 默认的样式
const TextStyle defaultStyle = TextStyle(
  /// Default text color.
  color: LcfarmColor.color80000000,

  /// Default text size.
  fontSize: 24.0,
);

abstract class CodeDecoration {
  /// The style of painting text.
  final TextStyle textStyle;

  final ObscureStyle obscureStyle;

  const CodeDecoration({
    this.textStyle,
    this.obscureStyle,
  });
}

/// The object determine the obscure display
class ObscureStyle {
  /// Determine whether replace [obscureText] with number.
  final bool isTextObscure;

  /// The display text when [isTextObscure] is true
  final String obscureText;

  const ObscureStyle({
    this.isTextObscure = false,
    this.obscureText = '*',
  }) : assert(obscureText.length == 1);
}

/// The object determine the underline color etc.
class UnderlineDecoration extends CodeDecoration {
  /// The space between text and underline.
  final double gapSpace;

  /// The color of the underline.
  final Color color;

  /// The height of the underline.
  final double lineHeight;

  /// The underline changed color when user enter pin.
  final Color enteredColor;

  const UnderlineDecoration({
    TextStyle textStyle,
    ObscureStyle obscureStyle,
    this.enteredColor = LcfarmColor.color3776E9,
    this.gapSpace = 15.0,
    this.color = LcfarmColor.color24000000,
    this.lineHeight = 0.5,
  }) : super(
          textStyle: textStyle,
          obscureStyle: obscureStyle,
        );
}

class LcfarmCodeInput extends StatefulWidget {
  /// The max length of pin.
  final int codeLength;

  /// The callback will execute when user click done.
  final ValueChanged<String> onSubmit;

  /// Decorate the pin.
  final CodeDecoration decoration;

  /// Just like [TextField]'s inputFormatter. final List<TextInputFormatter> inputFormatters; /// Just like [TextField]'s keyboardType.
  final TextInputType keyboardType;

  /// Same as [TextField]'s autoFocus. final bool autoFocus; /// Same as [TextField]'s focusNode.
  final FocusNode focusNode;

  /// Same as [TextField]'s textInputAction. final TextInputAction textInputAction; LcfarmCodeInput({ GlobalKey<LcfarmCodeInputState> key, this.codeLength = 6, this.onSubmit, this.decoration = const UnderlineDecoration(), List<TextInputFormatter> inputFormatter, this.keyboardType = TextInputType.number, this.focusNode, this.autoFocus = false, this.textInputAction = TextInputAction.done, }) : inputFormatters = inputFormatter ?? <TextInputFormatter>[WhitelistingTextInputFormatter.digitsOnly], super(key: key); @override State createState() { return LcfarmCodeInputState(); } } class LcfarmCodeInputState extends State<LcfarmCodeInput> with SingleTickerProviderStateMixin { ///输入监听器 TextEditingController _controller = TextEditingController(); /// The display text to the user. String _text; AnimationController _animationController; Animation<double> _animation; FocusNode _focusNode; @override void initState() { _focusNode = FocusNode(); _controller.addListener(() { setState(() { _text = _controller.text; }); submit(_controller.text); }); _animationController = AnimationController(duration: Duration(milliseconds: 500), vsync: this); _animation = Tween(begin: 0.0, end: 255.0).animate(_animationController) ..addStatusListener((status) { if (status == AnimationStatus.completed) { //动画执行结束时反向执行动画 _animationController.reverse(); } else if (status == AnimationStatus.dismissed) { //动画恢复到初始状态时执行动画(正向) _animationController.forward(); } }) ..addListener(() { setState(() {}); }); ///启动动画 _animationController.forward(); super.initState(); } void submit(String text) { if (text.length >= widget.codeLength) { widget.onSubmit(text.substring(0, widget.codeLength)); _controller.text = ""; //外部有传focusNode就直接使用外部的,没有则使用内部定义的 widget.focusNode == null ? _focusNode.unfocus() : widget.focusNode.unfocus(); } } @override void dispose() { /// Only execute when the controller is autoDispose. _controller.dispose(); _animationController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return CustomPaint( /// The foreground paint to display pin. foregroundPainter: _CodePaint( text: _text, codeLength: widget.codeLength, decoration: widget.decoration, alpha: _animation.value.toInt(), ), child: RepaintBoundary( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( /// Actual textEditingController. controller: _controller, /// Fake the text style. style: TextStyle( /// Hide the editing text. color: Colors.transparent, ), /// Hide the Cursor. cursorColor: Colors.transparent, /// Hide the cursor. cursorWidth: 0.0, /// No need to correct the user input. autocorrect: false, /// Center the input to make more natrual. textAlign: TextAlign.center, /// Disable the actual textField selection. enableInteractiveSelection: false, /// The maxLength of the pin input, the default value is 6. maxLength: widget.codeLength, /// If use system keyboard and user click done, it will execute callback /// Note!!! Custom keyboard in Android will not execute, see the related issue [https://github.com/flutter/flutter/issues/19027] onSubmitted: submit, /// Default text input type is number. keyboardType: widget.keyboardType, /// only accept digits. inputFormatters: widget.inputFormatters, /// Defines the keyboard focus for this widget. focusNode: widget.focusNode == null ? _focusNode : widget.focusNode, /// {@macro flutter.widgets.editableText.autofocus} autofocus: widget.autoFocus, /// The type of action button to use for the keyboard. /// /// Defaults to [TextInputAction.done] textInputAction: widget.textInputAction, /// {@macro flutter.widgets.editableText.obscureText} /// Default value of the obscureText is false. Make obscureText: true, /// Clear default text decoration. decoration: InputDecoration( /// Hide the counterText counterText: '', contentPadding: EdgeInsets.symmetric(vertical: LcfarmSize.dp(24)), /// Hide the outline border. border: OutlineInputBorder( borderSide: BorderSide.none, ), ), ), ]), ), ); } } class _CodePaint extends CustomPainter { String text; final int codeLength; final double space; final CodeDecoration decoration; final int alpha; _CodePaint({ @required String text, @required this.codeLength, this.decoration, this.space = 4.0, this.alpha, }) { text ??= ""; this.text = text.trim(); } @override bool shouldRepaint(CustomPainter oldDelegate) => !(oldDelegate is _CodePaint && oldDelegate.text == this.text); _drawUnderLine(Canvas canvas, Size size) { /// Force convert to [UnderlineDecoration]. var dr = decoration as UnderlineDecoration; Paint underlinePaint = Paint() ..color = dr.color ..strokeWidth = dr.lineHeight ..style = PaintingStyle.stroke ..isAntiAlias = true; var startX = 0.0; var startY = size.height; /// 画下划线 double singleWidth = (size.width - (codeLength - 1) * dr.gapSpace) / codeLength; for (int i = 0; i < codeLength; i++) { if (i == text.length && dr.enteredColor != null) { underlinePaint.color = dr.enteredColor; underlinePaint.strokeWidth = LcfarmSize.dp(1); } else { underlinePaint.color = dr.color; underlinePaint.strokeWidth = LcfarmSize.dp(0.5); } canvas.drawLine(Offset(startX, startY), Offset(startX + singleWidth, startY), underlinePaint); startX += singleWidth + dr.gapSpace; } /// 画文本 var index = 0; startX = 0.0; startY = LcfarmSize.dp(28); /// Determine whether display obscureText. bool obscureOn; obscureOn = decoration.obscureStyle != null && decoration.obscureStyle.isTextObscure; /// The text style of pin. TextStyle textStyle; if (decoration.textStyle == null) { textStyle = defaultStyle; } else { textStyle = decoration.textStyle; } text.runes.forEach((rune) { String code; if (obscureOn) { code = decoration.obscureStyle.obscureText; } else { code = String.fromCharCode(rune); } TextPainter textPainter = TextPainter( text: TextSpan( style: textStyle, text: code, ), textAlign: TextAlign.center, textDirection: TextDirection.ltr, ); /// Layout the text. textPainter.layout(); startX = singleWidth * index + singleWidth / 2 - textPainter.width / 2 + dr.gapSpace * index; textPainter.paint(canvas, Offset(startX, startY)); index++; }); ///画光标 若是外部有传,则直接使用外部 Color cursorColor = dr.enteredColor != null ? dr.enteredColor : LcfarmColor.color3776E9; cursorColor = cursorColor.withAlpha(alpha); double cursorWidth = LcfarmSize.dp(1); double cursorHeight = LcfarmSize.dp(24); //LogUtil.v("animation.value=$alpha"); Paint cursorPaint = Paint() ..color = cursorColor ..strokeWidth = cursorWidth ..style = PaintingStyle.stroke ..isAntiAlias = true; startX = text.length * (singleWidth + dr.gapSpace) + singleWidth / 2; var endX = startX + cursorWidth; var endY = startY + cursorHeight; // var endY = size.height - 28.0 - 12; // canvas.drawLine(Offset(startX, startY), Offset(startX, endY), cursorPaint); //绘制圆角光标 Rect rect = Rect.fromLTRB(startX, startY, endX, endY); RRect rrect = RRect.fromRectAndRadius(rect, Radius.circular(cursorWidth)); canvas.drawRRect(rrect, cursorPaint); } @override void paint(Canvas canvas, Size size) { _drawUnderLine(canvas, size); } } 复制代码
相关文章
相关标签/搜索