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.
341 lines
10 KiB
Java
341 lines
10 KiB
Java
'dart:math';
|
|
'package:flutter/gestures.dart';
|
|
'package:flutter/material.dart'; 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
|
|
enum TextFieldInertiaDirection {
|
|
left,
|
|
right,
|
|
}
|
|
|
|
Interval _getInternalInterval(
|
|
double start,
|
|
double end,
|
|
double externalStart,
|
|
double externalEnd, [
|
|
Curve curve = Curves.linear,
|
|
]) {
|
|
return Interval(
|
|
start + (end - start) * externalStart,
|
|
start + (end - start) * externalEnd,
|
|
curve: curve,
|
|
);
|
|
}
|
|
|
|
class AnimatedTextFormField extends StatefulWidget {
|
|
AnimatedTextFormField({
|
|
Key key,
|
|
this.interval = const Interval(0.0, 1.0),
|
|
@required this.width,
|
|
this.loadingController,
|
|
this.inertiaController,
|
|
this.inertiaDirection,
|
|
this.enabled = true,
|
|
this.labelText,
|
|
this.prefixIcon,
|
|
this.suffixIcon,
|
|
this.keyboardType,
|
|
this.textInputAction,
|
|
this.obscureText = false,
|
|
this.controller,
|
|
this.focusNode,
|
|
this.validator,
|
|
this.onFieldSubmitted,
|
|
this.onSaved,
|
|
}) : assert((inertiaController == null && inertiaDirection == null) ||
|
|
(inertiaController != null && inertiaDirection != null)),
|
|
super(key: key);
|
|
|
|
final Interval interval;
|
|
final AnimationController loadingController;
|
|
final AnimationController inertiaController;
|
|
final double width;
|
|
final bool enabled;
|
|
final String labelText;
|
|
final Widget prefixIcon;
|
|
final Widget suffixIcon;
|
|
final TextInputType keyboardType;
|
|
final TextInputAction textInputAction;
|
|
final bool obscureText;
|
|
final TextEditingController controller;
|
|
final FocusNode focusNode;
|
|
final FormFieldValidator<String> validator;
|
|
final ValueChanged<String> onFieldSubmitted;
|
|
final FormFieldSetter<String> onSaved;
|
|
final TextFieldInertiaDirection inertiaDirection;
|
|
|
|
@override
|
|
_AnimatedTextFormFieldState createState() => _AnimatedTextFormFieldState();
|
|
}
|
|
|
|
class _AnimatedTextFormFieldState extends State<AnimatedTextFormField> {
|
|
Animation<double> scaleAnimation;
|
|
Animation<double> sizeAnimation;
|
|
Animation<double> suffixIconOpacityAnimation;
|
|
|
|
Animation<double> fieldTranslateAnimation;
|
|
Animation<double> iconRotationAnimation;
|
|
Animation<double> iconTranslateAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
widget.inertiaController?.addStatusListener(handleAnimationStatus);
|
|
|
|
final interval = widget.interval;
|
|
final loadingController = widget.loadingController;
|
|
|
|
if (loadingController != null) {
|
|
scaleAnimation = Tween<double>(
|
|
begin: 0.0,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(
|
|
parent: loadingController,
|
|
curve: _getInternalInterval(
|
|
0, .2, interval.begin, interval.end, Curves.easeOutBack),
|
|
));
|
|
suffixIconOpacityAnimation =
|
|
Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(
|
|
parent: loadingController,
|
|
curve: _getInternalInterval(.65, 1.0, interval.begin, interval.end),
|
|
));
|
|
_updateSizeAnimation();
|
|
}
|
|
|
|
final inertiaController = widget.inertiaController;
|
|
final inertiaDirection = widget.inertiaDirection;
|
|
final sign = inertiaDirection == TextFieldInertiaDirection.right ? 1 : -1;
|
|
|
|
if (inertiaController != null) {
|
|
fieldTranslateAnimation = Tween<double>(
|
|
begin: 0.0,
|
|
end: sign * 15.0,
|
|
).animate(CurvedAnimation(
|
|
parent: inertiaController,
|
|
curve: Interval(0, .5, curve: Curves.easeOut),
|
|
reverseCurve: Curves.easeIn,
|
|
));
|
|
iconRotationAnimation =
|
|
Tween<double>(begin: 0.0, end: sign * pi / 12 /* ~15deg */)
|
|
.animate(CurvedAnimation(
|
|
parent: inertiaController,
|
|
curve: Interval(.5, 1.0, curve: Curves.easeOut),
|
|
reverseCurve: Curves.easeIn,
|
|
));
|
|
iconTranslateAnimation =
|
|
Tween<double>(begin: 0.0, end: 8.0).animate(CurvedAnimation(
|
|
parent: inertiaController,
|
|
curve: Interval(.5, 1.0, curve: Curves.easeOut),
|
|
reverseCurve: Curves.easeIn,
|
|
));
|
|
}
|
|
}
|
|
|
|
void _updateSizeAnimation() {
|
|
final interval = widget.interval;
|
|
final loadingController = widget.loadingController;
|
|
|
|
sizeAnimation = Tween<double>(
|
|
begin: 48.0,
|
|
end: widget.width,
|
|
).animate(CurvedAnimation(
|
|
parent: loadingController,
|
|
curve: _getInternalInterval(
|
|
.2, 1.0, interval.begin, interval.end, Curves.linearToEaseOut),
|
|
reverseCurve: Curves.easeInExpo,
|
|
));
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(AnimatedTextFormField oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
if (oldWidget.width != widget.width) {
|
|
_updateSizeAnimation();
|
|
}
|
|
}
|
|
|
|
@override
|
|
dispose() {
|
|
widget.inertiaController?.removeStatusListener(handleAnimationStatus);
|
|
super.dispose();
|
|
}
|
|
|
|
void handleAnimationStatus(status) {
|
|
if (status == AnimationStatus.completed) {
|
|
widget.inertiaController?.reverse();
|
|
}
|
|
}
|
|
|
|
Widget _buildInertiaAnimation(Widget child) {
|
|
if (widget.inertiaController == null) {
|
|
return child;
|
|
}
|
|
|
|
return AnimatedBuilder(
|
|
animation: iconTranslateAnimation,
|
|
builder: (context, child) => Transform(
|
|
alignment: Alignment.center,
|
|
transform: Matrix4.identity()
|
|
..translate(iconTranslateAnimation.value)
|
|
..rotateZ(iconRotationAnimation.value),
|
|
child: child,
|
|
),
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
InputDecoration _getInputDecoration(ThemeData theme) {
|
|
return InputDecoration(
|
|
labelText: widget.labelText,
|
|
prefixIcon: _buildInertiaAnimation(widget.prefixIcon),
|
|
suffixIcon: _buildInertiaAnimation(widget.loadingController != null
|
|
? FadeTransition(
|
|
opacity: suffixIconOpacityAnimation,
|
|
child: widget.suffixIcon,
|
|
)
|
|
: widget.suffixIcon),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
Widget textField = TextFormField(
|
|
controller: widget.controller,
|
|
focusNode: widget.focusNode,
|
|
decoration: _getInputDecoration(theme),
|
|
keyboardType: widget.keyboardType,
|
|
textInputAction: widget.textInputAction,
|
|
obscureText: widget.obscureText,
|
|
onFieldSubmitted: widget.onFieldSubmitted,
|
|
onSaved: widget.onSaved,
|
|
validator: widget.validator,
|
|
enabled: widget.enabled,
|
|
);
|
|
|
|
if (widget.loadingController != null) {
|
|
textField = ScaleTransition(
|
|
scale: scaleAnimation,
|
|
child: AnimatedBuilder(
|
|
animation: sizeAnimation,
|
|
builder: (context, child) => ConstrainedBox(
|
|
constraints: BoxConstraints.tightFor(width: sizeAnimation.value),
|
|
child: child,
|
|
),
|
|
child: textField,
|
|
),
|
|
);
|
|
}
|
|
|
|
if (widget.inertiaController != null) {
|
|
textField = AnimatedBuilder(
|
|
animation: fieldTranslateAnimation,
|
|
builder: (context, child) => Transform.translate(
|
|
offset: Offset(fieldTranslateAnimation.value, 0),
|
|
child: child,
|
|
),
|
|
child: textField,
|
|
);
|
|
}
|
|
|
|
return textField;
|
|
}
|
|
}
|
|
|
|
class AnimatedPasswordTextFormField extends StatefulWidget {
|
|
AnimatedPasswordTextFormField({
|
|
Key key,
|
|
this.interval = const Interval(0.0, 1.0),
|
|
@required this.animatedWidth,
|
|
this.loadingController,
|
|
this.inertiaController,
|
|
this.inertiaDirection,
|
|
this.enabled = true,
|
|
this.labelText,
|
|
this.keyboardType,
|
|
this.textInputAction,
|
|
this.controller,
|
|
this.focusNode,
|
|
this.validator,
|
|
this.onFieldSubmitted,
|
|
this.onSaved,
|
|
}) : assert((inertiaController == null && inertiaDirection == null) ||
|
|
(inertiaController != null && inertiaDirection != null)),
|
|
super(key: key);
|
|
|
|
final Interval interval;
|
|
final AnimationController loadingController;
|
|
final AnimationController inertiaController;
|
|
final double animatedWidth;
|
|
final bool enabled;
|
|
final String labelText;
|
|
final TextInputType keyboardType;
|
|
final TextInputAction textInputAction;
|
|
final TextEditingController controller;
|
|
final FocusNode focusNode;
|
|
final FormFieldValidator<String> validator;
|
|
final ValueChanged<String> onFieldSubmitted;
|
|
final FormFieldSetter<String> onSaved;
|
|
final TextFieldInertiaDirection inertiaDirection;
|
|
|
|
@override
|
|
_AnimatedPasswordTextFormFieldState createState() =>
|
|
_AnimatedPasswordTextFormFieldState();
|
|
}
|
|
|
|
class _AnimatedPasswordTextFormFieldState
|
|
extends State<AnimatedPasswordTextFormField> {
|
|
var _obscureText = true;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedTextFormField(
|
|
interval: widget.interval,
|
|
loadingController: widget.loadingController,
|
|
inertiaController: widget.inertiaController,
|
|
width: widget.animatedWidth,
|
|
enabled: widget.enabled,
|
|
labelText: widget.labelText,
|
|
prefixIcon: Icon(FontAwesomeIcons.lock, size: 20),
|
|
suffixIcon: GestureDetector(
|
|
onTap: () => setState(() => _obscureText = !_obscureText),
|
|
dragStartBehavior: DragStartBehavior.down,
|
|
child: AnimatedCrossFade(
|
|
duration: const Duration(milliseconds: 250),
|
|
firstCurve: Curves.easeInOutSine,
|
|
secondCurve: Curves.easeInOutSine,
|
|
alignment: Alignment.center,
|
|
layoutBuilder: (Widget topChild, _, Widget bottomChild, __) {
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: <Widget>[bottomChild, topChild],
|
|
);
|
|
},
|
|
firstChild: Icon(
|
|
Icons.visibility,
|
|
size: 25.0,
|
|
semanticLabel: 'show password',
|
|
),
|
|
secondChild: Icon(
|
|
Icons.visibility_off,
|
|
size: 25.0,
|
|
semanticLabel: 'hide password',
|
|
),
|
|
crossFadeState: _obscureText
|
|
? CrossFadeState.showFirst
|
|
: CrossFadeState.showSecond,
|
|
),
|
|
),
|
|
obscureText: _obscureText,
|
|
keyboardType: widget.keyboardType,
|
|
textInputAction: widget.textInputAction,
|
|
controller: widget.controller,
|
|
focusNode: widget.focusNode,
|
|
validator: widget.validator,
|
|
onFieldSubmitted: widget.onFieldSubmitted,
|
|
onSaved: widget.onSaved,
|
|
inertiaDirection: widget.inertiaDirection,
|
|
);
|
|
}
|
|
} |