Files
xianyan/lib/shared/widgets/tab_icon_sprite.dart
2026-05-22 02:04:46 +08:00

1401 lines
40 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 闲言APP — Tab图标精灵动画组件
// 创建时间: 2026-05-14
// 更新时间: 2026-05-22
// 作用: 底部导航Tab图标支持表情精灵/呼吸光效/果冻弹跳/相邻注视/文字显隐
// 上次更新: 集成PerformanceOrchestrator前后台感知后台暂停glow呼吸动画
// ============================================================
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import '../../core/services/performance/performance_orchestrator.dart';
import '../../core/theme/app_theme.dart';
import '../../core/theme/app_typography.dart';
enum TabSpriteType { home, discover, profile }
class TabIconSprite extends StatefulWidget {
const TabIconSprite({
super.key,
required this.type,
required this.label,
required this.isSelected,
required this.adjacentDirection,
required this.animationIntensity,
required this.characterId,
required this.eyeScale,
required this.mouthCurve,
required this.bounceMultiplier,
});
final TabSpriteType type;
final String label;
final bool isSelected;
final int adjacentDirection;
final double animationIntensity;
final String characterId;
final double eyeScale;
final double mouthCurve;
final double bounceMultiplier;
@override
State<TabIconSprite> createState() => _TabIconSpriteState();
}
class _TabIconSpriteState extends State<TabIconSprite>
with TickerProviderStateMixin {
late AnimationController _bounceController;
late AnimationController _glowController;
late AnimationController _expressionController;
late AnimationController _lookController;
late AnimationController _labelController;
late Animation<double> _bounceScale;
late Animation<double> _bounceRotation;
late Animation<double> _glowPulse;
late Animation<double> _expressionProgress;
late Animation<double> _lookOffset;
late Animation<double> _labelOpacity;
late Animation<Offset> _labelOffset;
@override
void initState() {
super.initState();
_bounceController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_glowController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
_expressionController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_lookController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 250),
);
_labelController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_setupBounceAnimations();
_glowPulse = Tween(begin: 0.3, end: 1.0).animate(
CurvedAnimation(parent: _glowController, curve: Curves.easeInOut),
);
_expressionProgress = CurvedAnimation(
parent: _expressionController,
curve: Curves.easeOutCubic,
);
_lookOffset = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _lookController, curve: Curves.easeOutCubic),
);
_labelOpacity = CurvedAnimation(
parent: _labelController,
curve: Curves.easeOutCubic,
);
_labelOffset = Tween<Offset>(begin: Offset.zero, end: const Offset(0, 0.5))
.animate(
CurvedAnimation(parent: _labelController, curve: Curves.easeInCubic),
);
if (widget.isSelected) {
_expressionController.value = 1.0;
_glowController.repeat(reverse: true);
_labelController.value = 1.0;
}
PerformanceOrchestrator.instance.onForeground(_onForeground);
PerformanceOrchestrator.instance.onBackground(_onBackground);
}
void _setupBounceAnimations() {
_bounceScale = TweenSequence<double>([
TweenSequenceItem(
tween: Tween(
begin: 1.0,
end: 1.0 + 0.35 * widget.bounceMultiplier,
).chain(CurveTween(curve: Curves.easeOutCubic)),
weight: 30,
),
TweenSequenceItem(
tween: Tween(
begin: 1.0 + 0.35 * widget.bounceMultiplier,
end: 1.0 - 0.08 * widget.bounceMultiplier,
).chain(CurveTween(curve: Curves.easeInCubic)),
weight: 25,
),
TweenSequenceItem(
tween: Tween(
begin: 1.0 - 0.08 * widget.bounceMultiplier,
end: 1.0 + 0.05 * widget.bounceMultiplier,
).chain(CurveTween(curve: Curves.easeOutCubic)),
weight: 20,
),
TweenSequenceItem(
tween: Tween(
begin: 1.0 + 0.05 * widget.bounceMultiplier,
end: 1.0,
).chain(CurveTween(curve: Curves.easeOutCubic)),
weight: 25,
),
]).animate(_bounceController);
_bounceRotation = TweenSequence<double>([
TweenSequenceItem(
tween: Tween(
begin: 0.0,
end: 0.08 * widget.bounceMultiplier,
).chain(CurveTween(curve: Curves.easeOutCubic)),
weight: 25,
),
TweenSequenceItem(
tween: Tween(
begin: 0.08 * widget.bounceMultiplier,
end: -0.05 * widget.bounceMultiplier,
).chain(CurveTween(curve: Curves.easeInOutCubic)),
weight: 25,
),
TweenSequenceItem(
tween: Tween(
begin: -0.05 * widget.bounceMultiplier,
end: 0.02 * widget.bounceMultiplier,
).chain(CurveTween(curve: Curves.easeOutCubic)),
weight: 25,
),
TweenSequenceItem(
tween: Tween(
begin: 0.02 * widget.bounceMultiplier,
end: 0.0,
).chain(CurveTween(curve: Curves.easeOutCubic)),
weight: 25,
),
]).animate(_bounceController);
}
@override
void didUpdateWidget(TabIconSprite oldWidget) {
super.didUpdateWidget(oldWidget);
final intensity = widget.animationIntensity;
if (widget.bounceMultiplier != oldWidget.bounceMultiplier) {
_setupBounceAnimations();
}
if (widget.isSelected && !oldWidget.isSelected) {
_bounceController.duration = Duration(
milliseconds: (500 * intensity).round(),
);
_bounceController.forward(from: 0.0);
_expressionController.duration = Duration(
milliseconds: (300 * intensity).round(),
);
_expressionController.forward();
_glowController.repeat(reverse: true);
_lookController.reverse();
_labelController.forward();
} else if (!widget.isSelected && oldWidget.isSelected) {
_expressionController.reverse();
_glowController.stop();
_glowController.value = 0.0;
_bounceController.reverse();
_labelController.reverse();
}
if (!widget.isSelected && widget.adjacentDirection != 0) {
_lookController.duration = Duration(
milliseconds: (250 * intensity).round(),
);
_lookController.forward();
} else if (!widget.isSelected && widget.adjacentDirection == 0) {
_lookController.reverse();
}
}
@override
void dispose() {
PerformanceOrchestrator.instance.removeCallbacks(_onForeground);
PerformanceOrchestrator.instance.removeCallbacks(_onBackground);
_bounceController.dispose();
_glowController.dispose();
_expressionController.dispose();
_lookController.dispose();
_labelController.dispose();
super.dispose();
}
void _onForeground() {
if (widget.isSelected) {
_glowController.repeat(reverse: true);
}
}
void _onBackground() {
_glowController.stop();
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return AnimatedBuilder(
animation: Listenable.merge([
_bounceController,
_glowController,
_expressionController,
_lookController,
_labelController,
]),
builder: (context, _) {
final scale = _bounceScale.value;
final rotation = _bounceRotation.value;
final glowAlpha = _glowPulse.value;
final exprProgress = _expressionProgress.value;
final lookDir = widget.adjacentDirection.toDouble() * _lookOffset.value;
final labelAlpha = 1.0 - _labelOpacity.value;
final labelSlide = _labelOffset.value;
final iconSize = widget.isSelected ? 36.0 : 24.0;
final glowSize = 40.0 * widget.bounceMultiplier;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: glowSize,
height: glowSize,
child: Stack(
alignment: Alignment.center,
clipBehavior: Clip.none,
children: [
if (widget.isSelected)
CustomPaint(
size: Size(glowSize * 1.4, glowSize * 1.4),
painter: _GlowPainter(
color: ext.accent,
alpha: glowAlpha * 0.55,
radiusMultiplier: 1.4,
),
),
Transform.scale(
scale: scale,
child: Transform.rotate(
angle: rotation,
child: SizedBox(
width: iconSize,
height: iconSize,
child: CustomPaint(
painter: _RefinedSpritePainter(
type: widget.type,
characterId: widget.characterId,
expressionProgress: exprProgress,
lookOffset: lookDir,
eyeScale: widget.eyeScale,
mouthCurve: widget.mouthCurve,
isSelected: widget.isSelected,
color: widget.isSelected
? (ext.isDark ? Colors.white : ext.accent)
: (ext.isDark
? Colors.white38
: const Color(0xFFAEAEB2)),
),
),
),
),
),
if (widget.isSelected)
...List.generate(
7,
(i) => _FloatingParticle(
color: ext.accent,
index: i,
progress: glowAlpha,
),
),
],
),
),
SizedBox(
height: 16,
child: FractionalTranslation(
translation: labelSlide,
child: Opacity(
opacity: labelAlpha.clamp(0.0, 1.0),
child: Text(
widget.label,
style: AppTypography.caption1.copyWith(
color: widget.isSelected
? (ext.isDark ? Colors.white : ext.accent)
: (ext.isDark
? Colors.white38
: const Color(0xFFAEAEB2)),
fontWeight: FontWeight.w500,
fontSize: 12,
height: 1.2,
),
),
),
),
),
],
);
},
);
}
}
class _GlowPainter extends CustomPainter {
_GlowPainter({
required this.color,
required this.alpha,
this.radiusMultiplier = 1.0,
});
final Color color;
final double alpha;
final double radiusMultiplier;
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width / 2) * radiusMultiplier;
final paint = Paint()
..shader = ui.Gradient.radial(
center,
radius,
[
color.withValues(alpha: alpha.clamp(0.0, 1.0)),
color.withValues(alpha: alpha.clamp(0.0, 1.0) * 0.6),
color.withValues(alpha: alpha.clamp(0.0, 1.0) * 0.2),
color.withValues(alpha: 0.0),
],
[0.0, 0.35, 0.7, 1.0],
)
..isAntiAlias = true
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8);
canvas.drawCircle(center, radius, paint);
}
@override
bool shouldRepaint(_GlowPainter oldDelegate) {
return alpha != oldDelegate.alpha ||
color != oldDelegate.color ||
radiusMultiplier != oldDelegate.radiusMultiplier;
}
}
class _FloatingParticle extends StatelessWidget {
const _FloatingParticle({
required this.color,
required this.index,
required this.progress,
});
final Color color;
final int index;
final double progress;
@override
Widget build(BuildContext context) {
final angle = (index * 0.898) + progress * 1.047;
final radius = 22.0 + progress * 14.0;
final dx = radius * math.cos(angle);
final dy = radius * math.sin(angle);
final particleAlpha = (1.0 - progress).clamp(0.0, 1.0) * 0.7;
final particleSize = 4.5 - index * 0.3;
return Transform.translate(
offset: Offset(dx, dy),
child: Container(
width: particleSize,
height: particleSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color.withValues(alpha: particleAlpha),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: particleAlpha * 0.6),
blurRadius: 6,
spreadRadius: 2,
),
],
),
),
);
}
}
class _RefinedSpritePainter extends CustomPainter {
_RefinedSpritePainter({
required this.type,
required this.characterId,
required this.expressionProgress,
required this.lookOffset,
required this.eyeScale,
required this.mouthCurve,
required this.isSelected,
required this.color,
});
final TabSpriteType type;
final String characterId;
final double expressionProgress;
final double lookOffset;
final double eyeScale;
final double mouthCurve;
final bool isSelected;
final Color color;
@override
void paint(Canvas canvas, Size size) {
final s = size.width / 32.0;
canvas.save();
canvas.scale(s, s);
switch (characterId) {
case 'cat':
_paintCat(canvas);
case 'dog':
_paintDog(canvas);
case 'boy':
_paintBoy(canvas);
case 'girl':
_paintGirl(canvas);
default:
_paintCat(canvas);
}
canvas.restore();
}
// ============================================================
// 猫咪造型
// ============================================================
void _paintCat(Canvas canvas) {
final fill = _fill();
final subFill = _subFill();
final stroke = _stroke(1.2);
switch (type) {
case TabSpriteType.home:
_paintCatHome(canvas, fill, subFill, stroke);
case TabSpriteType.discover:
_paintCatDiscover(canvas, fill, subFill, stroke);
case TabSpriteType.profile:
_paintCatProfile(canvas, fill, subFill, stroke);
}
}
void _paintCatHome(Canvas canvas, Paint fill, Paint subFill, Paint stroke) {
final roof = Path();
roof.moveTo(16, 2);
roof.cubicTo(14.5, 4, 3, 10, 3, 12);
roof.lineTo(29, 12);
roof.cubicTo(29, 10, 17.5, 4, 16, 2);
roof.close();
canvas.drawPath(roof, fill);
final body = Path();
body.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(5, 12, 22, 14),
topLeft: const Radius.circular(2),
topRight: const Radius.circular(2),
bottomLeft: const Radius.circular(3),
bottomRight: const Radius.circular(3),
),
);
canvas.drawPath(body, fill);
final door = Path();
door.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(13, 19, 6, 7),
topLeft: const Radius.circular(1),
topRight: const Radius.circular(1),
bottomLeft: const Radius.circular(3),
bottomRight: const Radius.circular(3),
),
);
canvas.drawPath(door, subFill);
final doorknob = _fill(alpha: 0.5);
canvas.drawCircle(const Offset(17.5, 23), 0.6, doorknob);
final window = _fill(alpha: 0.25);
canvas.drawRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(7, 14, 4, 3),
topLeft: const Radius.circular(0.8),
topRight: const Radius.circular(0.8),
bottomLeft: const Radius.circular(0.8),
bottomRight: const Radius.circular(0.8),
),
window,
);
canvas.drawRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(21, 14, 4, 3),
topLeft: const Radius.circular(0.8),
topRight: const Radius.circular(0.8),
bottomLeft: const Radius.circular(0.8),
bottomRight: const Radius.circular(0.8),
),
window,
);
_paintCatEars(canvas, fill, subFill);
_paintFace(canvas, 16, 14);
if (isSelected) {
_paintCatWhiskers(canvas, stroke);
_paintChimneyHeart(canvas);
}
}
void _paintCatDiscover(
Canvas canvas,
Paint fill,
Paint subFill,
Paint stroke,
) {
canvas.drawCircle(const Offset(16, 16), 11, fill);
final innerRing = _stroke(1.2, alpha: 0.25);
canvas.drawCircle(const Offset(16, 16), 7.5, innerRing);
final needle = Path();
needle.moveTo(16, 7);
needle.cubicTo(15.2, 10, 14.8, 13, 16, 16);
needle.cubicTo(17.2, 13, 16.8, 10, 16, 7);
needle.close();
canvas.drawPath(needle, fill);
final needle2 = Path();
needle2.moveTo(16, 25);
needle2.cubicTo(15.2, 22, 14.8, 19, 16, 16);
needle2.cubicTo(17.2, 19, 16.8, 22, 16, 25);
needle2.close();
canvas.drawPath(needle2, _fill(alpha: 0.35));
canvas.drawCircle(const Offset(16, 16), 1.5, _fill(alpha: 0.5));
_paintCatEars(canvas, fill, subFill);
_paintFace(canvas, 16, 16);
if (isSelected) {
_paintCatWhiskers(canvas, stroke);
_paintCompassSparks(canvas);
}
}
void _paintCatProfile(
Canvas canvas,
Paint fill,
Paint subFill,
Paint stroke,
) {
final head = Path();
head.addOval(const Rect.fromLTWH(8, 4, 16, 14));
canvas.drawPath(head, fill);
final body = Path();
body.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(7, 17, 18, 10),
topLeft: const Radius.circular(6),
topRight: const Radius.circular(6),
bottomLeft: const Radius.circular(4),
bottomRight: const Radius.circular(4),
),
);
canvas.drawPath(body, fill);
_paintCatEars(canvas, fill, subFill);
_paintFace(canvas, 16, 10);
if (isSelected) {
_paintCatWhiskers(canvas, stroke);
_paintHalo(canvas);
}
}
void _paintCatEars(Canvas canvas, Paint fill, Paint subFill) {
final leftEar = Path();
leftEar.moveTo(7, 9);
leftEar.cubicTo(6, 5, 8, 1, 11, 3);
leftEar.cubicTo(10, 5, 9, 7, 10, 9);
leftEar.close();
canvas.drawPath(leftEar, fill);
final leftInner = Path();
leftInner.moveTo(8, 8);
leftInner.cubicTo(7.5, 5.5, 9, 3, 10.5, 4);
leftInner.cubicTo(10, 5.5, 9.5, 7, 10, 8);
leftInner.close();
canvas.drawPath(leftInner, subFill);
final rightEar = Path();
rightEar.moveTo(22, 9);
rightEar.cubicTo(22, 7, 21, 5, 21, 3);
rightEar.cubicTo(24, 1, 26, 5, 25, 9);
rightEar.close();
canvas.drawPath(rightEar, fill);
final rightInner = Path();
rightInner.moveTo(22, 8);
rightInner.cubicTo(22, 6, 21.5, 5.5, 21.5, 4);
rightInner.cubicTo(23, 3, 24.5, 5.5, 24, 8);
rightInner.close();
canvas.drawPath(rightInner, subFill);
}
void _paintCatWhiskers(Canvas canvas, Paint stroke) {
final whisker = _stroke(0.8, alpha: 0.5);
for (var i = 0; i < 3; i++) {
final y = 14.0 + i * 1.5;
final bend = (i - 1) * 1.5;
canvas.drawLine(Offset(9, y), Offset(2, y + bend), whisker);
canvas.drawLine(Offset(23, y), Offset(30, y + bend), whisker);
}
}
// ============================================================
// 狗狗造型
// ============================================================
void _paintDog(Canvas canvas) {
final fill = _fill();
final subFill = _subFill();
final stroke = _stroke(1.2);
switch (type) {
case TabSpriteType.home:
_paintDogHome(canvas, fill, subFill, stroke);
case TabSpriteType.discover:
_paintDogDiscover(canvas, fill, subFill, stroke);
case TabSpriteType.profile:
_paintDogProfile(canvas, fill, subFill, stroke);
}
}
void _paintDogHome(Canvas canvas, Paint fill, Paint subFill, Paint stroke) {
final roof = Path();
roof.moveTo(16, 3);
roof.cubicTo(14.5, 5, 4, 10, 4, 12);
roof.lineTo(28, 12);
roof.cubicTo(28, 10, 17.5, 5, 16, 3);
roof.close();
canvas.drawPath(roof, fill);
final body = Path();
body.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(5, 12, 22, 14),
topLeft: const Radius.circular(2),
topRight: const Radius.circular(2),
bottomLeft: const Radius.circular(3),
bottomRight: const Radius.circular(3),
),
);
canvas.drawPath(body, fill);
final door = Path();
door.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(13, 19, 6, 7),
topLeft: const Radius.circular(1),
topRight: const Radius.circular(1),
bottomLeft: const Radius.circular(3),
bottomRight: const Radius.circular(3),
),
);
canvas.drawPath(door, subFill);
canvas.drawCircle(const Offset(17.5, 23), 0.6, _fill(alpha: 0.5));
_paintDogEars(canvas, fill, subFill);
_paintFace(canvas, 16, 14);
if (isSelected) {
_paintDogTongue(canvas);
_paintChimneyHeart(canvas);
}
}
void _paintDogDiscover(
Canvas canvas,
Paint fill,
Paint subFill,
Paint stroke,
) {
canvas.drawCircle(const Offset(16, 16), 11, fill);
final innerRing = _stroke(1.2, alpha: 0.25);
canvas.drawCircle(const Offset(16, 16), 7.5, innerRing);
final needle = Path();
needle.moveTo(16, 7);
needle.cubicTo(15.2, 10, 14.8, 13, 16, 16);
needle.cubicTo(17.2, 13, 16.8, 10, 16, 7);
needle.close();
canvas.drawPath(needle, fill);
final needle2 = Path();
needle2.moveTo(16, 25);
needle2.cubicTo(15.2, 22, 14.8, 19, 16, 16);
needle2.cubicTo(17.2, 19, 16.8, 22, 16, 25);
needle2.close();
canvas.drawPath(needle2, _fill(alpha: 0.35));
canvas.drawCircle(const Offset(16, 16), 1.5, _fill(alpha: 0.5));
_paintDogEars(canvas, fill, subFill);
_paintFace(canvas, 16, 16);
if (isSelected) {
_paintDogTongue(canvas);
_paintCompassSparks(canvas);
}
}
void _paintDogProfile(
Canvas canvas,
Paint fill,
Paint subFill,
Paint stroke,
) {
final head = Path();
head.addOval(const Rect.fromLTWH(8, 4, 16, 14));
canvas.drawPath(head, fill);
final body = Path();
body.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(7, 17, 18, 10),
topLeft: const Radius.circular(6),
topRight: const Radius.circular(6),
bottomLeft: const Radius.circular(4),
bottomRight: const Radius.circular(4),
),
);
canvas.drawPath(body, fill);
_paintDogEars(canvas, fill, subFill);
_paintFace(canvas, 16, 10);
if (isSelected) {
_paintDogTongue(canvas);
_paintHalo(canvas);
}
}
void _paintDogEars(Canvas canvas, Paint fill, Paint subFill) {
final leftEar = Path();
leftEar.moveTo(8, 8);
leftEar.cubicTo(5, 8, 3, 12, 4, 17);
leftEar.cubicTo(4.5, 19, 6, 19, 8, 17);
leftEar.cubicTo(9, 14, 9, 10, 8, 8);
leftEar.close();
canvas.drawPath(leftEar, fill);
final leftInner = Path();
leftInner.moveTo(7.5, 9.5);
leftInner.cubicTo(5.5, 10, 4.5, 13, 5, 16);
leftInner.cubicTo(5.5, 17, 6.5, 17, 7.5, 15.5);
leftInner.cubicTo(8, 13, 8, 11, 7.5, 9.5);
leftInner.close();
canvas.drawPath(leftInner, subFill);
final rightEar = Path();
rightEar.moveTo(24, 8);
rightEar.cubicTo(27, 8, 29, 12, 28, 17);
rightEar.cubicTo(27.5, 19, 26, 19, 24, 17);
rightEar.cubicTo(23, 14, 23, 10, 24, 8);
rightEar.close();
canvas.drawPath(rightEar, fill);
final rightInner = Path();
rightInner.moveTo(24.5, 9.5);
rightInner.cubicTo(26.5, 10, 27.5, 13, 27, 16);
rightInner.cubicTo(26.5, 17, 25.5, 17, 24.5, 15.5);
rightInner.cubicTo(24, 13, 24, 11, 24.5, 9.5);
rightInner.close();
canvas.drawPath(rightInner, subFill);
}
void _paintDogTongue(Canvas canvas) {
final tongue = Path();
tongue.moveTo(15, 18);
tongue.cubicTo(14.5, 20, 14.5, 22, 16, 23);
tongue.cubicTo(17.5, 22, 17.5, 20, 17, 18);
tongue.close();
canvas.drawPath(tongue, _fill(alpha: 0.5));
final tongueLine = _stroke(0.5, alpha: 0.3);
canvas.drawLine(const Offset(16, 18.5), const Offset(16, 22), tongueLine);
}
// ============================================================
// 男孩造型
// ============================================================
void _paintBoy(Canvas canvas) {
final fill = _fill();
final subFill = _subFill();
switch (type) {
case TabSpriteType.home:
_paintBoyHome(canvas, fill, subFill);
case TabSpriteType.discover:
_paintBoyDiscover(canvas, fill, subFill);
case TabSpriteType.profile:
_paintBoyProfile(canvas, fill, subFill);
}
}
void _paintBoyHome(Canvas canvas, Paint fill, Paint subFill) {
final roof = Path();
roof.moveTo(16, 3);
roof.cubicTo(14.5, 5, 4, 10, 4, 12);
roof.lineTo(28, 12);
roof.cubicTo(28, 10, 17.5, 5, 16, 3);
roof.close();
canvas.drawPath(roof, fill);
final body = Path();
body.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(5, 12, 22, 14),
topLeft: const Radius.circular(2),
topRight: const Radius.circular(2),
bottomLeft: const Radius.circular(3),
bottomRight: const Radius.circular(3),
),
);
canvas.drawPath(body, fill);
final door = Path();
door.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(13, 19, 6, 7),
topLeft: const Radius.circular(1),
topRight: const Radius.circular(1),
bottomLeft: const Radius.circular(3),
bottomRight: const Radius.circular(3),
),
);
canvas.drawPath(door, subFill);
canvas.drawCircle(const Offset(17.5, 23), 0.6, _fill(alpha: 0.5));
_paintBoyHair(canvas, fill);
_paintFace(canvas, 16, 14);
if (isSelected) {
_paintChimneyHeart(canvas);
}
}
void _paintBoyDiscover(Canvas canvas, Paint fill, Paint subFill) {
canvas.drawCircle(const Offset(16, 16), 11, fill);
final innerRing = _stroke(1.2, alpha: 0.25);
canvas.drawCircle(const Offset(16, 16), 7.5, innerRing);
final needle = Path();
needle.moveTo(16, 7);
needle.cubicTo(15.2, 10, 14.8, 13, 16, 16);
needle.cubicTo(17.2, 13, 16.8, 10, 16, 7);
needle.close();
canvas.drawPath(needle, fill);
final needle2 = Path();
needle2.moveTo(16, 25);
needle2.cubicTo(15.2, 22, 14.8, 19, 16, 16);
needle2.cubicTo(17.2, 19, 16.8, 22, 16, 25);
needle2.close();
canvas.drawPath(needle2, _fill(alpha: 0.35));
canvas.drawCircle(const Offset(16, 16), 1.5, _fill(alpha: 0.5));
_paintBoyHair(canvas, fill);
_paintFace(canvas, 16, 16);
if (isSelected) {
_paintCompassSparks(canvas);
}
}
void _paintBoyProfile(Canvas canvas, Paint fill, Paint subFill) {
final head = Path();
head.addOval(const Rect.fromLTWH(8, 4, 16, 14));
canvas.drawPath(head, fill);
final body = Path();
body.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(7, 17, 18, 10),
topLeft: const Radius.circular(6),
topRight: const Radius.circular(6),
bottomLeft: const Radius.circular(4),
bottomRight: const Radius.circular(4),
),
);
canvas.drawPath(body, fill);
_paintBoyHair(canvas, fill);
_paintFace(canvas, 16, 10);
if (isSelected) {
_paintHalo(canvas);
}
}
void _paintBoyHair(Canvas canvas, Paint fill) {
final hair = Path();
hair.moveTo(8, 9);
hair.cubicTo(8, 3, 24, 3, 24, 9);
hair.cubicTo(22, 6, 20, 5, 16, 5.5);
hair.cubicTo(12, 5, 10, 6, 8, 9);
hair.close();
canvas.drawPath(hair, _fill(alpha: 0.65));
final bangs = Path();
bangs.moveTo(10, 8);
bangs.cubicTo(11, 5, 14, 4.5, 15, 6);
bangs.cubicTo(15.5, 7, 14, 8, 13, 8.5);
bangs.lineTo(10, 8);
bangs.close();
canvas.drawPath(bangs, _fill(alpha: 0.45));
}
// ============================================================
// 女孩造型
// ============================================================
void _paintGirl(Canvas canvas) {
final fill = _fill();
final subFill = _subFill();
switch (type) {
case TabSpriteType.home:
_paintGirlHome(canvas, fill, subFill);
case TabSpriteType.discover:
_paintGirlDiscover(canvas, fill, subFill);
case TabSpriteType.profile:
_paintGirlProfile(canvas, fill, subFill);
}
}
void _paintGirlHome(Canvas canvas, Paint fill, Paint subFill) {
final roof = Path();
roof.moveTo(16, 3);
roof.cubicTo(14.5, 5, 4, 10, 4, 12);
roof.lineTo(28, 12);
roof.cubicTo(28, 10, 17.5, 5, 16, 3);
roof.close();
canvas.drawPath(roof, fill);
final body = Path();
body.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(5, 12, 22, 14),
topLeft: const Radius.circular(2),
topRight: const Radius.circular(2),
bottomLeft: const Radius.circular(3),
bottomRight: const Radius.circular(3),
),
);
canvas.drawPath(body, fill);
final door = Path();
door.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(13, 19, 6, 7),
topLeft: const Radius.circular(1),
topRight: const Radius.circular(1),
bottomLeft: const Radius.circular(3),
bottomRight: const Radius.circular(3),
),
);
canvas.drawPath(door, subFill);
canvas.drawCircle(const Offset(17.5, 23), 0.6, _fill(alpha: 0.5));
_paintGirlHair(canvas, fill);
_paintFace(canvas, 16, 14);
if (isSelected) {
_paintChimneyHeart(canvas);
}
}
void _paintGirlDiscover(Canvas canvas, Paint fill, Paint subFill) {
canvas.drawCircle(const Offset(16, 16), 11, fill);
final innerRing = _stroke(1.2, alpha: 0.25);
canvas.drawCircle(const Offset(16, 16), 7.5, innerRing);
final needle = Path();
needle.moveTo(16, 7);
needle.cubicTo(15.2, 10, 14.8, 13, 16, 16);
needle.cubicTo(17.2, 13, 16.8, 10, 16, 7);
needle.close();
canvas.drawPath(needle, fill);
final needle2 = Path();
needle2.moveTo(16, 25);
needle2.cubicTo(15.2, 22, 14.8, 19, 16, 16);
needle2.cubicTo(17.2, 19, 16.8, 22, 16, 25);
needle2.close();
canvas.drawPath(needle2, _fill(alpha: 0.35));
canvas.drawCircle(const Offset(16, 16), 1.5, _fill(alpha: 0.5));
_paintGirlHair(canvas, fill);
_paintFace(canvas, 16, 16);
if (isSelected) {
_paintCompassSparks(canvas);
}
}
void _paintGirlProfile(Canvas canvas, Paint fill, Paint subFill) {
final head = Path();
head.addOval(const Rect.fromLTWH(8, 4, 16, 14));
canvas.drawPath(head, fill);
final body = Path();
body.addRRect(
RRect.fromRectAndCorners(
const Rect.fromLTWH(7, 17, 18, 10),
topLeft: const Radius.circular(6),
topRight: const Radius.circular(6),
bottomLeft: const Radius.circular(4),
bottomRight: const Radius.circular(4),
),
);
canvas.drawPath(body, fill);
_paintGirlHair(canvas, fill);
_paintFace(canvas, 16, 10);
if (isSelected) {
_paintHalo(canvas);
}
}
void _paintGirlHair(Canvas canvas, Paint fill) {
final hair = Path();
hair.moveTo(7, 10);
hair.cubicTo(6, 3, 26, 3, 25, 10);
hair.cubicTo(24, 6, 20, 4, 16, 4.5);
hair.cubicTo(12, 4, 8, 6, 7, 10);
hair.close();
canvas.drawPath(hair, _fill(alpha: 0.65));
final leftSide = Path();
leftSide.moveTo(7, 10);
leftSide.cubicTo(6, 14, 5.5, 20, 6, 25);
leftSide.cubicTo(6.5, 26, 8, 25.5, 8.5, 24);
leftSide.cubicTo(8.5, 18, 8, 13, 8, 10);
leftSide.close();
canvas.drawPath(leftSide, _fill(alpha: 0.5));
final rightSide = Path();
rightSide.moveTo(25, 10);
rightSide.cubicTo(26, 14, 26.5, 20, 26, 25);
rightSide.cubicTo(25.5, 26, 24, 25.5, 23.5, 24);
rightSide.cubicTo(23.5, 18, 24, 13, 24, 10);
rightSide.close();
canvas.drawPath(rightSide, _fill(alpha: 0.5));
final bangs = Path();
bangs.moveTo(9, 9);
bangs.cubicTo(10, 5, 14, 4, 16, 5);
bangs.cubicTo(18, 4, 22, 5, 23, 9);
bangs.cubicTo(21, 7, 18, 6, 16, 7);
bangs.cubicTo(14, 6, 11, 7, 9, 9);
bangs.close();
canvas.drawPath(bangs, _fill(alpha: 0.4));
}
// ============================================================
// 共享表情系统
// ============================================================
void _paintFace(Canvas canvas, double cx, double cy) {
_paintEyes(canvas, cx, cy);
_paintMouth(canvas, cx, cy);
_paintBlush(canvas, cx, cy);
}
void _paintEyes(Canvas canvas, double cx, double cy) {
final eyeY = cy - 1;
final lookDx = lookOffset * 1.2;
final leftX = cx - 3.5 + lookDx;
final rightX = cx + 3.5 + lookDx;
if (expressionProgress > 0.5) {
final openR = 1.2 * (1.0 + (eyeScale - 1.0) * expressionProgress * 0.5);
final eyeFill = _fill();
canvas.drawOval(
Rect.fromCenter(
center: Offset(leftX, eyeY),
width: openR * 2,
height: openR * 2.2,
),
eyeFill,
);
canvas.drawOval(
Rect.fromCenter(
center: Offset(rightX, eyeY),
width: openR * 2,
height: openR * 2.2,
),
eyeFill,
);
final highlight = Paint()
..color = const Color(0xFFFFFFFF).withValues(alpha: 0.7)
..style = PaintingStyle.fill;
canvas.drawCircle(
Offset(leftX + openR * 0.3, eyeY - openR * 0.4),
openR * 0.35,
highlight,
);
canvas.drawCircle(
Offset(rightX + openR * 0.3, eyeY - openR * 0.4),
openR * 0.35,
highlight,
);
final smallHighlight = Paint()
..color = const Color(0xFFFFFFFF).withValues(alpha: 0.4)
..style = PaintingStyle.fill;
canvas.drawCircle(
Offset(leftX - openR * 0.2, eyeY + openR * 0.3),
openR * 0.18,
smallHighlight,
);
canvas.drawCircle(
Offset(rightX - openR * 0.2, eyeY + openR * 0.3),
openR * 0.18,
smallHighlight,
);
} else {
final closedStroke = _stroke(1.2);
final closeProgress = 1.0 - expressionProgress * 2.0;
final eyeW = 2.0 * closeProgress;
final leftPath = Path();
leftPath.moveTo(leftX - eyeW, eyeY);
leftPath.cubicTo(
leftX - eyeW * 0.5,
eyeY + 0.5,
leftX + eyeW * 0.5,
eyeY + 0.5,
leftX + eyeW,
eyeY,
);
canvas.drawPath(leftPath, closedStroke);
final rightPath = Path();
rightPath.moveTo(rightX - eyeW, eyeY);
rightPath.cubicTo(
rightX - eyeW * 0.5,
eyeY + 0.5,
rightX + eyeW * 0.5,
eyeY + 0.5,
rightX + eyeW,
eyeY,
);
canvas.drawPath(rightPath, closedStroke);
}
}
void _paintMouth(Canvas canvas, double cx, double cy) {
final mouthY = cy + 2.5;
final mouthW = 2.0 + mouthCurve * expressionProgress * 1.0;
final curveH = 1.5 * mouthCurve * expressionProgress;
if (expressionProgress < 0.1) {
final neutralStroke = _stroke(1.0, alpha: 0.6);
canvas.drawLine(
Offset(cx - 1.5, mouthY),
Offset(cx + 1.5, mouthY),
neutralStroke,
);
} else if (expressionProgress > 0.7 && mouthCurve > 0.5) {
final mouthPath = Path();
mouthPath.moveTo(cx - mouthW, mouthY);
mouthPath.cubicTo(
cx - mouthW * 0.5,
mouthY + curveH * 1.8,
cx + mouthW * 0.5,
mouthY + curveH * 1.8,
cx + mouthW,
mouthY,
);
mouthPath.lineTo(cx - mouthW, mouthY);
mouthPath.close();
canvas.drawPath(mouthPath, _fill(alpha: 0.35));
final mouthStroke = Path();
mouthStroke.moveTo(cx - mouthW, mouthY);
mouthStroke.cubicTo(
cx - mouthW * 0.5,
mouthY + curveH * 1.8,
cx + mouthW * 0.5,
mouthY + curveH * 1.8,
cx + mouthW,
mouthY,
);
canvas.drawPath(mouthStroke, _stroke(1.0));
} else {
final mouthPath = Path();
mouthPath.moveTo(cx - mouthW, mouthY);
mouthPath.cubicTo(
cx - mouthW * 0.3,
mouthY + curveH,
cx + mouthW * 0.3,
mouthY + curveH,
cx + mouthW,
mouthY,
);
canvas.drawPath(mouthPath, _stroke(1.0));
}
}
void _paintBlush(Canvas canvas, double cx, double cy) {
if (expressionProgress < 0.6 || !isSelected) return;
final blushAlpha = (expressionProgress - 0.6) * 2.5 * 0.2;
final blushPaint = Paint()
..color = color.withValues(alpha: blushAlpha.clamp(0.0, 0.2))
..style = PaintingStyle.fill
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1.5);
canvas.drawOval(
Rect.fromCenter(
center: Offset(cx - 5.5, cy + 1.5),
width: 3.5,
height: 2,
),
blushPaint,
);
canvas.drawOval(
Rect.fromCenter(
center: Offset(cx + 5.5, cy + 1.5),
width: 3.5,
height: 2,
),
blushPaint,
);
}
// ============================================================
// 选中特效
// ============================================================
void _paintChimneyHeart(Canvas canvas) {
final heartPaint = _fill(alpha: 0.55);
const hx = 22.0, hy = 5.0, hs = 2.0;
final heart = Path();
heart.moveTo(hx, hy + hs * 0.3);
heart.cubicTo(
hx - hs,
hy - hs * 0.3,
hx - hs * 0.5,
hy - hs,
hx,
hy - hs * 0.4,
);
heart.cubicTo(
hx + hs * 0.5,
hy - hs,
hx + hs,
hy - hs * 0.3,
hx,
hy + hs * 0.3,
);
canvas.drawPath(heart, heartPaint);
}
void _paintCompassSparks(Canvas canvas) {
final sparkPaint = _fill(alpha: 0.55);
for (var i = 0; i < 4; i++) {
final angle = i * math.pi / 2 + 0.4;
final sx = 16 + 9.5 * math.cos(angle);
final sy = 16 + 9.5 * math.sin(angle);
final spark = Path();
spark.moveTo(sx, sy - 1);
spark.lineTo(sx + 0.3, sy);
spark.lineTo(sx, sy + 1);
spark.lineTo(sx - 0.3, sy);
spark.close();
canvas.drawPath(spark, sparkPaint);
}
}
void _paintHalo(Canvas canvas) {
final haloPaint = Paint()
..color = color.withValues(alpha: 0.3)
..style = PaintingStyle.stroke
..strokeWidth = 1.2;
canvas.drawOval(
Rect.fromCenter(center: const Offset(16, 3.5), width: 8, height: 2.5),
haloPaint,
);
}
// ============================================================
// 画笔工具
// ============================================================
Paint _fill({double alpha = 1.0}) {
return Paint()
..color = color.withValues(alpha: alpha)
..style = PaintingStyle.fill
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..isAntiAlias = true;
}
Paint _subFill() {
return Paint()
..color = color.withValues(alpha: 0.3)
..style = PaintingStyle.fill
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..isAntiAlias = true;
}
Paint _stroke(double width, {double alpha = 1.0}) {
return Paint()
..color = color.withValues(alpha: alpha)
..style = PaintingStyle.stroke
..strokeWidth = width
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..isAntiAlias = true;
}
@override
bool shouldRepaint(_RefinedSpritePainter oldDelegate) {
return expressionProgress != oldDelegate.expressionProgress ||
lookOffset != oldDelegate.lookOffset ||
isSelected != oldDelegate.isSelected ||
characterId != oldDelegate.characterId ||
color != oldDelegate.color;
}
}