326 lines
12 KiB
Dart
326 lines
12 KiB
Dart
// ignore_for_file: deprecated_member_use
|
|
|
|
import 'package:didvan/config/design_config.dart';
|
|
import 'package:didvan/config/theme_data.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
|
|
class SearchField extends StatefulWidget {
|
|
final String title;
|
|
final FocusNode focusNode;
|
|
final bool? isFiltered;
|
|
final void Function(String value) onChanged;
|
|
final VoidCallback? onFilterButtonPressed;
|
|
final VoidCallback? onGoBack;
|
|
final String? value;
|
|
final String? extraIconPath;
|
|
final VoidCallback? onExtraIconPressed;
|
|
|
|
const SearchField({
|
|
Key? key,
|
|
required this.title,
|
|
required this.onChanged,
|
|
required this.focusNode,
|
|
this.onFilterButtonPressed,
|
|
this.isFiltered,
|
|
this.onGoBack,
|
|
this.value,
|
|
this.extraIconPath,
|
|
this.onExtraIconPressed,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<SearchField> createState() => _SearchFieldState();
|
|
}
|
|
|
|
class _SearchFieldState extends State<SearchField>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _pulseController;
|
|
late Animation<double> _pulseAnimation;
|
|
late AnimationController _rotationController;
|
|
late Animation<double> _rotationAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_pulseController = AnimationController(
|
|
duration: const Duration(milliseconds: 1500),
|
|
vsync: this,
|
|
)..repeat(reverse: true);
|
|
|
|
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
|
|
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
|
);
|
|
|
|
_rotationController = AnimationController(
|
|
duration: const Duration(seconds: 4),
|
|
vsync: this,
|
|
)..repeat();
|
|
|
|
_rotationAnimation = Tween<double>(begin: 0, end: 1).animate(
|
|
CurvedAnimation(parent: _rotationController, curve: Curves.linear),
|
|
);
|
|
|
|
widget.focusNode.addListener(() {
|
|
if (mounted) {
|
|
setState(() {});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
height: 47,
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: DesignConfig.isDark
|
|
? const Color.fromARGB(255, 188, 188, 188)
|
|
: const Color.fromARGB(255, 235, 235, 235),
|
|
borderRadius: BorderRadius.circular(40),
|
|
),
|
|
child: TextFormField(
|
|
initialValue: widget.value,
|
|
focusNode: widget.focusNode,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: Colors.black,
|
|
),
|
|
textAlignVertical: TextAlignVertical.center,
|
|
onChanged: widget.onChanged,
|
|
keyboardType: TextInputType.text,
|
|
textInputAction: TextInputAction.search,
|
|
decoration: InputDecoration(
|
|
suffixIcon: widget.onFilterButtonPressed != null
|
|
? SizedBox(
|
|
width: 48,
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: 8),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Stack(
|
|
children: [
|
|
GestureDetector(
|
|
onTap: widget.onFilterButtonPressed!,
|
|
child: Container(
|
|
width: 27,
|
|
height: 27,
|
|
padding: const EdgeInsets.all(4),
|
|
child: SvgPicture.asset(
|
|
"lib/assets/icons/search sort.svg",
|
|
),
|
|
),
|
|
),
|
|
if (widget.isFiltered!)
|
|
Positioned(
|
|
child: Container(
|
|
width: 10,
|
|
height: 10,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.secondary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
)
|
|
: null,
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: const BorderRadius.all(
|
|
Radius.circular(35),
|
|
),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
prefixIcon: GestureDetector(
|
|
onTap: widget.onGoBack,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(11.0),
|
|
child: SvgPicture.asset(
|
|
widget.onGoBack == null
|
|
? 'lib/assets/icons/search.svg'
|
|
: 'lib/assets/icons/search.svg',
|
|
width: 23,
|
|
height: 23,
|
|
),
|
|
),
|
|
),
|
|
prefixIconColor: Theme.of(context).colorScheme.inputText,
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: const BorderRadius.all(
|
|
Radius.circular(20),
|
|
),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).colorScheme.border,
|
|
),
|
|
),
|
|
fillColor: Colors.red,
|
|
contentPadding: const EdgeInsets.only(
|
|
left: 12,
|
|
right: 12,
|
|
),
|
|
border: InputBorder.none,
|
|
hintText: 'جستوجو در ${widget.title}',
|
|
hintStyle: TextStyle(
|
|
color: DesignConfig.isDark
|
|
? const Color.fromARGB(255, 98, 98, 98)
|
|
: const Color.fromARGB(255, 122, 122, 122),
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (widget.extraIconPath != null &&
|
|
widget.onExtraIconPressed != null) ...[
|
|
const SizedBox(width: 12),
|
|
AnimatedBuilder(
|
|
animation:
|
|
Listenable.merge([_pulseAnimation, _rotationAnimation]),
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: _pulseAnimation.value,
|
|
child: GestureDetector(
|
|
onTap: widget.onExtraIconPressed,
|
|
child: CustomPaint(
|
|
painter: RotatingBorderPainter(
|
|
rotation: _rotationAnimation.value,
|
|
color1: const Color(0xFFB20436),
|
|
color2: const Color(0xFFFFC8D7),
|
|
),
|
|
child: Container(
|
|
width: 50,
|
|
height: 50,
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
Color(0xFF0066AA),
|
|
Color(0xFF00AAFF),
|
|
],
|
|
),
|
|
shape: BoxShape.circle,
|
|
// border: Border.all(
|
|
// color: Colors.white.withOpacity(0.5),
|
|
// width: 2.5,
|
|
// ),
|
|
),
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Container(
|
|
width: 50,
|
|
height: 50,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [
|
|
Colors.white.withOpacity(0.3),
|
|
Colors.transparent,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(4.0),
|
|
child: SvgPicture.asset(
|
|
"lib/assets/icons/fluent_bot-sparkle-16-regular.svg",
|
|
width: 27,
|
|
height: 27,
|
|
colorFilter: const ColorFilter.mode(
|
|
Colors.white,
|
|
BlendMode.srcIn,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_pulseController.dispose();
|
|
_rotationController.dispose();
|
|
widget.focusNode.removeListener(() {});
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
class RotatingBorderPainter extends CustomPainter {
|
|
final double rotation;
|
|
final Color color1;
|
|
final Color color2;
|
|
|
|
RotatingBorderPainter({
|
|
required this.rotation,
|
|
required this.color1,
|
|
required this.color2,
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final center = Offset(size.width / 2, size.height / 2);
|
|
final radius = size.width / 2;
|
|
|
|
// 0.0 -> 0.5: fade in
|
|
// 0.5 -> 1.0: fade out
|
|
final fadeProgress = rotation < 0.5 ? rotation * 2 : (1 - rotation) * 2;
|
|
|
|
final opacity = fadeProgress.clamp(0.0, 1.0);
|
|
|
|
final paint = Paint()
|
|
..shader = SweepGradient(
|
|
colors: [
|
|
Colors.transparent,
|
|
color1.withOpacity(0.3 * opacity),
|
|
color1.withOpacity(0.7 * opacity),
|
|
color1.withOpacity(0.9 * opacity),
|
|
color1.withOpacity(0.7 * opacity),
|
|
color1.withOpacity(0.3 * opacity),
|
|
Colors.transparent,
|
|
],
|
|
stops: const [0.0, 0.15, 0.3, 0.5, 0.7, 0.85, 1.0],
|
|
transform: GradientRotation(rotation * 6.28318),
|
|
).createShader(Rect.fromCircle(center: center, radius: radius))
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 2
|
|
..strokeCap = StrokeCap.round;
|
|
|
|
canvas.drawCircle(
|
|
center,
|
|
radius + 0.5,
|
|
paint,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(RotatingBorderPainter oldDelegate) {
|
|
return oldDelegate.rotation != rotation;
|
|
}
|
|
}
|