194 lines
6.1 KiB
Dart
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;
|
||
|
|
}
|
||
|
|
}
|