...
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart'; // 👈 [핵심] 이 import 문이 누락되었습니다.
|
||||
|
||||
/// [수정] "SBSPACE"를 한 줄로 그리는 IntroView
|
||||
///
|
||||
/// @param mainColor 뷰의 텍스트 색상 (S, B 강조)
|
||||
/// @param onAnimationFinished 뷰의 애니메이션이 완료될 때 호출될 콜백
|
||||
class IntroViewFlutter extends StatefulWidget {
|
||||
final Color mainColor;
|
||||
final VoidCallback onAnimationFinished;
|
||||
|
||||
const IntroViewFlutter({
|
||||
super.key,
|
||||
required this.mainColor,
|
||||
required this.onAnimationFinished,
|
||||
});
|
||||
|
||||
@override
|
||||
_IntroViewFlutterState createState() => _IntroViewFlutterState();
|
||||
}
|
||||
|
||||
class _IntroViewFlutterState extends State<IntroViewFlutter>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
late final Animation<int> _logoTextAnimation; // 0 -> 7
|
||||
late final Animation<int> _missionTextAnimation; // 0 -> 15
|
||||
|
||||
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; // 7 * 150ms = 1050ms
|
||||
const int missionDuration = _missionString.length * 100; // 15 * 100ms = 1500ms
|
||||
final int totalAnimationDuration = logoDuration + missionDuration; // 2550ms
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: Duration(milliseconds: totalAnimationDuration),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// 로고 타이핑 애니메이션 (0 -> 7)
|
||||
_logoTextAnimation = IntTween(begin: 0, end: _logoString.length).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(
|
||||
0.0,
|
||||
logoDuration / totalAnimationDuration,
|
||||
curve: Curves.linear,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 미션 타이핑 애니메이션 (0 -> 15)
|
||||
_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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [수정] RichText(TextSpan)를 사용해 그리는 CustomPainter
|
||||
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,
|
||||
});
|
||||
|
||||
/// 헬퍼: 현재 길이에 맞는 "SBSPACE" TextSpan을 생성
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
/// 헬퍼: TextSpan과 폰트 크기로 TextPainter를 생성 (레이아웃 포함)
|
||||
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) {
|
||||
// 1. 로고 폰트 크기 정의
|
||||
final double logoFontSize = size.shortestSide / 6.0;
|
||||
|
||||
// 2. 미션 폰트 크기를 화면 너비에 꽉 차게 동적 계산
|
||||
|
||||
// 2a. 임시 폰트 크기(100)로 TextPainter를 생성하여 원본 너비를 측정
|
||||
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();
|
||||
|
||||
// 2b. (화면 너비 * 0.9) / (임시 텍스트 너비) = 스케일 비율
|
||||
final double targetWidth = size.width * 0.9;
|
||||
final double scale = targetWidth / tpMissionTemp.width;
|
||||
|
||||
// 2c. 실제 폰트 크기 계산
|
||||
final double missionFontSize = tempMissionFontSize * scale;
|
||||
|
||||
// 3. 레이아웃 계산용 TextPainter (항상 전체 텍스트 기준)
|
||||
|
||||
// 3a. 로고
|
||||
final tpLogoFull = _createTextPainter(_buildLogoSpan(_logoString.length), logoFontSize);
|
||||
|
||||
// 3b. 미션 (계산된 missionFontSize 사용)
|
||||
final TextPainter tpMissionFull = TextPainter(
|
||||
text: TextSpan(
|
||||
text: _missionString,
|
||||
style: TextStyle(
|
||||
color: mainColor,
|
||||
fontSize: missionFontSize,
|
||||
fontFamily: fontFamily,
|
||||
fontWeight: FontWeight.bold
|
||||
)
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
|
||||
// 4. 전체 텍스트 블록의 세로 중앙 정렬 Y좌표 계산
|
||||
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;
|
||||
|
||||
// 5. 각 텍스트 라인의 가로 중앙 정렬 X좌표 계산
|
||||
final double startxLogo = (size.width - tpLogoFull.width) / 2.0;
|
||||
final double startxMission = (size.width - tpMissionFull.width) / 2.0;
|
||||
|
||||
// 6. 그리기용 TextPainter (애니메이션 적용된 길이 기준)
|
||||
|
||||
// 6a. 로고 그리기 (현재 길이: logoTextLength)
|
||||
final tpLogoSub = _createTextPainter(_buildLogoSpan(logoTextLength), logoFontSize);
|
||||
tpLogoSub.paint(canvas, Offset(startxLogo, startyLogo));
|
||||
|
||||
// 6b. 미션 그리기 (현재 길이: missionTextLength)
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user