playWith/apps/app/lib/intro/intro_view.dart
2025-11-25 16:34:13 +09:00

194 lines
6.1 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
/// "SBSPACE"를 한 줄로 그리는 IntroView
class IntroViewFlutter extends StatefulWidget {
final Color mainColor;
final VoidCallback onAnimationFinished;
const IntroViewFlutter({
super.key,
required this.mainColor,
required this.onAnimationFinished,
});
@override
State<IntroViewFlutter> createState() => _IntroViewFlutterState();
}
class _IntroViewFlutterState extends State<IntroViewFlutter>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<int> _logoTextAnimation;
late final Animation<int> _missionTextAnimation;
static const String _logoString = "SBSPACE";
static const String _missionString = "Simple is Best.";
// [참고] 폰트가 없으면 기본 폰트로 나옵니다. 에셋에 폰트 추가가 필요할 수 있습니다.
static const String _fontFamily = "Sdmisaeng";
@override
void initState() {
super.initState();
const int logoDuration = _logoString.length * 150;
const int missionDuration = _missionString.length * 100;
final int totalAnimationDuration = logoDuration + missionDuration;
_controller = AnimationController(
duration: Duration(milliseconds: totalAnimationDuration),
vsync: this,
);
_logoTextAnimation = IntTween(begin: 0, end: _logoString.length).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, logoDuration / totalAnimationDuration, curve: Curves.linear),
),
);
_missionTextAnimation = IntTween(begin: 0, end: _missionString.length).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(logoDuration / totalAnimationDuration, 1.0, curve: Curves.linear),
),
);
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
Future.delayed(const Duration(seconds: 1), () {
if (mounted) widget.onAnimationFinished();
});
}
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: _IntroPainter(
mainColor: widget.mainColor,
fontFamily: _fontFamily,
logoTextLength: _logoTextAnimation.value,
missionTextLength: _missionTextAnimation.value,
),
size: Size.infinite,
);
},
);
}
}
class _IntroPainter extends CustomPainter {
final Color mainColor;
final String fontFamily;
final int logoTextLength;
final int missionTextLength;
static const String _logoString = "SBSPACE";
static const String _missionString = "Simple is Best.";
_IntroPainter({
required this.mainColor,
required this.fontFamily,
required this.logoTextLength,
required this.missionTextLength,
});
TextSpan _buildLogoSpan(int length) {
final Color normalColor = mainColor.withOpacity(0.6);
final List<TextSpan> children = [];
if (length >= 1) children.add(TextSpan(text: "S", style: TextStyle(color: mainColor)));
if (length >= 2) children.add(TextSpan(text: "B", style: TextStyle(color: mainColor)));
if (length >= 3) {
final String spaceToDraw = _logoString.substring(2, length.clamp(2, _logoString.length));
children.add(TextSpan(text: spaceToDraw, style: TextStyle(color: normalColor)));
}
return TextSpan(
style: TextStyle(fontFamily: fontFamily, fontWeight: FontWeight.bold),
children: children,
);
}
TextPainter _createTextPainter(TextSpan textSpan, double fontSize) {
final style = textSpan.style!.copyWith(fontSize: fontSize);
final painter = TextPainter(
text: TextSpan(children: textSpan.children, style: style),
textDirection: TextDirection.ltr,
);
painter.layout();
return painter;
}
@override
void paint(Canvas canvas, Size size) {
final double logoFontSize = size.shortestSide / 6.0;
const double tempMissionFontSize = 100.0;
final TextPainter tpMissionTemp = TextPainter(
text: TextSpan(
text: _missionString,
style: TextStyle(fontFamily: fontFamily, fontWeight: FontWeight.bold, fontSize: tempMissionFontSize)
),
textDirection: TextDirection.ltr,
)..layout();
final double targetWidth = size.width * 0.9;
final double scale = targetWidth / tpMissionTemp.width;
final double missionFontSize = tempMissionFontSize * scale;
final tpLogoFull = _createTextPainter(_buildLogoSpan(_logoString.length), logoFontSize);
final TextPainter tpMissionFull = TextPainter(
text: TextSpan(
text: _missionString,
style: TextStyle(
color: mainColor,
fontSize: missionFontSize,
fontFamily: fontFamily,
fontWeight: FontWeight.bold
)
),
textDirection: TextDirection.ltr,
)..layout();
final double padding = logoFontSize * 0.1;
final double totalHeight = tpLogoFull.height + padding + tpMissionFull.height;
final double startyLogo = (size.height - totalHeight) / 2.0;
final double startyMission = startyLogo + tpLogoFull.height + padding;
final double startxLogo = (size.width - tpLogoFull.width) / 2.0;
final double startxMission = (size.width - tpMissionFull.width) / 2.0;
final tpLogoSub = _createTextPainter(_buildLogoSpan(logoTextLength), logoFontSize);
tpLogoSub.paint(canvas, Offset(startxLogo, startyLogo));
final String missionToDraw = _missionString.substring(0, missionTextLength);
final tpMissionSub = TextPainter(
text: TextSpan(text: missionToDraw, style: tpMissionFull.text!.style),
textDirection: TextDirection.ltr,
)..layout();
tpMissionSub.paint(canvas, Offset(startxMission, startyMission));
}
@override
bool shouldRepaint(covariant _IntroPainter oldDelegate) {
return oldDelegate.mainColor != mainColor ||
oldDelegate.logoTextLength != logoTextLength ||
oldDelegate.missionTextLength != missionTextLength;
}
}