This commit is contained in:
toly
2023-11-08 09:35:29 +08:00
parent 88cd6fb3b4
commit 8fb4bf57d6
78 changed files with 4344 additions and 544 deletions

View File

@@ -0,0 +1,175 @@
import 'dart:ui';
import 'package:flutter/material.dart';
abstract class CodeDecoration {
final Color activeColor;
final Color inactiveColor;
final Size cursorSize;
final Color cursorColor;
final TextStyle textStyle;
final String? obscureText;
CodeDecoration({
required this.activeColor,
required this.inactiveColor,
required this.textStyle,
required this.cursorSize,
required this.cursorColor,
this.obscureText,
});
void paint(
Canvas canvas, Size size, int alpha, String text, int count, double gap) {
double boxWidth = (size.width - (count - 1) * gap) / count;
/// 绘制装饰
paintDecoration(canvas, size, text, count, gap);
/// 绘制游标
paintCursor(canvas, alpha,size, text.length, boxWidth, gap);
/// 绘制文字
paintText(canvas,size, text, boxWidth, gap);
}
void paintCursor(
Canvas canvas, int alpha,Size size, int count, double boxWidth, double gap) {
Paint cursorPaint = Paint()
..color = cursorColor.withAlpha(alpha)
..strokeWidth = cursorSize.width
..style = PaintingStyle.fill
..isAntiAlias = true;
double startX = count * (boxWidth + gap) + boxWidth / 2;
double startY = size.height/2-cursorSize.height/2;
var endX = startX + cursorSize.width;
var endY = size.height/2 + cursorSize.height/2;
// 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(cursorSize.width));
canvas.drawRRect(rrect, cursorPaint);
}
void paintText(Canvas canvas, Size size,String text, double boxWidth, double gap) {
/// 画文本
double startX = 0.0;
/// Determine whether display obscureText.
bool obscure = obscureText != null;
for (int i = 0; i < text.runes.length; i++) {
int rune = text.runes.elementAt(i);
String code = obscure ? obscureText! : String.fromCharCode(rune);
TextPainter textPainter = TextPainter(
text: TextSpan(
style: textStyle,
text: code,
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
/// Layout the text.
textPainter.layout();
var startY = size.height/2-textPainter.height/2;
startX = boxWidth * i + boxWidth / 2 - textPainter.width / 2 + gap * i;
textPainter.paint(canvas, Offset(startX, startY));
}
}
void paintDecoration(
Canvas canvas, Size size, String text, int count, double gap);
}
class UnderlineCodeDecoration extends CodeDecoration {
final double strokeWidth;
UnderlineCodeDecoration(
{required this.strokeWidth,
required super.activeColor,
required super.inactiveColor,
required super.cursorColor,
required super.textStyle,
required super.cursorSize,
super.obscureText});
@override
void paintDecoration(
Canvas canvas, Size size, String text, int count, double gap) {
Paint underlinePaint = Paint()
..color = activeColor
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..isAntiAlias = true;
/// 画下划线
double singleWidth = (size.width - (count - 1) * gap) / count;
var startX = 0.0;
var startY = size.height;
for (int i = 0; i < count; i++) {
if (i == text.length) {
underlinePaint.color = activeColor;
// underlinePaint.strokeWidth = strokeWidth;
} else {
underlinePaint.color = inactiveColor;
// underlinePaint.strokeWidth = strokeWidth;
}
canvas.drawLine(Offset(startX, startY),
Offset(startX + singleWidth, startY), underlinePaint);
startX += singleWidth + gap;
}
}
}
class RRectCodeDecoration extends CodeDecoration {
final double strokeWidth;
final double height;
RRectCodeDecoration(
{required this.height,
required this.strokeWidth,
required super.activeColor,
required super.inactiveColor,
required super.cursorColor,
required super.textStyle,
required super.cursorSize,
super.obscureText});
@override
void paintDecoration(
Canvas canvas, Size size, String text, int count, double gap) {
Paint paint = Paint()
..color = activeColor
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..isAntiAlias = true;
/// 画下划线
double singleWidth = (size.width - (count - 1) * gap) / count;
var startX = 0.0;
var startY = size.height;
for (int i = 0; i < count; i++) {
if (i == text.length) {
paint.color = activeColor;
} else {
paint.color = inactiveColor;
}
RRect rRect = RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(startX, 0),
Offset(startX + singleWidth, startY),
),
Radius.circular(6));
canvas.drawRRect(rRect, paint);
// canvas.drawLine(Offset(startX, startY),
// Offset(startX + singleWidth, startY), underlinePaint);
startX += singleWidth + gap;
}
}
}

View File

@@ -0,0 +1,215 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'decoration/code_decoration.dart';
import 'input_painter.dart';
typedef AsyncSubmit = Future<bool> Function(String value);
/// @desc 短信验证码输入框
/// @time 2019-05-14 16:16
/// @author Cheney
class TolyCodeInput extends StatefulWidget {
final int count;
final AsyncSubmit onSubmit;
final CodeDecoration decoration;
final TextInputType keyboardType;
final bool autoFocus;
final FocusNode? focusNode;
final TextInputAction textInputAction;
const TolyCodeInput(
{this.count = 6,
required this.onSubmit,
this.decoration = const UnderlineDecoration(),
this.keyboardType = TextInputType.number,
this.focusNode,
this.autoFocus = false,
this.textInputAction = TextInputAction.done,
super.key});
@override
State createState() {
return TolyCodeInputState();
}
}
class TolyCodeInputState extends State<TolyCodeInput>
with SingleTickerProviderStateMixin {
///输入监听器
final TextEditingController _controller = TextEditingController();
late AnimationController _animaCtrl;
late Animation<int> _animation;
late FocusNode _focusNode;
@override
void initState() {
_focusNode = widget.focusNode ?? FocusNode();
_controller.addListener(_listenValueChange);
initAnimation();
super.initState();
}
void initAnimation() {
_animaCtrl = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = IntTween(begin: 0, end: 255).animate(_animaCtrl)
..addStatusListener(_stateChange);
_animaCtrl.forward();
}
void _listenValueChange() {
submit(_controller.text);
}
void submit(String text) async{
if (text.length >= widget.count) {
bool success = await widget.onSubmit.call(text.substring(0, widget.count));
if(success){
_focusNode.unfocus();
}else{
_controller.text = "";
}
}
}
@override
void dispose() {
/// Only execute when the controller is autoDispose.
_controller.dispose();
_animaCtrl.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: InputPainter(
RRectCodeDecoration(
height: 56,
strokeWidth: 1,
activeColor: const Color(0xff010101),
inactiveColor: const Color(0xffB6B6B6),
cursorColor: Color(0xff3776E9),
textStyle: TextStyle(color: Colors.black),
cursorSize: Size(1, 24),
),
controller: _controller,
count: widget.count,
decoration: widget.decoration,
alpha: _animation,
),
child: RepaintBoundary(
child: TextField(
controller: _controller,
/// Fake the text style.
style: const TextStyle(
color: Colors.transparent,
),
cursorColor: Colors.transparent,
cursorWidth: 0.0,
autocorrect: false,
textAlign: TextAlign.center,
enableInteractiveSelection: false,
maxLength: widget.count,
onSubmitted: submit,
keyboardType: widget.keyboardType,
focusNode: _focusNode,
autofocus: widget.autoFocus,
textInputAction: widget.textInputAction,
obscureText: true,
decoration: const InputDecoration(
counterText: '',
contentPadding: EdgeInsets.symmetric(vertical: 24),
border: OutlineInputBorder(
borderSide: BorderSide.none,
),
),
),
),
);
}
void _stateChange(AnimationStatus status) {
if (status == AnimationStatus.completed) {
_animaCtrl.reverse();
} else if (status == AnimationStatus.dismissed) {
_animaCtrl.forward();
}
}
}
/// 默认的样式
const TextStyle defaultStyle = TextStyle(
/// Default text color.
color: Color(0x80000000),
/// 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 = const Color(0xff3776E9),
this.gapSpace = 15.0,
this.color = const Color(0x24000000),
this.lineHeight = 0.5,
}) : super(
textStyle: textStyle,
obscureStyle: obscureStyle,
);
}

View File

@@ -0,0 +1,141 @@
import 'package:flutter/cupertino.dart';
import 'decoration/code_decoration.dart' as c;
import 'input.dart';
class InputPainter extends CustomPainter {
final TextEditingController controller;
final int count;
final double space;
final CodeDecoration decoration;
final c.CodeDecoration underlineCodeDecoration;
final Animation<int> alpha;
InputPainter(
this.underlineCodeDecoration, {
required this.controller,
required this.count,
required this.decoration,
this.space = 4.0,
required this.alpha,
}) : super(repaint: Listenable.merge([controller, alpha]));
void _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 - (count - 1) * dr.gapSpace) / count;
for (int i = 0; i < count; i++) {
if (i == controller.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;
/// 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!;
}
controller.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 ?? const Color(0xff3776E9);
cursorColor = cursorColor.withAlpha(alpha.value);
double cursorWidth = 1;
double cursorHeight = 24;
//LogUtil.v("animation.value=$alpha");
Paint cursorPaint = Paint()
..color = cursorColor
..strokeWidth = cursorWidth
..style = PaintingStyle.stroke
..isAntiAlias = true;
startX =
controller.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);
underlineCodeDecoration.paint(
canvas,
size,
alpha.value,
controller.text,
count,
space,
);
}
@override
bool shouldRepaint(covariant InputPainter oldDelegate) {
return oldDelegate.controller.text != controller.text;
}
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'input.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: TolyCodeInput(
autoFocus: true,
onSubmit: (value) async {
print(value);
return true;
},
)),
),
);
}
}