와챠의 우당탕탕 코딩 일기장
[Flutter] OnBoarding Screen/온보딩 화면/introduction_screen custom 본문
코딩 일기장/Flutter
[Flutter] OnBoarding Screen/온보딩 화면/introduction_screen custom
minWachya 2023. 7. 13. 12:57반응형
flutter에서는 온보딩 화면을 간단하게 만들 수 있는 라이브러리를 제공해준다.
바로 이것.
https://pub.dev/packages/introduction_screen
기존에 제공해주는 기능은 다음과 같다.
이미지, 타이틀, 설명 텍스트, 스킵 버튼, 다음 버튼, 인디케이터 이것들의 위치가 고정이다.
그래서 아래와 같이 이미지, 타이틀, 설명을 지정해주면 나만의 온보딩을 간단하게 만들 수 있다.
PageViewModel(
title: "Title of introduction page",
body: "Welcome to the app! This is a description of how it works.",
image: const Center(
child: Icon(Icons.waving_hand, size: 50.0),
),
)
그런데 나는 위에서 보여드린 그림과 같이.. 이런 형태의 온보딩을 만들어야만 한다.ㅎㅎ
그런데 다행히도 이 라이브러리는 커스텀도 할 수 있어서 커스텀을 하여 만들어보기로 했다.
1. 의존성 추가
dependencies:
introduction_screen: ^3.1.10
2. 온보딩 화면이 보여졌는지 아닌지를 SharedPreference로 관리하려고 한다.
data/local/provider/local_pref_provider.dart생성 후 아래 코드 작성~
import 'package:shared_preferences/shared_preferences.dart';
class LocalPrefProvider {
late SharedPreferences prefs;
final showOnBoarding = "onboarding";
void setShowOnBoarding(bool bool) async {
prefs = await SharedPreferences.getInstance();
prefs.setBool(showOnBoarding, bool);
}
Future<bool> getShowOnBoarding() async {
prefs = await SharedPreferences.getInstance();
return prefs.getBool(showOnBoarding) ?? true;
}
}
3. main화면에 온보딩화면 본 적 없으면 보이도록 추가
Future<void> main() async {
// 비동기 메서드를 사용함
WidgetsFlutterBinding.ensureInitialized();
// 온보딩 보였는지 아닌지 변수 가져오기
bool showOnBoarding = await LocalPrefProvider().getShowOnBoarding();
runApp(MultiProvider(child: MyApp(showOnBoarding: showOnBoarding));
}
class MyApp extends StatelessWidget {
final bool showOnBoarding;
const MyApp({super.key, required this.showOnBoarding});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.system,
// 온보딩 보인 적 없으면 보이고, 보인 적 있으면 메인 화면 보여주기
home: showOnBoarding ? const OnBoardingScreen() : const MainScreen(),
);
}
}
4. 온보딩 화면 생성
내가 원하던 모습으로 커스텀한 코드는 다음과 같다.
(이미지는 더미 이미지를 넣었다. 앱 모두 개발한 후에 캡쳐된 화면을 넣을 예정이다.)
PageViewModel(
useScrollView: false, // 스크롤 없이 한 화면에 보이기
title: "", // 기본 타이틀 사용 안 함
bodyWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 제목 + 네온 효과
Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 24,
shadows: [
for (double i = 1; i < 7; i++)
Shadow(color: AppColor.purpleColor2, blurRadius: 3 * i)
],
),
),
const SizedBox(height: 10.0),
// 내용
SizedBox(
height: 60,
child: Text(
context,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
const SizedBox(height: 10.0),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 왼쪽 이동 버튼
IconButton(
onPressed: () {
_introKey.currentState?.previous();
},
icon: Visibility(
visible: isVisibleLeft,
child: SvgPicture.asset(
'assets/icons/ic_left_purple.svg',
),
),
),
const SizedBox(
width: 26.0,
),
// 이미지
Expanded(
child: Image.asset(
imgPath,
height: screenHeightSize * 0.5,
fit: BoxFit.fitHeight,
),
),
const SizedBox(
width: 26.0,
),
// 오른쪽 이동 버튼
IconButton(
onPressed: () {
_introKey.currentState?.next();
},
icon: Visibility(
visible: isVisibleRight,
child: SvgPicture.asset(
'assets/icons/ic_right_purple.svg',
),
),
),
],
)
],
),
);
globalHeader를 사용해 스킵 버튼을 오른쪽 상단에 두었다.
또한 state를 관리하며 마지막 페이지가 되면 스킵버튼 안 보이게 함..ㅎ
globalHeader:
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
Padding(
padding: const EdgeInsets.all(18.0),
child: TextButton(
onPressed: () {
_introKey.currentState?.skipToEnd();
},
child: Text(
_skipState ? "건너뛰기" : "",
style: const TextStyle(color: Colors.white, fontSize: 16),
textAlign: TextAlign.right,
),
),
)
]),
onChange: (value) {
if (value == 4) {
_skipState = false;
} else {
_skipState = true;
}
setState(() {});
},
전체 코드는 다음과 같다.
마지막 페이지에서 권한 요청을 했기 때문에 권한 요청 코드도 함께 있다.
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:introduction_screen/introduction_screen.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pocket_pose/config/app_color.dart';
import 'package:pocket_pose/data/local/provider/local_pref_provider.dart';
import 'package:pocket_pose/ui/screen/main_screen.dart';
class OnBoardingScreen extends StatefulWidget {
const OnBoardingScreen({super.key});
@override
State<OnBoardingScreen> createState() => _OnBoardingScreenState();
}
class _OnBoardingScreenState extends State<OnBoardingScreen> {
final _introKey = GlobalKey<IntroductionScreenState>();
late double screenHeightSize;
bool _skipState = true;
@override
Widget build(BuildContext context) {
screenHeightSize = MediaQuery.of(context).size.height;
return IntroductionScreen(
onChange: (value) {
if (value == 4) {
_skipState = false;
} else {
_skipState = true;
}
setState(() {});
},
globalBackgroundColor: Colors.black,
globalHeader:
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
Padding(
padding: const EdgeInsets.all(18.0),
child: TextButton(
onPressed: () {
_introKey.currentState?.skipToEnd();
},
child: Text(
_skipState ? "건너뛰기" : "",
style: const TextStyle(color: Colors.white, fontSize: 16),
textAlign: TextAlign.right,
),
),
)
]),
globalFooter: const SizedBox(
height: 30.0,
),
key: _introKey,
pages: [
getImagePageViewModel(
title: "대기",
context: "참여자가 3명 이상이어야\n‘PoPo 스테이지’를시작할 수 있어요.🔥",
imgPath: "assets/images/bg_popo_result.png",
isVisibleLeft: false),
getImagePageViewModel(
title: "캐치",
context:
"랜덤으로 챌린지 노래가 선정됩니다.\n선착순 3명만 참여 가능하니 캐치 버튼을 빨리 눌러 참여해봐요! 💪",
imgPath: "assets/images/bg_popo_result.png"),
getImagePageViewModel(
title: "플레이",
context: "노래에 맞춰 춤을 춰봐요.✨\n춤 동작 마다 점수가 표시됩니다.",
imgPath: "assets/images/bg_popo_result.png"),
getImagePageViewModel(
title: "결과",
context:
"최고의 평가를 받은 MVP가 선정 됩니다. 🥳🎉\nMVP는 5초간 모두의 앞에서 세레머니를 할 기회가 주어집니다.",
imgPath: "assets/images/bg_popo_result.png"),
getSvgPageViewModel(
title: "시작하기",
context: "자 그럼 지금부터\n포포와 함께 춤 짱이 되러 가볼까요?😝",
imgPath: "assets/images/bg_popo_result.png"),
],
onDone: () => goHomepage(),
showDoneButton: false,
showNextButton: false,
showSkipButton: false,
skip: const Text(
'Skip',
style: TextStyle(color: Colors.white),
),
next: const Icon(
Icons.arrow_forward,
color: Colors.white,
),
done: const Text(
'Done',
style: TextStyle(color: Colors.white),
),
dotsDecorator: DotsDecorator(
size: const Size(10.0, 10.0),
color: Colors.white,
activeSize: const Size(10.0, 10.0),
activeColor: AppColor.purpleColor,
activeShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(25.0)),
),
),
);
}
PageViewModel getImagePageViewModel(
{required String title,
required context,
required imgPath,
bool isVisibleLeft = true,
bool isVisibleRight = true,
bool isVisibleSkip = true}) {
return PageViewModel(
useScrollView: false,
title: "",
bodyWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 24,
shadows: [
for (double i = 1; i < 7; i++)
Shadow(color: AppColor.purpleColor2, blurRadius: 3 * i)
],
),
),
const SizedBox(height: 10.0),
SizedBox(
height: 60,
child: Text(
context,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
const SizedBox(height: 10.0),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
_introKey.currentState?.previous();
},
icon: Visibility(
visible: isVisibleLeft,
child: SvgPicture.asset(
'assets/icons/ic_left_purple.svg',
),
),
),
const SizedBox(
width: 26.0,
),
Expanded(
child: Image.asset(
imgPath,
height: screenHeightSize * 0.5,
fit: BoxFit.fitHeight,
),
),
const SizedBox(
width: 26.0,
),
IconButton(
onPressed: () {
_introKey.currentState?.next();
},
icon: Visibility(
visible: isVisibleRight,
child: SvgPicture.asset(
'assets/icons/ic_right_purple.svg',
),
),
),
],
)
],
),
);
}
PageViewModel getSvgPageViewModel(
{required String title, required context, required imgPath}) {
return PageViewModel(
useScrollView: false,
title: "",
bodyWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 24,
shadows: [
for (double i = 1; i < 7; i++)
Shadow(color: AppColor.purpleColor2, blurRadius: 3 * i)
],
),
),
const SizedBox(height: 10.0),
SizedBox(
height: 60,
child: Text(
context,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
const SizedBox(height: 10.0),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
_introKey.currentState?.previous();
},
icon: SvgPicture.asset(
'assets/icons/ic_left_purple.svg',
),
),
Flexible(
child: Column(
children: [
SvgPicture.asset(
'assets/images/charactor_on_boarding.svg',
fit: BoxFit.contain,
height: screenHeightSize * 0.4,
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
boxShadow: [
for (double i = 1; i < 4; i++)
BoxShadow(
color: AppColor.blueColor,
blurStyle: BlurStyle.outer,
blurRadius: 3 * i)
]),
child: TextButton(
style: TextButton.styleFrom(
side: const BorderSide(
color: Colors.white, width: 2.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30)),
),
onPressed: () {
permission();
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"PoPo 입장",
style:
TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
],
),
),
IconButton(
onPressed: () {
_introKey.currentState?.next();
},
icon: Visibility(
visible: false,
child: SvgPicture.asset(
'assets/icons/ic_right_purple.svg',
),
),
),
],
)
],
));
}
void goHomepage() async {
LocalPrefProvider().setShowOnBoarding(false);
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MainScreen()),
);
}
Future<bool> permission() async {
await [Permission.camera, Permission.storage].request();
if (await Permission.camera.isGranted &&
await Permission.storage.isGranted) {
goHomepage();
return Future.value(true);
} else {
Fluttertoast.showToast(msg: '포포 스테이지를 즐기기 위해 권한을 설정해주세요.');
return Future.value(false);
}
}
}
완성 화면은 다음과 같다.
참고
반응형
'코딩 일기장 > Flutter' 카테고리의 다른 글
[Flutter] Base Response 사용하여 Response 받기 + build_runner(중복 코드 줄이기) (0) | 2023.07.27 |
---|---|
[Flutter] Camera/Gallery/Preview/Select Video/카메라/갤러리/미리보기/동영상 선택 (0) | 2023.07.17 |
[Flutter] Permission Request/권한 요청 (0) | 2023.07.13 |
[Flutter] Splash + 로고 잘림 현상 해결 (0) | 2023.07.12 |
[Flutter] Camera에서 실시간으로 이미지 받아오기 (0) | 2023.05.28 |
Comments