code_input.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import 'package:flutter/foundation.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. //import 'package:flutter_common_utils/lcfarm_size.dart';
  5. //import 'package:kappa_app/utils/lcfarm_color.dart';
  6. /// @desc 短信验证码输入框
  7. /// @time 2019-05-14 16:16
  8. /// @author Cheney
  9. class LcfarmCodeInput extends StatefulWidget {
  10. /// The max length of pin.
  11. final int codeLength;
  12. /// The callback will execute when user click done.
  13. final ValueChanged<String> onSubmit;
  14. /// Decorate the pin.
  15. final CodeDecoration decoration;
  16. /// Just like [TextField]'s inputFormatter.
  17. final List<TextInputFormatter>? inputFormatters;
  18. /// Just like [TextField]'s keyboardType.
  19. final TextInputType keyboardType;
  20. /// Same as [TextField]'s autoFocus.
  21. final bool autoFocus;
  22. /// Same as [TextField]'s focusNode.
  23. final FocusNode? focusNode;
  24. /// Same as [TextField]'s textInputAction.
  25. final TextInputAction textInputAction;
  26. LcfarmCodeInput({
  27. GlobalKey<LcfarmCodeInputState>? key,
  28. this.codeLength = 6,
  29. required this.onSubmit,
  30. this.decoration = const UnderlineDecoration(),
  31. List<TextInputFormatter>? inputFormatter,
  32. this.keyboardType = TextInputType.number,
  33. this.focusNode,
  34. this.autoFocus = false,
  35. this.textInputAction = TextInputAction.done,
  36. }) : inputFormatters = inputFormatter ??
  37. <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
  38. super(key: key);
  39. @override
  40. State createState() {
  41. return LcfarmCodeInputState();
  42. }
  43. }
  44. class LcfarmCodeInputState extends State<LcfarmCodeInput>
  45. with SingleTickerProviderStateMixin {
  46. ///输入监听器
  47. TextEditingController _controller = TextEditingController();
  48. /// The display text to the user.
  49. String? _text;
  50. AnimationController? _animationController;
  51. Animation<double>? _animation;
  52. FocusNode? _focusNode;
  53. @override
  54. void initState() {
  55. _focusNode = FocusNode();
  56. _controller.addListener(() {
  57. setState(() {
  58. _text = _controller.text;
  59. });
  60. submit(_controller.text);
  61. });
  62. _animationController =
  63. AnimationController(duration: Duration(milliseconds: 500), vsync: this);
  64. _animation = Tween(begin: 0.0, end: 255.0).animate(_animationController!)
  65. ..addStatusListener((status) {
  66. if (status == AnimationStatus.completed) {
  67. //动画执行结束时反向执行动画
  68. _animationController!.reverse();
  69. } else if (status == AnimationStatus.dismissed) {
  70. //动画恢复到初始状态时执行动画(正向)
  71. _animationController!.forward();
  72. }
  73. })
  74. ..addListener(() {
  75. setState(() {});
  76. });
  77. ///启动动画
  78. _animationController!.forward();
  79. super.initState();
  80. }
  81. void submit(String text) {
  82. if (text.length >= widget.codeLength) {
  83. widget.onSubmit(text.substring(0, widget.codeLength));
  84. _controller.text = "";
  85. //外部有传focusNode就直接使用外部的,没有则使用内部定义的
  86. widget.focusNode == null
  87. ? _focusNode?.unfocus()
  88. : widget.focusNode?.unfocus();
  89. }
  90. }
  91. @override
  92. void dispose() {
  93. /// Only execute when the controller is autoDispose.
  94. _controller.dispose();
  95. _animationController?.dispose();
  96. _focusNode?.dispose();
  97. super.dispose();
  98. }
  99. @override
  100. Widget build(BuildContext context) {
  101. return CustomPaint(
  102. /// The foreground paint to display pin.
  103. foregroundPainter: _CodePaint(
  104. text: _text ??"",
  105. codeLength: widget.codeLength,
  106. decoration: widget.decoration,
  107. alpha: _animation?.value.toInt() ?? 0,
  108. ),
  109. child: RepaintBoundary(
  110. child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
  111. TextField(
  112. /// Actual textEditingController.
  113. controller: _controller,
  114. /// Fake the text style.
  115. style: TextStyle(
  116. /// Hide the editing text.
  117. color: Colors.transparent,
  118. ),
  119. /// Hide the Cursor.
  120. cursorColor: Colors.transparent,
  121. /// Hide the cursor.
  122. cursorWidth: 0.0,
  123. /// No need to correct the user input.
  124. autocorrect: false,
  125. /// Center the input to make more natrual.
  126. textAlign: TextAlign.center,
  127. /// Disable the actual textField selection.
  128. enableInteractiveSelection: false,
  129. /// The maxLength of the pin input, the default value is 6.
  130. maxLength: widget.codeLength,
  131. /// If use system keyboard and user click done, it will execute callback
  132. /// Note!!! Custom keyboard in Android will not execute, see the related issue [https://github.com/flutter/flutter/issues/19027]
  133. onSubmitted: submit,
  134. /// Default text input type is number.
  135. keyboardType: widget.keyboardType,
  136. /// only accept digits.
  137. inputFormatters: widget.inputFormatters,
  138. /// Defines the keyboard focus for this widget.
  139. focusNode: widget.focusNode == null ? _focusNode : widget.focusNode,
  140. /// {@macro flutter.widgets.editableText.autofocus}
  141. autofocus: widget.autoFocus,
  142. /// The type of action button to use for the keyboard.
  143. ///
  144. /// Defaults to [TextInputAction.done]
  145. textInputAction: widget.textInputAction,
  146. /// {@macro flutter.widgets.editableText.obscureText}
  147. /// Default value of the obscureText is false. Make
  148. obscureText: true,
  149. /// Clear default text decoration.
  150. decoration: InputDecoration(
  151. /// Hide the counterText
  152. counterText: '',
  153. contentPadding: EdgeInsets.symmetric(vertical: 24.0),
  154. /// Hide the outline border.
  155. border: OutlineInputBorder(
  156. borderSide: BorderSide.none,
  157. ),
  158. ),
  159. ),
  160. ]),
  161. ),
  162. );
  163. }
  164. }
  165. class _CodePaint extends CustomPainter {
  166. String text;
  167. final int codeLength;
  168. final double space;
  169. final CodeDecoration? decoration;
  170. final int? alpha;
  171. _CodePaint({
  172. required this.text,
  173. required this.codeLength,
  174. this.decoration,
  175. this.space = 4.0,
  176. this.alpha = 0,
  177. }) {
  178. this.text = text.trim();
  179. }
  180. @override
  181. bool shouldRepaint(CustomPainter oldDelegate) =>
  182. !(oldDelegate is _CodePaint && oldDelegate.text == this.text);
  183. _drawUnderLine(Canvas canvas, Size size) {
  184. /// Force convert to [UnderlineDecoration].
  185. ///
  186. if(decoration == null)
  187. return;
  188. var dr = decoration as UnderlineDecoration;
  189. Paint underlinePaint = Paint()
  190. ..color = dr.color
  191. ..strokeWidth = dr.lineHeight
  192. ..style = PaintingStyle.stroke
  193. ..isAntiAlias = true;
  194. var startX = 0.0;
  195. var startY = size.height;
  196. /// 画下划线
  197. double singleWidth =
  198. (size.width - (codeLength - 1) * dr.gapSpace) / codeLength;
  199. for (int i = 0; i < codeLength; i++) {
  200. if (i == text.length && dr.enteredColor != null) {
  201. underlinePaint.color = dr.enteredColor;
  202. underlinePaint.strokeWidth = 1;
  203. } else {
  204. underlinePaint.color = dr.color;
  205. underlinePaint.strokeWidth = 0.5;
  206. }
  207. canvas.drawLine(Offset(startX, startY),
  208. Offset(startX + singleWidth, startY), underlinePaint);
  209. startX += singleWidth + dr.gapSpace;
  210. }
  211. /// 画文本
  212. var index = 0;
  213. startX = 0.0;
  214. startY = 28.0;
  215. /// Determine whether display obscureText.
  216. bool obscureOn;
  217. obscureOn = decoration!.obscureStyle != null &&
  218. decoration!.obscureStyle?.isTextObscure == true;
  219. /// The text style of pin.
  220. TextStyle textStyle;
  221. if (decoration!.textStyle == null) {
  222. textStyle = defaultStyle;
  223. } else {
  224. textStyle = decoration!.textStyle!;
  225. }
  226. text.runes.forEach((rune) {
  227. String code;
  228. if (obscureOn) {
  229. code = decoration!.obscureStyle?.obscureText??"";
  230. } else {
  231. code = String.fromCharCode(rune);
  232. }
  233. TextPainter textPainter = TextPainter(
  234. text: TextSpan(
  235. style: textStyle,
  236. text: code,
  237. ),
  238. textAlign: TextAlign.center,
  239. textDirection: TextDirection.ltr,
  240. );
  241. /// Layout the text.
  242. textPainter.layout();
  243. startX = singleWidth * index +
  244. singleWidth / 2 -
  245. textPainter.width / 2 +
  246. dr.gapSpace * index;
  247. textPainter.paint(canvas, Offset(startX, startY));
  248. index++;
  249. });
  250. ///画光标 如果外部有传,则直接使用外部
  251. Color cursorColor = dr.enteredColor != null
  252. ? dr.enteredColor
  253. : Color.fromRGBO(255, 145, 0, 1);
  254. cursorColor = cursorColor.withAlpha(alpha ?? 0);
  255. double cursorWidth = 1;
  256. double cursorHeight = 24;
  257. //LogUtil.v("animation.value=$alpha");
  258. Paint cursorPaint = Paint()
  259. ..color = cursorColor
  260. ..strokeWidth = cursorWidth
  261. ..style = PaintingStyle.stroke
  262. ..isAntiAlias = true;
  263. startX = text.length * (singleWidth + dr.gapSpace) + singleWidth / 2;
  264. var endX = startX + cursorWidth;
  265. var endY = startY + cursorHeight;
  266. // var endY = size.height - 28.0 - 12;
  267. // canvas.drawLine(Offset(startX, startY), Offset(startX, endY), cursorPaint);
  268. //绘制圆角光标
  269. Rect rect = Rect.fromLTRB(startX, startY, endX, endY);
  270. RRect rrect = RRect.fromRectAndRadius(rect, Radius.circular(cursorWidth));
  271. canvas.drawRRect(rrect, cursorPaint);
  272. }
  273. @override
  274. void paint(Canvas canvas, Size size) {
  275. _drawUnderLine(canvas, size);
  276. }
  277. }
  278. /// 默认的样式
  279. const TextStyle defaultStyle = TextStyle(
  280. /// Default text color.
  281. color: Colors.black,
  282. /// Default text size.
  283. fontSize: 24.0,
  284. );
  285. abstract class CodeDecoration {
  286. /// The style of painting text.
  287. final TextStyle? textStyle;
  288. final ObscureStyle? obscureStyle;
  289. const CodeDecoration({
  290. this.textStyle,
  291. this.obscureStyle,
  292. });
  293. }
  294. /// The object determine the obscure display
  295. class ObscureStyle {
  296. /// Determine whether replace [obscureText] with number.
  297. final bool isTextObscure;
  298. /// The display text when [isTextObscure] is true
  299. final String obscureText;
  300. const ObscureStyle({
  301. this.isTextObscure = false,
  302. this.obscureText = '*',
  303. }) : assert(obscureText.length == 1);
  304. }
  305. /// The object determine the underline color etc.
  306. class UnderlineDecoration extends CodeDecoration {
  307. /// The space between text and underline.
  308. final double gapSpace;
  309. /// The color of the underline.
  310. final Color color;
  311. /// The height of the underline.
  312. final double lineHeight;
  313. /// The underline changed color when user enter pin.
  314. final Color enteredColor;
  315. static const _enterColor = Color.fromRGBO(255, 145, 0, 1);
  316. const UnderlineDecoration({
  317. TextStyle? textStyle,
  318. ObscureStyle? obscureStyle,
  319. this.enteredColor = _enterColor,
  320. this.gapSpace = 15.0,
  321. this.color = Colors.black,
  322. this.lineHeight = 0.5,
  323. }) : super(
  324. textStyle: textStyle,
  325. obscureStyle: obscureStyle,
  326. );
  327. }