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.
HMG_Patient_App/lib/widgets/otp_widget.dart

421 lines
12 KiB
Dart

4 months ago
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';
4 months ago
import 'package:hmg_patient_app/uitl/utils.dart';
4 months ago
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 {
4 months ago
late AnimationController _highlightAnimationController;
4 months ago
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]);
}
}
3 months ago
// Updated: Always use full screen width for responsive design
3 months ago
double _width(BuildContext context) {
3 months ago
return MediaQuery.of(context).size.width;
4 months ago
}
@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(
4 months ago
fit: StackFit.loose,
4 months ago
children: <Widget>[
3 months ago
_otpTextInput(context),
4 months ago
_touchPinBoxRow(),
],
);
}
Widget _touchPinBoxRow() {
return widget.hideDefaultKeyboard
? _pinBoxRow(context)
: GestureDetector(
3 months ago
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),
);
4 months ago
}
3 months ago
// Updated: Accept context for width calculation
Widget _otpTextInput(BuildContext context) {
4 months ago
var transparentBorder = OutlineInputBorder(
4 months ago
borderSide: BorderSide(color: Colors.transparent, width: 0),
4 months ago
);
return Container(
3 months ago
width: _width(context), // Use dynamic width
4 months ago
height: widget.pinBoxHeight,
child: TextField(
autofocus: !kIsWeb ? widget.autoFocus : false,
enableInteractiveSelection: false,
focusNode: focusNode,
controller: widget.controller,
3 months ago
textAlign: TextAlign.center,
4 months ago
keyboardType: widget.keyboardType,
inputFormatters: widget.keyboardType == TextInputType.number ? <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly] : null,
3 months ago
style: TextStyle(
height: 0.5,
color: Colors.transparent,
),
4 months ago
decoration: InputDecoration(
4 months ago
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,
3 months ago
border: InputBorder.none,
isDense: true),
4 months ago
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();
3 months ago
3 months ago
// Calculate dynamic width for all screen sizes to prevent scrolling
double screenWidth = MediaQuery.of(context).size.width;
double totalHorizontalPadding = widget.pinBoxOuterPadding.left + widget.pinBoxOuterPadding.right;
double totalMargin = widget.maxLength * totalHorizontalPadding;
double availableWidth = screenWidth - totalMargin - 32; // 32 for additional safe padding
double boxWidth = (availableWidth / widget.maxLength).clamp(60.0, 200.0); // Reasonable limits
3 months ago
4 months ago
List<Widget> pinCodes = List.generate(widget.maxLength, (int i) {
3 months ago
return Expanded(
child: Container(
margin: widget.pinBoxOuterPadding,
child: _buildPinCode(i, context, boxWidth),
),
);
4 months ago
});
3 months ago
3 months ago
return Row(
children: pinCodes,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
4 months ago
);
}
3 months ago
// Updated: Accept dynamic width
Widget _buildPinCode(int i, BuildContext context, double boxWidth) {
3 months ago
final bool isFilled = i < text.length && strList[i].isNotEmpty; // Check both conditions
4 months ago
final bool isCurrent = i == currentIndex;
final bool isFocused = hasFocus && isCurrent;
Color bgColor = Colors.white;
3 months ago
Color borderColor = Colors.white;
4 months ago
Color textColor = Colors.black;
4 months ago
if (widget.hasError) {
borderColor = widget.errorBorderColor;
4 months ago
bgColor = Colors.red.shade50;
} else if (isFocused) {
borderColor = Colors.transparent;
bgColor = Colors.white;
3 months ago
textColor = Colors.black; // Changed from white to black for better visibility
4 months ago
} else if (isFilled) {
borderColor = Colors.green;
bgColor = Colors.green;
textColor = Colors.white;
4 months ago
}
3 months ago
// When cleared/empty, colors remain as default initialized above
4 months ago
return Container(
key: ValueKey<String>("container$i"),
alignment: Alignment.center,
3 months ago
padding: EdgeInsets.zero,
4 months ago
decoration: BoxDecoration(
4 months ago
color: bgColor,
3 months ago
border: Border.all(color: borderColor, width: 1),
3 months ago
borderRadius: BorderRadius.circular(16),
4 months ago
),
height: widget.pinBoxHeight,
4 months ago
child: _animatedTextBox(
strList[i],
i,
textColor,
),
4 months ago
);
}
4 months ago
Widget _animatedTextBox(String text, int i, Color textColor) {
final bool isFilled = text.isNotEmpty;
3 months ago
final double fontSize = isFilled ? 50 : 50;
4 months ago
final FontWeight fontWeight = isFilled ? FontWeight.w600 : FontWeight.normal;
4 months ago
if (widget.pinTextAnimatedSwitcherTransition != null) {
return AnimatedSwitcher(
duration: widget.pinTextAnimatedSwitcherDuration,
4 months ago
transitionBuilder: widget.pinTextAnimatedSwitcherTransition!,
4 months ago
child: Text(
text,
3 months ago
softWrap: true,
4 months ago
key: ValueKey<String>("$text$i"),
3 months ago
style: widget.pinTextStyle?.copyWith(
color: textColor,
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: context.fontFamily,
) ??
TextStyle(
color: textColor,
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: context.fontFamily,
),
4 months ago
),
);
} else {
return Text(
text,
3 months ago
softWrap: true,
4 months ago
key: ValueKey<String>("${strList[i]}$i"),
3 months ago
style: widget.pinTextStyle?.copyWith(
color: textColor,
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: context.fontFamily,
) ??
TextStyle(
color: textColor,
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: context.fontFamily,
),
4 months ago
);
}
}
3 months ago
}
4 months ago
//
// 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),
// );
// }
3 months ago
// }}