code_input.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  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. 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>[WhitelistingTextInputFormatter.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(),
  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 String text,
  173. @required this.codeLength,
  174. this.decoration,
  175. this.space = 4.0,
  176. this.alpha,
  177. }) {
  178. text ??= "";
  179. this.text = text.trim();
  180. }
  181. @override
  182. bool shouldRepaint(CustomPainter oldDelegate) =>
  183. !(oldDelegate is _CodePaint && oldDelegate.text == this.text);
  184. _drawUnderLine(Canvas canvas, Size size) {
  185. /// Force convert to [UnderlineDecoration].
  186. var dr = decoration as UnderlineDecoration;
  187. Paint underlinePaint = Paint()
  188. ..color = dr.color
  189. ..strokeWidth = dr.lineHeight
  190. ..style = PaintingStyle.stroke
  191. ..isAntiAlias = true;
  192. var startX = 0.0;
  193. var startY = size.height;
  194. /// 画下划线
  195. double singleWidth =
  196. (size.width - (codeLength - 1) * dr.gapSpace) / codeLength;
  197. for (int i = 0; i < codeLength; i++) {
  198. if (i == text.length && dr.enteredColor != null) {
  199. underlinePaint.color = dr.enteredColor;
  200. underlinePaint.strokeWidth = 1;
  201. } else {
  202. underlinePaint.color = dr.color;
  203. underlinePaint.strokeWidth = 0.5;
  204. }
  205. canvas.drawLine(Offset(startX, startY),
  206. Offset(startX + singleWidth, startY), underlinePaint);
  207. startX += singleWidth + dr.gapSpace;
  208. }
  209. /// 画文本
  210. var index = 0;
  211. startX = 0.0;
  212. startY = 28.0;
  213. /// Determine whether display obscureText.
  214. bool obscureOn;
  215. obscureOn = decoration.obscureStyle != null &&
  216. decoration.obscureStyle.isTextObscure;
  217. /// The text style of pin.
  218. TextStyle textStyle;
  219. if (decoration.textStyle == null) {
  220. textStyle = defaultStyle;
  221. } else {
  222. textStyle = decoration.textStyle;
  223. }
  224. text.runes.forEach((rune) {
  225. String code;
  226. if (obscureOn) {
  227. code = decoration.obscureStyle.obscureText;
  228. } else {
  229. code = String.fromCharCode(rune);
  230. }
  231. TextPainter textPainter = TextPainter(
  232. text: TextSpan(
  233. style: textStyle,
  234. text: code,
  235. ),
  236. textAlign: TextAlign.center,
  237. textDirection: TextDirection.ltr,
  238. );
  239. /// Layout the text.
  240. textPainter.layout();
  241. startX = singleWidth * index +
  242. singleWidth / 2 -
  243. textPainter.width / 2 +
  244. dr.gapSpace * index;
  245. textPainter.paint(canvas, Offset(startX, startY));
  246. index++;
  247. });
  248. ///画光标 如果外部有传,则直接使用外部
  249. Color cursorColor =
  250. dr.enteredColor != null ? dr.enteredColor : Color.fromRGBO(255, 145, 0, 1);
  251. cursorColor = cursorColor.withAlpha(alpha);
  252. double cursorWidth = 1;
  253. double cursorHeight = 24;
  254. //LogUtil.v("animation.value=$alpha");
  255. Paint cursorPaint = Paint()
  256. ..color = cursorColor
  257. ..strokeWidth = cursorWidth
  258. ..style = PaintingStyle.stroke
  259. ..isAntiAlias = true;
  260. startX = text.length * (singleWidth + dr.gapSpace) + singleWidth / 2;
  261. var endX = startX + cursorWidth;
  262. var endY = startY + cursorHeight;
  263. // var endY = size.height - 28.0 - 12;
  264. // canvas.drawLine(Offset(startX, startY), Offset(startX, endY), cursorPaint);
  265. //绘制圆角光标
  266. Rect rect = Rect.fromLTRB(startX, startY, endX, endY);
  267. RRect rrect = RRect.fromRectAndRadius(rect, Radius.circular(cursorWidth));
  268. canvas.drawRRect(rrect, cursorPaint);
  269. }
  270. @override
  271. void paint(Canvas canvas, Size size) {
  272. _drawUnderLine(canvas, size);
  273. }
  274. }
  275. /// 默认的样式
  276. const TextStyle defaultStyle = TextStyle(
  277. /// Default text color.
  278. color: Colors.black,
  279. /// Default text size.
  280. fontSize: 24.0,
  281. );
  282. abstract class CodeDecoration {
  283. /// The style of painting text.
  284. final TextStyle textStyle;
  285. final ObscureStyle obscureStyle;
  286. const CodeDecoration({
  287. this.textStyle,
  288. this.obscureStyle,
  289. });
  290. }
  291. /// The object determine the obscure display
  292. class ObscureStyle {
  293. /// Determine whether replace [obscureText] with number.
  294. final bool isTextObscure;
  295. /// The display text when [isTextObscure] is true
  296. final String obscureText;
  297. const ObscureStyle({
  298. this.isTextObscure = false,
  299. this.obscureText = '*',
  300. }) : assert(obscureText.length == 1);
  301. }
  302. /// The object determine the underline color etc.
  303. class UnderlineDecoration extends CodeDecoration {
  304. /// The space between text and underline.
  305. final double gapSpace;
  306. /// The color of the underline.
  307. final Color color;
  308. /// The height of the underline.
  309. final double lineHeight;
  310. /// The underline changed color when user enter pin.
  311. final Color enteredColor;
  312. static const _enterColor = Color.fromRGBO(255, 145, 0, 1);
  313. const UnderlineDecoration({
  314. TextStyle textStyle,
  315. ObscureStyle obscureStyle,
  316. this.enteredColor = _enterColor,
  317. this.gapSpace = 15.0,
  318. this.color = Colors.black,
  319. this.lineHeight = 0.5,
  320. }) : super(
  321. textStyle: textStyle,
  322. obscureStyle: obscureStyle,
  323. );
  324. }