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'; /// @desc 短信验证码输入框 /// @time 2019-05-14 16:16 /// @author Cheney class LcfarmCodeInput extends StatefulWidget { /// The max length of pin. final int codeLength; /// The callback will execute when user click done. final ValueChanged onSubmit; /// Decorate the pin. final CodeDecoration decoration; /// Just like [TextField]'s inputFormatter. final List 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 key, this.codeLength = 6, this.onSubmit, this.decoration = const UnderlineDecoration(), List inputFormatter, this.keyboardType = TextInputType.number, this.focusNode, this.autoFocus = false, this.textInputAction = TextInputAction.done, }) : inputFormatters = inputFormatter ?? [WhitelistingTextInputFormatter.digitsOnly], super(key: key); @override State createState() { return LcfarmCodeInputState(); } } class LcfarmCodeInputState extends State with SingleTickerProviderStateMixin { ///输入监听器 TextEditingController _controller = TextEditingController(); /// The display text to the user. String _text; AnimationController _animationController; Animation _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: 24.0), /// 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 = 1; } else { underlinePaint.color = dr.color; underlinePaint.strokeWidth = 0.5; } canvas.drawLine(Offset(startX, startY), Offset(startX + singleWidth, startY), underlinePaint); startX += singleWidth + dr.gapSpace; } /// 画文本 var index = 0; startX = 0.0; startY = 28.0; /// 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 : Color.fromRGBO(255, 145, 0, 1); cursorColor = cursorColor.withAlpha(alpha); double cursorWidth = 1; double cursorHeight = 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); } } /// 默认的样式 const TextStyle defaultStyle = TextStyle( /// Default text color. color: Colors.black, /// 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; static const _enterColor = Color.fromRGBO(255, 145, 0, 1); const UnderlineDecoration({ TextStyle textStyle, ObscureStyle obscureStyle, this.enteredColor = _enterColor, this.gapSpace = 15.0, this.color = Colors.black, this.lineHeight = 0.5, }) : super( textStyle: textStyle, obscureStyle: obscureStyle, ); }