在 Flutter 做的一個專案中,要用到一個驗證碼輸入框,在原生應用中很常見,但 Flutter 中資料比較少,就自己簡單寫個。
UI 設計效果如下:
實現效果
實現思路比較簡單,直接看程式碼就會懂了。
支援屬性
屬性名 | 作用 | |
---|---|---|
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);
}
}
複製程式碼