Merge pull request 'validations & otp widget & focus & auto fill' (#32) from dev_aamir into master
Reviewed-on: #32pull/35/head
commit
f15018b0e8
@ -1,377 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double get _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(
|
|
||||||
children: <Widget>[
|
|
||||||
_otpTextInput(),
|
|
||||||
_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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _otpTextInput() {
|
|
||||||
var transparentBorder = OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Colors.transparent,
|
|
||||||
width: 0.0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return Container(
|
|
||||||
width: _width,
|
|
||||||
height: widget.pinBoxHeight,
|
|
||||||
child: TextField(
|
|
||||||
autofocus: !kIsWeb ? widget.autoFocus : false,
|
|
||||||
enableInteractiveSelection: false,
|
|
||||||
focusNode: focusNode,
|
|
||||||
controller: widget.controller,
|
|
||||||
keyboardType: widget.keyboardType,
|
|
||||||
inputFormatters: widget.keyboardType == TextInputType.number ? <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly] : null,
|
|
||||||
style: TextStyle(
|
|
||||||
height: 0.1,
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
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();
|
|
||||||
List<Widget> pinCodes = List.generate(widget.maxLength, (int i) {
|
|
||||||
return _buildPinCode(i, context);
|
|
||||||
});
|
|
||||||
return Row(
|
|
||||||
children: pinCodes,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPinCode(int i, BuildContext context) {
|
|
||||||
Color borderColor;
|
|
||||||
Color pinBoxColor = widget.pinBoxColor;
|
|
||||||
|
|
||||||
if (widget.hasError) {
|
|
||||||
borderColor = widget.errorBorderColor;
|
|
||||||
} else if (i < text.length) {
|
|
||||||
borderColor = widget.textBorderColor;
|
|
||||||
} else {
|
|
||||||
borderColor = widget.defaultBorderColor;
|
|
||||||
pinBoxColor = widget.pinBoxColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
EdgeInsets insets;
|
|
||||||
if (i == 0) {
|
|
||||||
insets = EdgeInsets.only(
|
|
||||||
left: 0,
|
|
||||||
top: widget.pinBoxOuterPadding.top,
|
|
||||||
right: widget.pinBoxOuterPadding.right,
|
|
||||||
bottom: widget.pinBoxOuterPadding.bottom,
|
|
||||||
);
|
|
||||||
} else if (i == strList.length - 1) {
|
|
||||||
insets = EdgeInsets.only(
|
|
||||||
left: widget.pinBoxOuterPadding.left,
|
|
||||||
top: widget.pinBoxOuterPadding.top,
|
|
||||||
right: 0,
|
|
||||||
bottom: widget.pinBoxOuterPadding.bottom,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
insets = widget.pinBoxOuterPadding;
|
|
||||||
}
|
|
||||||
return Container(
|
|
||||||
key: ValueKey<String>("container$i"),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0),
|
|
||||||
margin: insets,
|
|
||||||
child: _animatedTextBox(strList[i], i),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: borderColor,
|
|
||||||
width: widget.pinBoxBorderWidth,
|
|
||||||
),
|
|
||||||
color: pinBoxColor,
|
|
||||||
borderRadius: BorderRadius.circular(widget.pinBoxRadius),
|
|
||||||
),
|
|
||||||
width: widget.pinBoxWidth,
|
|
||||||
height: widget.pinBoxHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _animatedTextBox(String text, int i) {
|
|
||||||
if (widget.pinTextAnimatedSwitcherTransition != null) {
|
|
||||||
return AnimatedSwitcher(
|
|
||||||
duration: widget.pinTextAnimatedSwitcherDuration,
|
|
||||||
transitionBuilder: widget.pinTextAnimatedSwitcherTransition ??
|
|
||||||
(Widget child, Animation<double> animation) {
|
|
||||||
return child;
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
key: ValueKey<String>("$text$i"),
|
|
||||||
style: widget.pinTextStyle,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Text(
|
|
||||||
text,
|
|
||||||
key: ValueKey<String>("${strList[i]}$i"),
|
|
||||||
style: widget.pinTextStyle,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue