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 createState() => _IntroViewFlutterState(); } class _IntroViewFlutterState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _logoTextAnimation; late final Animation _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 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; } }