You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
443 lines
13 KiB
Dart
443 lines
13 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/animation.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:hmg_patient_app/uitl/utils.dart';
|
|
|
|
typedef OnDone = void Function(String text);
|
|
|
|
class ProvidedPinBoxTextAnimation {
|
|
static AnimatedSwitcherTransitionBuilder scalingTransition = (child, animation) {
|
|
return ScaleTransition(
|
|
child: child,
|
|
scale: animation,
|
|
);
|
|
};
|
|
|
|
static AnimatedSwitcherTransitionBuilder defaultNoTransition = (Widget child, Animation<double> animation) {
|
|
return child;
|
|
};
|
|
}
|
|
|
|
class OTPWidget extends StatefulWidget {
|
|
final int maxLength;
|
|
late TextEditingController? controller;
|
|
|
|
final Color defaultBorderColor;
|
|
final Color pinBoxColor;
|
|
final double pinBoxBorderWidth;
|
|
final double pinBoxRadius;
|
|
final bool hideDefaultKeyboard;
|
|
|
|
final TextStyle? pinTextStyle;
|
|
final double pinBoxHeight;
|
|
final double pinBoxWidth;
|
|
final OnDone? onDone;
|
|
final bool hasError;
|
|
final Color errorBorderColor;
|
|
final Color textBorderColor;
|
|
final Function(String)? onTextChanged;
|
|
final bool autoFocus;
|
|
final FocusNode? focusNode;
|
|
final AnimatedSwitcherTransitionBuilder? pinTextAnimatedSwitcherTransition;
|
|
final Duration pinTextAnimatedSwitcherDuration;
|
|
final TextDirection textDirection;
|
|
final TextInputType keyboardType;
|
|
final EdgeInsets pinBoxOuterPadding;
|
|
|
|
OTPWidget({
|
|
Key? key,
|
|
this.maxLength = 4,
|
|
this.controller,
|
|
this.pinBoxWidth = 70.0,
|
|
this.pinBoxHeight = 70.0,
|
|
this.pinTextStyle,
|
|
this.onDone,
|
|
this.defaultBorderColor = Colors.black,
|
|
this.textBorderColor = Colors.black,
|
|
this.pinTextAnimatedSwitcherTransition,
|
|
this.pinTextAnimatedSwitcherDuration = const Duration(),
|
|
this.hasError = false,
|
|
this.errorBorderColor = Colors.red,
|
|
this.onTextChanged,
|
|
this.autoFocus = false,
|
|
this.focusNode,
|
|
this.textDirection = TextDirection.ltr,
|
|
this.keyboardType = TextInputType.number,
|
|
this.pinBoxOuterPadding = const EdgeInsets.symmetric(horizontal: 4.0),
|
|
this.pinBoxColor = Colors.white,
|
|
this.pinBoxBorderWidth = 2.0,
|
|
this.pinBoxRadius = 0,
|
|
this.hideDefaultKeyboard = false,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<StatefulWidget> createState() {
|
|
return OTPWidgetState();
|
|
}
|
|
}
|
|
|
|
class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixin {
|
|
late AnimationController _highlightAnimationController;
|
|
late FocusNode focusNode;
|
|
String text = "";
|
|
int currentIndex = 0;
|
|
List<String> strList = [];
|
|
bool hasFocus = false;
|
|
|
|
@override
|
|
void didUpdateWidget(OTPWidget oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
focusNode = widget.focusNode ?? focusNode;
|
|
|
|
if (oldWidget.maxLength < widget.maxLength) {
|
|
setState(() {
|
|
currentIndex = text.length;
|
|
});
|
|
widget.controller?.text = text;
|
|
widget.controller?.selection = TextSelection.collapsed(offset: text.length);
|
|
} else if (oldWidget.maxLength > widget.maxLength && widget.maxLength > 0 && text.length > 0 && text.length > widget.maxLength) {
|
|
setState(() {
|
|
text = text.substring(0, widget.maxLength);
|
|
currentIndex = text.length;
|
|
});
|
|
widget.controller?.text = text;
|
|
widget.controller?.selection = TextSelection.collapsed(offset: text.length);
|
|
}
|
|
}
|
|
|
|
_calculateStrList() {
|
|
if (strList.length > widget.maxLength) {
|
|
strList.length = widget.maxLength;
|
|
}
|
|
while (strList.length < widget.maxLength) {
|
|
strList.add("");
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
focusNode = widget.focusNode ?? FocusNode();
|
|
_highlightAnimationController = AnimationController(vsync: this);
|
|
_initTextController();
|
|
_calculateStrList();
|
|
widget.controller!.addListener(_controllerListener);
|
|
focusNode.addListener(_focusListener);
|
|
}
|
|
|
|
void _controllerListener() {
|
|
if (mounted == true) {
|
|
setState(() {
|
|
_initTextController();
|
|
});
|
|
var onTextChanged = widget.onTextChanged;
|
|
if (onTextChanged != null) {
|
|
onTextChanged(widget.controller?.text ?? "");
|
|
}
|
|
}
|
|
}
|
|
|
|
void _focusListener() {
|
|
if (mounted == true) {
|
|
setState(() {
|
|
hasFocus = focusNode?.hasFocus ?? false;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _initTextController() {
|
|
if (widget.controller == null) {
|
|
return;
|
|
}
|
|
strList.clear();
|
|
var text = widget.controller?.text ?? "";
|
|
if (text.isNotEmpty) {
|
|
if (text.length > widget.maxLength) {
|
|
throw Exception("TextEditingController length exceeded maxLength!");
|
|
}
|
|
}
|
|
for (var i = 0; i < text.length; i++) {
|
|
strList.add(text[i]);
|
|
}
|
|
}
|
|
|
|
// Updated: Accept context to access MediaQuery
|
|
double _width(BuildContext context) {
|
|
// For tablets, use full screen width
|
|
if (MediaQuery.of(context).size.shortestSide >= 600) {
|
|
return MediaQuery.of(context).size.width;
|
|
}
|
|
|
|
// For phones, use original fixed width
|
|
var width = 0.0;
|
|
for (var i = 0; i < widget.maxLength; i++) {
|
|
width += widget.pinBoxWidth;
|
|
if (i == 0) {
|
|
width += widget.pinBoxOuterPadding.left;
|
|
} else if (i + 1 == widget.maxLength) {
|
|
width += widget.pinBoxOuterPadding.right;
|
|
} else {
|
|
width += widget.pinBoxOuterPadding.left;
|
|
}
|
|
}
|
|
return width;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if (widget.focusNode == null) {
|
|
focusNode.dispose();
|
|
} else {
|
|
focusNode.removeListener(_focusListener);
|
|
}
|
|
_highlightAnimationController.dispose();
|
|
widget.controller?.removeListener(_controllerListener);
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
fit: StackFit.loose,
|
|
children: <Widget>[
|
|
_touchPinBoxRow(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _touchPinBoxRow() {
|
|
return widget.hideDefaultKeyboard
|
|
? _pinBoxRow(context)
|
|
: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
if (hasFocus) {
|
|
FocusScope.of(context).requestFocus(FocusNode());
|
|
Future.delayed(Duration(milliseconds: 100), () {
|
|
FocusScope.of(context).requestFocus(focusNode);
|
|
});
|
|
} else {
|
|
FocusScope.of(context).requestFocus(focusNode);
|
|
}
|
|
},
|
|
child: _pinBoxRow(context),
|
|
);
|
|
}
|
|
|
|
// Updated: Accept context for width calculation
|
|
Widget _otpTextInput(BuildContext context) {
|
|
var transparentBorder = OutlineInputBorder(
|
|
borderSide: BorderSide(color: Colors.transparent, width: 0),
|
|
);
|
|
return Container(
|
|
width: _width(context), // Use dynamic width
|
|
height: widget.pinBoxHeight,
|
|
child: TextField(
|
|
autofocus: !kIsWeb ? widget.autoFocus : false,
|
|
enableInteractiveSelection: false,
|
|
focusNode: focusNode,
|
|
controller: widget.controller,
|
|
textAlign: TextAlign.center,
|
|
keyboardType: widget.keyboardType,
|
|
inputFormatters: widget.keyboardType == TextInputType.number ? <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly] : null,
|
|
style: TextStyle(
|
|
height: 0.5,
|
|
color: Colors.transparent,
|
|
),
|
|
decoration: InputDecoration(
|
|
contentPadding: EdgeInsets.all(0),
|
|
focusedErrorBorder: transparentBorder,
|
|
errorBorder: transparentBorder,
|
|
disabledBorder: transparentBorder,
|
|
enabledBorder: transparentBorder,
|
|
focusedBorder: transparentBorder,
|
|
counterText: null,
|
|
counterStyle: null,
|
|
helperStyle: TextStyle(height: 0.0, color: Colors.transparent),
|
|
labelStyle: TextStyle(height: 0.1),
|
|
fillColor: Colors.transparent,
|
|
border: InputBorder.none,
|
|
isDense: true),
|
|
cursorColor: Colors.transparent,
|
|
showCursor: false,
|
|
maxLength: widget.maxLength,
|
|
onChanged: _onTextChanged,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onTextChanged(text) {
|
|
var onTextChanged = widget.onTextChanged;
|
|
if (onTextChanged != null) {
|
|
onTextChanged(text);
|
|
}
|
|
setState(() {
|
|
this.text = text;
|
|
if (text.length >= currentIndex) {
|
|
for (int i = currentIndex; i < text.length; i++) {
|
|
strList[i] = text[i];
|
|
}
|
|
}
|
|
currentIndex = text.length;
|
|
});
|
|
if (text.length == widget.maxLength) {
|
|
FocusScope.of(context).requestFocus(FocusNode());
|
|
var onDone = widget.onDone;
|
|
if (onDone != null) {
|
|
onDone(text);
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _pinBoxRow(BuildContext context) {
|
|
_calculateStrList();
|
|
|
|
// Determine if it's a tablet
|
|
bool isTablet = MediaQuery.of(context).size.shortestSide >= 600;
|
|
double boxWidth = widget.pinBoxWidth;
|
|
|
|
// Calculate dynamic width for tablets
|
|
if (isTablet) {
|
|
double screenWidth = MediaQuery.of(context).size.width;
|
|
double totalMargin = (widget.maxLength - 1) *
|
|
(widget.pinBoxOuterPadding.left + widget.pinBoxOuterPadding.right);
|
|
double availableWidth = screenWidth - totalMargin;
|
|
boxWidth = (availableWidth / widget.maxLength).clamp(100.0, 180.0); // Reasonable limits
|
|
}
|
|
|
|
List<Widget> pinCodes = List.generate(widget.maxLength, (int i) {
|
|
return _buildPinCode(i, context, boxWidth); // Pass dynamic width
|
|
});
|
|
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: pinCodes,
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
),
|
|
);
|
|
}
|
|
|
|
// Updated: Accept dynamic width
|
|
Widget _buildPinCode(int i, BuildContext context, double boxWidth) {
|
|
final bool isFilled = i < text.length && strList[i].isNotEmpty; // Check both conditions
|
|
final bool isCurrent = i == currentIndex;
|
|
final bool isFocused = hasFocus && isCurrent;
|
|
|
|
Color bgColor = Colors.white;
|
|
Color borderColor = Colors.white;
|
|
Color textColor = Colors.black;
|
|
|
|
if (widget.hasError) {
|
|
borderColor = widget.errorBorderColor;
|
|
bgColor = Colors.red.shade50;
|
|
} else if (isFocused) {
|
|
borderColor = Colors.transparent;
|
|
bgColor = Colors.white;
|
|
textColor = Colors.black; // Changed from white to black for better visibility
|
|
} else if (isFilled) {
|
|
borderColor = Colors.green;
|
|
bgColor = Colors.green;
|
|
textColor = Colors.white;
|
|
}
|
|
// When cleared/empty, colors remain as default initialized above
|
|
|
|
return Container(
|
|
key: ValueKey<String>("container$i"),
|
|
alignment: Alignment.center,
|
|
margin: widget.pinBoxOuterPadding,
|
|
padding: EdgeInsets.zero,
|
|
decoration: BoxDecoration(
|
|
color: bgColor,
|
|
border: Border.all(color: borderColor, width: 1),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
width: boxWidth, // Use dynamic width
|
|
height: widget.pinBoxHeight,
|
|
child: _animatedTextBox(
|
|
strList[i],
|
|
i,
|
|
textColor,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _animatedTextBox(String text, int i, Color textColor) {
|
|
final bool isFilled = text.isNotEmpty;
|
|
final double fontSize = isFilled ? 50 : 50;
|
|
final FontWeight fontWeight = isFilled ? FontWeight.w600 : FontWeight.normal;
|
|
|
|
if (widget.pinTextAnimatedSwitcherTransition != null) {
|
|
return AnimatedSwitcher(
|
|
duration: widget.pinTextAnimatedSwitcherDuration,
|
|
transitionBuilder: widget.pinTextAnimatedSwitcherTransition!,
|
|
child: Text(
|
|
text,
|
|
softWrap: true,
|
|
key: ValueKey<String>("$text$i"),
|
|
style: widget.pinTextStyle?.copyWith(
|
|
color: textColor,
|
|
fontSize: fontSize,
|
|
fontWeight: fontWeight,
|
|
fontFamily: context.fontFamily,
|
|
) ??
|
|
TextStyle(
|
|
color: textColor,
|
|
fontSize: fontSize,
|
|
fontWeight: fontWeight,
|
|
fontFamily: context.fontFamily,
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
return Text(
|
|
text,
|
|
softWrap: true,
|
|
key: ValueKey<String>("${strList[i]}$i"),
|
|
style: widget.pinTextStyle?.copyWith(
|
|
color: textColor,
|
|
fontSize: fontSize,
|
|
fontWeight: fontWeight,
|
|
fontFamily: context.fontFamily,
|
|
) ??
|
|
TextStyle(
|
|
color: textColor,
|
|
fontSize: fontSize,
|
|
fontWeight: fontWeight,
|
|
fontFamily: context.fontFamily,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Widget _animatedTextBox(String text, int i, Color textColor) {
|
|
// if (widget.pinTextAnimatedSwitcherTransition != null) {
|
|
// return AnimatedSwitcher(
|
|
// duration: widget.pinTextAnimatedSwitcherDuration,
|
|
// transitionBuilder: widget.pinTextAnimatedSwitcherTransition!,
|
|
// child: Text(
|
|
// text,
|
|
// key: ValueKey<String>("$text$i"),
|
|
// style: widget.pinTextStyle?.copyWith(color: textColor) ??
|
|
// TextStyle(color: textColor, fontSize: 24, fontWeight: FontWeight.bold),
|
|
// ),
|
|
// );
|
|
// } else {
|
|
// return Text(
|
|
// text,
|
|
// key: ValueKey<String>("${strList[i]}$i"),
|
|
// style: widget.pinTextStyle?.copyWith(color: textColor) ??
|
|
// TextStyle(color: textColor, fontSize: 24, fontWeight: FontWeight.bold),
|
|
// );
|
|
// }
|
|
// }}
|