596 lines
20 KiB
Dart
596 lines
20 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_svg/svg.dart';
|
|
import 'package:lba/gen/assets.gen.dart';
|
|
import 'package:lba/res/colors.dart';
|
|
import 'package:lba/widgets/buildWarpedInfo.dart';
|
|
import 'package:lba/widgets/rate.dart';
|
|
|
|
enum LikeState { none, liked, disliked }
|
|
|
|
class Reviews extends StatefulWidget {
|
|
final String name;
|
|
final String comment;
|
|
final double rate;
|
|
final int yesCount;
|
|
final int noCount;
|
|
final String date;
|
|
final Function(bool isLike)? onLikeDislike;
|
|
final bool? initialLikeState;
|
|
|
|
const Reviews({
|
|
super.key,
|
|
required this.name,
|
|
required this.comment,
|
|
required this.rate,
|
|
required this.yesCount,
|
|
required this.noCount,
|
|
required this.date,
|
|
this.onLikeDislike,
|
|
this.initialLikeState,
|
|
});
|
|
|
|
@override
|
|
State<Reviews> createState() => _ReviewsState();
|
|
}
|
|
|
|
class _ReviewsState extends State<Reviews> with TickerProviderStateMixin {
|
|
LikeState _likeState = LikeState.none;
|
|
int _currentYesCount = 0;
|
|
int _currentNoCount = 0;
|
|
|
|
late AnimationController _likeScaleController;
|
|
late AnimationController _dislikeScaleController;
|
|
late AnimationController _particleController;
|
|
late AnimationController _pulseController;
|
|
late AnimationController _countController;
|
|
late AnimationController _likeIconController;
|
|
late AnimationController _dislikeIconController;
|
|
|
|
late Animation<double> _likeScaleAnimation;
|
|
late Animation<double> _dislikeScaleAnimation;
|
|
late Animation<double> _particleAnimation;
|
|
late Animation<double> _pulseAnimation;
|
|
late Animation<double> _countAnimation;
|
|
late Animation<Color?> _likeColorAnimation;
|
|
late Animation<Color?> _dislikeColorAnimation;
|
|
late Animation<double> _likeIconRotationAnimation;
|
|
late Animation<double> _dislikeIconRotationAnimation;
|
|
late Animation<double> _likeIconScaleAnimation;
|
|
late Animation<double> _dislikeIconScaleAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_currentYesCount = widget.yesCount;
|
|
_currentNoCount = widget.noCount;
|
|
|
|
if (widget.initialLikeState == true) {
|
|
_likeState = LikeState.liked;
|
|
} else if (widget.initialLikeState == false) {
|
|
_likeState = LikeState.disliked;
|
|
}
|
|
|
|
_likeScaleController = AnimationController(
|
|
duration: const Duration(milliseconds: 600),
|
|
vsync: this,
|
|
);
|
|
_dislikeScaleController = AnimationController(
|
|
duration: const Duration(milliseconds: 600),
|
|
vsync: this,
|
|
);
|
|
_particleController = AnimationController(
|
|
duration: const Duration(milliseconds: 1200),
|
|
vsync: this,
|
|
);
|
|
_pulseController = AnimationController(
|
|
duration: const Duration(milliseconds: 800),
|
|
vsync: this,
|
|
);
|
|
_countController = AnimationController(
|
|
duration: const Duration(milliseconds: 400),
|
|
vsync: this,
|
|
);
|
|
_likeIconController = AnimationController(
|
|
duration: const Duration(milliseconds: 500),
|
|
vsync: this,
|
|
);
|
|
_dislikeIconController = AnimationController(
|
|
duration: const Duration(milliseconds: 500),
|
|
vsync: this,
|
|
);
|
|
|
|
_likeScaleAnimation = Tween<double>(begin: 1.0, end: 1.08).animate(
|
|
CurvedAnimation(parent: _likeScaleController, curve: Curves.easeOutBack),
|
|
);
|
|
|
|
_dislikeScaleAnimation = Tween<double>(begin: 1.0, end: 1.08).animate(
|
|
CurvedAnimation(
|
|
parent: _dislikeScaleController,
|
|
curve: Curves.easeOutBack,
|
|
),
|
|
);
|
|
|
|
_particleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
CurvedAnimation(parent: _particleController, curve: Curves.easeOutCubic),
|
|
);
|
|
|
|
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.15).animate(
|
|
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
|
);
|
|
|
|
_countAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
CurvedAnimation(parent: _countController, curve: Curves.bounceOut),
|
|
);
|
|
|
|
_likeColorAnimation = ColorTween(
|
|
begin: AppColors.hint,
|
|
end: const Color(0xFF4CAF50),
|
|
).animate(_likeScaleController);
|
|
|
|
_dislikeColorAnimation = ColorTween(
|
|
begin: AppColors.hint,
|
|
end: const Color(0xFFF44336),
|
|
).animate(_dislikeScaleController);
|
|
|
|
_likeIconRotationAnimation = Tween<double>(begin: 0.0, end: 0.1).animate(
|
|
CurvedAnimation(parent: _likeIconController, curve: Curves.elasticOut),
|
|
);
|
|
|
|
_dislikeIconRotationAnimation = Tween<double>(
|
|
begin: 0.0,
|
|
end: -0.1,
|
|
).animate(
|
|
CurvedAnimation(parent: _dislikeIconController, curve: Curves.elasticOut),
|
|
);
|
|
|
|
_likeIconScaleAnimation = Tween<double>(begin: 1.0, end: 1.3).animate(
|
|
CurvedAnimation(parent: _likeIconController, curve: Curves.elasticOut),
|
|
);
|
|
|
|
_dislikeIconScaleAnimation = Tween<double>(begin: 1.0, end: 1.3).animate(
|
|
CurvedAnimation(parent: _dislikeIconController, curve: Curves.elasticOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_likeScaleController.dispose();
|
|
_dislikeScaleController.dispose();
|
|
_particleController.dispose();
|
|
_pulseController.dispose();
|
|
_countController.dispose();
|
|
_likeIconController.dispose();
|
|
_dislikeIconController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onLikeTap() async {
|
|
if (_likeState == LikeState.liked) {
|
|
setState(() {
|
|
_likeState = LikeState.none;
|
|
_currentYesCount--;
|
|
});
|
|
_likeScaleController.reverse();
|
|
} else {
|
|
if (_likeState == LikeState.disliked) {
|
|
_currentNoCount--;
|
|
_dislikeScaleController.reverse();
|
|
}
|
|
|
|
setState(() {
|
|
_likeState = LikeState.liked;
|
|
_currentYesCount++;
|
|
});
|
|
|
|
_likeScaleController.forward();
|
|
_likeIconController.forward().then((_) => _likeIconController.reverse());
|
|
_particleController.forward().then((_) => _particleController.reset());
|
|
_pulseController.forward().then((_) => _pulseController.reverse());
|
|
_countController.forward().then((_) => _countController.reset());
|
|
}
|
|
|
|
widget.onLikeDislike?.call(true);
|
|
}
|
|
|
|
void _onDislikeTap() async {
|
|
if (_likeState == LikeState.disliked) {
|
|
setState(() {
|
|
_likeState = LikeState.none;
|
|
_currentNoCount--;
|
|
});
|
|
_dislikeScaleController.reverse();
|
|
} else {
|
|
if (_likeState == LikeState.liked) {
|
|
_currentYesCount--;
|
|
_likeScaleController.reverse();
|
|
}
|
|
|
|
setState(() {
|
|
_likeState = LikeState.disliked;
|
|
_currentNoCount++;
|
|
});
|
|
|
|
_dislikeScaleController.forward();
|
|
_dislikeIconController.forward().then(
|
|
(_) => _dislikeIconController.reverse(),
|
|
);
|
|
_particleController.forward().then((_) => _particleController.reset());
|
|
_pulseController.forward().then((_) => _pulseController.reverse());
|
|
_countController.forward().then((_) => _countController.reset());
|
|
}
|
|
|
|
widget.onLikeDislike?.call(false);
|
|
}
|
|
|
|
Widget _buildParticleEffect({required bool isLike}) {
|
|
return AnimatedBuilder(
|
|
animation: _particleAnimation,
|
|
builder: (context, child) {
|
|
return CustomPaint(
|
|
painter: ParticleEffectPainter(
|
|
progress: _particleAnimation.value,
|
|
color: isLike ? const Color(0xFF4CAF50) : const Color(0xFFF44336),
|
|
),
|
|
size: const Size(10, 10),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildAnimatedLikeButton() {
|
|
return AnimatedBuilder(
|
|
animation: Listenable.merge([
|
|
_likeScaleController,
|
|
_pulseController,
|
|
_countController,
|
|
]),
|
|
builder: (context, child) {
|
|
final isLiked = _likeState == LikeState.liked;
|
|
return GestureDetector(
|
|
onTap: _onLikeTap,
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
if (isLiked)
|
|
Positioned(
|
|
child: Transform.scale(
|
|
scale: _pulseAnimation.value,
|
|
child: Container(
|
|
width: 30,
|
|
height: 25,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(15),
|
|
color: _likeColorAnimation.value?.withOpacity(0.2),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
Transform.scale(
|
|
scale: _likeScaleAnimation.value,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(15),
|
|
color:
|
|
isLiked
|
|
? _likeColorAnimation.value?.withOpacity(0.1)
|
|
: null,
|
|
border:
|
|
isLiked
|
|
? Border.all(
|
|
color:
|
|
_likeColorAnimation.value ??
|
|
Colors.transparent,
|
|
width: 1.5,
|
|
)
|
|
: null,
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
AnimatedBuilder(
|
|
animation: Listenable.merge([
|
|
_likeColorAnimation,
|
|
_likeIconController,
|
|
]),
|
|
builder: (context, child) {
|
|
return Transform.rotate(
|
|
angle: _likeIconRotationAnimation.value,
|
|
child: Transform.scale(
|
|
scale:
|
|
isLiked ? _likeIconScaleAnimation.value : 1.0,
|
|
child: AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 200),
|
|
child: SvgPicture.asset(
|
|
isLiked
|
|
? Assets.icons.like.path
|
|
: Assets.icons.like.path,
|
|
key: ValueKey(isLiked ? 'liked' : 'normal'),
|
|
color:
|
|
isLiked
|
|
? _likeColorAnimation.value
|
|
: AppColors.hint,
|
|
width: 16,
|
|
height: 16,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(width: 4),
|
|
AnimatedBuilder(
|
|
animation: _countAnimation,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: 1.0 + (_countAnimation.value * 0.1),
|
|
child: AnimatedBuilder(
|
|
animation: _likeColorAnimation,
|
|
builder: (context, child) {
|
|
return Text(
|
|
"Yes ($_currentYesCount)",
|
|
style: TextStyle(
|
|
color:
|
|
isLiked
|
|
? _likeColorAnimation.value
|
|
: AppColors.hint,
|
|
fontWeight:
|
|
isLiked
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
fontSize: 12 + (_countAnimation.value * 1),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
if (isLiked)
|
|
Positioned(
|
|
top: -5,
|
|
right: -5,
|
|
child: _buildParticleEffect(isLike: true),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildAnimatedDislikeButton() {
|
|
return AnimatedBuilder(
|
|
animation: Listenable.merge([
|
|
_dislikeScaleController,
|
|
_pulseController,
|
|
_countController,
|
|
]),
|
|
builder: (context, child) {
|
|
final isDisliked = _likeState == LikeState.disliked;
|
|
return GestureDetector(
|
|
onTap: _onDislikeTap,
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
if (isDisliked)
|
|
Positioned(
|
|
child: Transform.scale(
|
|
scale: _pulseAnimation.value,
|
|
child: Container(
|
|
width: 30,
|
|
height: 25,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(15),
|
|
color: _dislikeColorAnimation.value?.withOpacity(0.2),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
Transform.scale(
|
|
scale: _dislikeScaleAnimation.value,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(15),
|
|
color:
|
|
isDisliked
|
|
? _dislikeColorAnimation.value?.withOpacity(0.1)
|
|
: null,
|
|
border:
|
|
isDisliked
|
|
? Border.all(
|
|
color:
|
|
_dislikeColorAnimation.value ??
|
|
Colors.transparent,
|
|
width: 1.5,
|
|
)
|
|
: null,
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
AnimatedBuilder(
|
|
animation: Listenable.merge([
|
|
_dislikeColorAnimation,
|
|
_dislikeIconController,
|
|
]),
|
|
builder: (context, child) {
|
|
return Transform.rotate(
|
|
angle: _dislikeIconRotationAnimation.value,
|
|
child: Transform.scale(
|
|
scale:
|
|
isDisliked
|
|
? _dislikeIconScaleAnimation.value
|
|
: 1.0,
|
|
child: AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 200),
|
|
child: SvgPicture.asset(
|
|
isDisliked
|
|
? Assets.icons.dislike.path
|
|
: Assets.icons.dislike.path,
|
|
key: ValueKey(
|
|
isDisliked ? 'disliked' : 'normal',
|
|
),
|
|
color:
|
|
isDisliked
|
|
? _dislikeColorAnimation.value
|
|
: AppColors.hint,
|
|
width: 16,
|
|
height: 16,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(width: 4),
|
|
AnimatedBuilder(
|
|
animation: _countAnimation,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: 1.0 + (_countAnimation.value * 0.1),
|
|
child: AnimatedBuilder(
|
|
animation: _dislikeColorAnimation,
|
|
builder: (context, child) {
|
|
return Text(
|
|
"No ($_currentNoCount)",
|
|
style: TextStyle(
|
|
color:
|
|
isDisliked
|
|
? _dislikeColorAnimation.value
|
|
: AppColors.hint,
|
|
fontWeight:
|
|
isDisliked
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
fontSize: 12 + (_countAnimation.value * 1),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
if (isDisliked)
|
|
Positioned(
|
|
top: -5,
|
|
right: -5,
|
|
child: _buildParticleEffect(isLike: false),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(widget.name),
|
|
const SizedBox(width: 2),
|
|
Image.asset(Assets.images.usa.path),
|
|
],
|
|
),
|
|
Text(
|
|
"Verified Buyer",
|
|
style: TextStyle(color: AppColors.confirmButton),
|
|
),
|
|
],
|
|
),
|
|
CustomStarRating(rating: widget.rate),
|
|
],
|
|
),
|
|
const SizedBox(height: 7),
|
|
buildWrappedInfo("", widget.comment),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
_buildAnimatedLikeButton(),
|
|
const SizedBox(width: 7),
|
|
_buildAnimatedDislikeButton(),
|
|
],
|
|
),
|
|
Text(widget.date, style: TextStyle(color: AppColors.hint)),
|
|
],
|
|
),
|
|
const SizedBox(height: 5),
|
|
const Divider(thickness: 1.2, height: 2),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ParticleEffectPainter extends CustomPainter {
|
|
final double progress;
|
|
final Color color;
|
|
|
|
ParticleEffectPainter({required this.progress, required this.color});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
if (progress == 0.0) return;
|
|
|
|
final paint =
|
|
Paint()
|
|
..color = color.withOpacity(1.0 - progress)
|
|
..style = PaintingStyle.fill;
|
|
|
|
final particles = [
|
|
[0.2, -0.3, 3.0],
|
|
[0.5, -0.5, 2.5],
|
|
[0.8, -0.2, 2.0],
|
|
[0.1, -0.6, 1.5],
|
|
[0.9, -0.4, 2.0],
|
|
];
|
|
|
|
for (final particle in particles) {
|
|
final x =
|
|
size.width * particle[0] + (progress * 20 * (particle[0] - 0.5));
|
|
final y = size.height * particle[1] * progress;
|
|
final particleSize = particle[2] * (1.0 - progress * 0.5);
|
|
|
|
canvas.drawCircle(Offset(x, y), particleSize, paint);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(ParticleEffectPainter oldDelegate) {
|
|
return oldDelegate.progress != progress;
|
|
}
|
|
}
|