在 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); } } 複製代碼