Initial commit
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
|
||||
class OtpCodeInput extends StatefulWidget {
|
||||
|
||||
final int length;
|
||||
|
||||
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
|
||||
final ValueChanged<String>? onCompleted;
|
||||
|
||||
|
||||
final bool enabled;
|
||||
|
||||
|
||||
final bool autofocus;
|
||||
|
||||
|
||||
final Color foreground;
|
||||
|
||||
|
||||
final Color accent;
|
||||
|
||||
const OtpCodeInput({
|
||||
super.key,
|
||||
this.length = 6,
|
||||
required this.onChanged,
|
||||
this.onCompleted,
|
||||
this.enabled = true,
|
||||
this.autofocus = true,
|
||||
this.foreground = Colors.white,
|
||||
this.accent = const Color(0xFF1677FF),
|
||||
});
|
||||
|
||||
@override
|
||||
State<OtpCodeInput> createState() => _OtpCodeInputState();
|
||||
}
|
||||
|
||||
class _OtpCodeInputState extends State<OtpCodeInput> {
|
||||
late final List<TextEditingController> _controllers;
|
||||
late final List<FocusNode> _nodes;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controllers = List.generate(widget.length, (_) => TextEditingController());
|
||||
_nodes = List.generate(widget.length, (_) {
|
||||
final node = FocusNode();
|
||||
node.addListener(() {
|
||||
if (node.hasFocus) {
|
||||
final i = _nodes.indexOf(node);
|
||||
final c = _controllers[i];
|
||||
c.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: c.text.length,
|
||||
);
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in _controllers) {
|
||||
c.dispose();
|
||||
}
|
||||
for (final n in _nodes) {
|
||||
n.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String get _value => _controllers.map((c) => c.text).join();
|
||||
|
||||
void _emit() {
|
||||
final v = _value;
|
||||
widget.onChanged(v);
|
||||
if (v.length == widget.length) widget.onCompleted?.call(v);
|
||||
}
|
||||
|
||||
|
||||
void _spread(int startIndex, String digits) {
|
||||
var idx = startIndex;
|
||||
for (final ch in digits.split('')) {
|
||||
if (idx >= widget.length) break;
|
||||
_controllers[idx].text = ch;
|
||||
idx++;
|
||||
}
|
||||
final focusIndex = idx >= widget.length ? widget.length - 1 : idx;
|
||||
_nodes[focusIndex].requestFocus();
|
||||
_emit();
|
||||
}
|
||||
|
||||
void _onChanged(int index, String raw) {
|
||||
final digits = raw.replaceAll(RegExp(r'\D'), '');
|
||||
if (digits.isEmpty) {
|
||||
_controllers[index].text = '';
|
||||
_emit();
|
||||
return;
|
||||
}
|
||||
if (digits.length == 1) {
|
||||
final c = _controllers[index];
|
||||
c.text = digits;
|
||||
c.selection = TextSelection.collapsed(offset: c.text.length);
|
||||
if (index < widget.length - 1) _nodes[index + 1].requestFocus();
|
||||
_emit();
|
||||
} else {
|
||||
_spread(index, digits);
|
||||
}
|
||||
}
|
||||
|
||||
KeyEventResult _onKey(int index, KeyEvent event) {
|
||||
if (event is KeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.backspace &&
|
||||
_controllers[index].text.isEmpty &&
|
||||
index > 0) {
|
||||
_controllers[index - 1].text = '';
|
||||
_nodes[index - 1].requestFocus();
|
||||
_emit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(widget.length, (i) {
|
||||
final focused = _nodes[i].hasFocus;
|
||||
final filled = _controllers[i].text.isNotEmpty;
|
||||
final borderColor = focused
|
||||
? widget.accent
|
||||
: widget.foreground.withValues(alpha: filled ? 0.45 : 0.18);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||
child: SizedBox(
|
||||
width: 46,
|
||||
height: 56,
|
||||
child: Focus(
|
||||
canRequestFocus: false,
|
||||
skipTraversal: true,
|
||||
onKeyEvent: (_, event) => _onKey(i, event),
|
||||
child: TextField(
|
||||
controller: _controllers[i],
|
||||
focusNode: _nodes[i],
|
||||
enabled: widget.enabled,
|
||||
autofocus: widget.autofocus && i == 0,
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
cursorColor: widget.accent,
|
||||
showCursor: true,
|
||||
style: TextStyle(
|
||||
color: widget.foreground,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
filled: true,
|
||||
fillColor: widget.foreground.withValues(alpha: 0.06),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: borderColor, width: 1.2),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: widget.accent, width: 1.6),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: borderColor, width: 1.2),
|
||||
),
|
||||
),
|
||||
onChanged: (v) => _onChanged(i, v),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user