전체 코드
import 'package:flutter/material.dart';
import 'package:tiktok_clone/constants/gaps.dart';
import 'package:tiktok_clone/constants/sizes.dart';
const interests = [
"Daily Life",
"Comedy",
"Entertainment",
"Animals",
"Food",
"Beauty & Style",
"Drama",
"Learning",
"Talent",
"Sports",
"Auto",
"Family",
"Fitness & Health",
"DIY & Life Hacks",
"Arts & Crafts",
"Dance",
"Outdoors",
"Oddly Satisfying",
"Home & Garden",
"Daily Life",
"Comedy",
"Entertainment",
"Animals",
"Food",
"Beauty & Style",
"Drama",
"Learning",
"Talent",
"Sports",
"Auto",
"Family",
"Fitness & Health",
"DIY & Life Hacks",
"Arts & Crafts",
"Dance",
"Outdoors",
"Oddly Satisfying",
"Home & Garden",
];
class InterestsScreen extends StatelessWidget {
const InterestsScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Choose your interests'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(
left: Sizes.size24,
right: Sizes.size24,
bottom: Sizes.size16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gaps.v32,
const Text(
'Choose your interests',
style: TextStyle(
fontSize: Sizes.size40,
fontWeight: FontWeight.bold,
),
),
Gaps.v20,
const Text(
'Get better video recommendations',
style: TextStyle(
fontSize: Sizes.size20,
),
),
Gaps.v64,
Wrap(
runSpacing: 15,
spacing: 20,
children: [
for (var interest in interests)
Container(
padding: const EdgeInsets.symmetric(
vertical: Sizes.size16,
horizontal: Sizes.size24,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(Sizes.size32),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
spreadRadius: 5,
),
],
border:
Border.all(color: Colors.black.withOpacity(0.1))),
child: Text(
interest,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
)
],
)
],
),
),
),
bottomNavigationBar: BottomAppBar(
elevation: 2,
height: 100,
padding: const EdgeInsets.only(
bottom: Sizes.size40,
top: Sizes.size16,
left: Sizes.size24,
right: Sizes.size24,
),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: Sizes.size10,
horizontal: Sizes.size24,
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: const Text(
'Next',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: Sizes.size16,
),
),
),
),
);
}
}
다음과 같이 요소들이 다음 화면으로 넘어갈 때 자동으로 줄바꿈을 하게 만들고 싶으면 Wrap
Widget을 사용하면 된다.
안드로이드 애뮬레이터 환경에서 bottomAppBar의 padding을 잘못 조정하면 안의 글씨가 안 보이는 현상이 생긴다.
내 추측이지만 appBar
widget 자체의 높이는 한정적이기 때문에 container의 크기도 한정적인데 무리하게 Container
widget의 padding
을 늘리면 생기는 문제인 것 같다.
지금까지 Material의 Container, Padding과 같은 앱을 사용하는 것이 귀찮다면 Cupertino Button을 사용해보자.
보다시피 우리가 디자인 한 것과 유사하지만 코드량은 훨씬 줄어든다.
bottomNavigationBar: BottomAppBar(
elevation: 2,
height: 100,
padding: const EdgeInsets.only(
bottom: Sizes.size40,
top: Sizes.size16,
left: Sizes.size24,
right: Sizes.size24,
),
child: CupertinoButton(
child: Text('Next'),
color: Theme.of(context).primaryColor,
onChanged: () {},
)
),
)
scrollbar
...
class InterestsScreen extends StatefulWidget {
const InterestsScreen({super.key});
State<InterestsScreen> createState() => _InterestsScreenState();
}
class _InterestsScreenState extends State<InterestsScreen> {
final ScrollController _scrollController = ScrollController();
bool _showTitle = false;
void _onScroll() {
if (_scrollController.offset > 200) {
if (_showTitle) return;
setState(() {
_showTitle = true;
});
} else {
setState(() {
_showTitle = false;
});
}
}
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void dispose() {
_scrollController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: AnimatedOpacity(
opacity: _showTitle ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: const Text(
'Choose your interests',
),
),
),
body: Scrollbar(
controller: _scrollController,
child: SingleChildScrollView(
controller: _scrollController,
child: Padding(
padding: const EdgeInsets.only(
left: Sizes.size24,
right: Sizes.size24,
bottom: Sizes.size16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gaps.v32,
const Text(
'Choose your interests',
style: TextStyle(
fontSize: Sizes.size40,
fontWeight: FontWeight.bold,
),
),
Gaps.v20,
const Text(
'Get better video recommendations',
style: TextStyle(
fontSize: Sizes.size20,
),
),
Gaps.v64,
Wrap(
runSpacing: 15,
spacing: 20,
children: [
for (var interest in interests)
InterestButton(interest: interest),
],
),
],
),
),
),
),
bottomNavigationBar: BottomAppBar(
elevation: 2,
height: 100,
padding: const EdgeInsets.only(
bottom: Sizes.size40,
top: Sizes.size16,
left: Sizes.size24,
right: Sizes.size24,
),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: Sizes.size10,
horizontal: Sizes.size24,
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: const Text(
'Next',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: Sizes.size16,
),
),
),
));
}
}
TabBarView
import 'package:flutter/material.dart';
import 'package:tiktok_clone/constants/gaps.dart';
import 'package:tiktok_clone/constants/sizes.dart';
class TutorialScreen extends StatefulWidget {
const TutorialScreen({super.key});
State<TutorialScreen> createState() => _TutorialScreenState();
}
class _TutorialScreenState extends State<TutorialScreen> {
Widget build(BuildContext context) {
return const DefaultTabController(
length: 3,
child: Scaffold(
bottomNavigationBar: BottomAppBar(
height: 100,
padding: EdgeInsets.symmetric(vertical: Sizes.size10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TabPageSelector(
color: Colors.white,
selectedColor: Colors.black38,
)
],
),
),
body: SafeArea(
child: TabBarView(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: Sizes.size24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gaps.v52,
Text(
'Watch cool videos',
style: TextStyle(
fontSize: Sizes.size40,
fontWeight: FontWeight.bold,
),
),
Gaps.v16,
Text(
'Videos are personalized for you based on what you watch, like, and share. ',
style: TextStyle(
fontSize: Sizes.size20,
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: Sizes.size24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gaps.v52,
Text(
'Follow the rules',
style: TextStyle(
fontSize: Sizes.size40,
fontWeight: FontWeight.bold,
),
),
Gaps.v16,
Text(
'Videos are personalized for you based on what you watch, like, and share. ',
style: TextStyle(
fontSize: Sizes.size20,
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: Sizes.size24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gaps.v52,
Text(
'Enjoy the ride.',
style: TextStyle(
fontSize: Sizes.size40,
fontWeight: FontWeight.bold,
),
),
Gaps.v16,
Text(
'Videos are personalized for you based on what you watch, like, and share. ',
style: TextStyle(
fontSize: Sizes.size20,
),
),
],
),
),
],
),
),
),
);
}
}
화면에서 양옆으로 스와이핑 하면서 넘어가는 화면을 구현하고 싶을 때 사용하는 위젯이 바로
TabBarView
다.
TabBarView
위젯은 컨트롤러를 필요로 하기 때문에 컨트롤러를 만들거나, DefaultTabController
위젯으로 감싸주면 된다.
그리고 TabPageSelector
위젯을 사용하면 현재 tab이 어디인지 시각적으로 확인할 수 있다.
하지만 우리는 이제부터 GestureDetector
와 AnimatedCrossFade
위젯을 갖고 tutorial page를 꾸며볼 것이다.
AnimatedCrossFade
전체 코드
enum Direction { left, right }
enum Page { first, second }
class TutorialScreen extends StatefulWidget {
const TutorialScreen({super.key});
State<TutorialScreen> createState() => _TutorialScreenState();
}
class _TutorialScreenState extends State<TutorialScreen> {
Direction _direction = Direction.right;
Page _showingPage = Page.first;
void _onPanUpdate(DragUpdateDetails details) {
if (details.delta.dx > 0) {
setState(() {
_direction = Direction.right;
});
} else {
setState(() {
_direction = Direction.left;
});
}
}
void _onPanEnd(DragEndDetails details) {
if (_direction == Direction.left) {
setState(() {
_showingPage = Page.second;
});
} else {
setState(() {
_showingPage = Page.first;
});
}
}
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: Scaffold(
bottomNavigationBar: BottomAppBar(
elevation: _showingPage == Page.first ? 0 : 1,
height: Sizes.size96,
padding: const EdgeInsets.symmetric(
horizontal: Sizes.size24,
vertical: Sizes.size24,
),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: _showingPage == Page.first ? 0 : 1,
child: CupertinoButton(
onPressed: () {},
color: Theme.of(context).primaryColor,
child: const Text('Enter the app!'),
),
)),
body: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Sizes.size24,
),
child: SafeArea(
child: AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: _showingPage == Page.first
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gaps.v80,
Text(
'Watch cool videos',
style: TextStyle(
fontSize: Sizes.size40,
fontWeight: FontWeight.bold,
),
),
Gaps.v16,
Text(
'Videos are personalized for you based on what you watch, like, and share. ',
style: TextStyle(
fontSize: Sizes.size20,
),
),
],
),
secondChild: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gaps.v80,
Text(
'Follow the rules',
style: TextStyle(
fontSize: Sizes.size40,
fontWeight: FontWeight.bold,
),
),
Gaps.v16,
Text(
'Take care of one another! Plis!',
style: TextStyle(
fontSize: Sizes.size20,
),
),
],
),
),
),
),
),
);
}
}
onPanUpdate
onPanEnd