Source code : https://github.com/FreeBono/youtube-music
플러터로 YOUTUBE MUSIC 홈 화면을 구현해 보았다. 완전히 일치하게 만들지는 않았고, 기본 UI틀을 지키면서 비슷한 느낌을 낼 수 있도록 하였다.
Youtube Music의 홈화면은 ListView의 사용이 많을 것이라 생각되었다. 그래서 ListView를 보다 쉽고 효과적이게 구현할 수 있는 SliverList를 사용하는 것을 계획했다.
appBar부터 bottomNavigationBar까지 어떤 식으로 코드를 작성했는지 살펴보겠다.
appBar 구현
홈 화면 최상단에 유튜브 뮤직로고와 함께 서치 아이콘, 프로필 아이콘이 있는 appBar를 확인할 수 있다. 그리고 그 아래 음악 장르를 나누는 카테고리를 확인할 수 있는데, 실제 유튜브 뮤직앱을 보면 앱을 스크롤하여 내렸을 때, 최상단의 앱바는 사라지고 카테고리가 pin되는 것을 확인할 수 있다. 나는 이것을 구현하기 위해 최상단 로고 부분과 카테고리 부분 둘 다 SliverAppBar로 구현을 하였다. 처음에는 하나의 SliverAppBar에서 ExpandedHeight속성을 통해서 구현하려 했지만 내가 원하는 형태가 제대로 나오지 않아 AppBar를 두개 사용하였다.
import 'package:flutter/material.dart';
class CustomAppBar extends StatelessWidget {
const CustomAppBar();
@override
Widget build(BuildContext context) {
return SliverAppBar(
pinned: false,
backgroundColor: Colors.transparent,
title: Row(
children: [
Container(
// color: Colors.red,
width: 120,
height: 30,
child: Image.asset('assets/youtube-icon.png', fit: BoxFit.cover),
),
Spacer(),
Padding(
padding: const EdgeInsets.only(right: 15),
child: Icon(
Icons.search,
size: 30,
),
),
Icon(
Icons.account_circle,
size: 30,
)
],
),
);
}
}
- pinned는 false로 하여 앱이 스크롤되어 내려갔을 때 사라지도록 하였다.
import 'package:flutter/material.dart';
import 'package:youtube_music/constants.dart';
import 'package:youtube_music/model/home.dart';
class HomeCategory extends StatelessWidget {
final bool isSliverAppBarVisible;
const HomeCategory({Key? key, required this.isSliverAppBarVisible})
: super(key: key);
@override
Widget build(BuildContext context) {
print(isSliverAppBarVisible);
return SliverAppBar(
pinned: true,
toolbarHeight: 60,
backgroundColor:
!isSliverAppBarVisible ? Colors.transparent : Colors.black,
elevation: 0,
title: SizedBox(
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: category.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(right: 15.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.grey.withOpacity(0.3),
),
// height: 30,
child: Padding(
padding: homePadding,
child: Center(
child: Text(
category[index],
style: TextStyle(fontWeight: FontWeight.bold),
)),
),
),
);
},
),
));
}
}
- pinned를 true로 하여 앱이 스크롤되어 내려가도, 앱바를 화면 상단에 고정할 수 있도록 하였다.
- isSliverAppBarVisible은 부모 위젯에서 전달받은 값으로, 스크롤되어 앱바가 보이지 않았을 때, 배경색을 바꿔주기 위해 사용하였다.
import 'package:flutter/material.dart';
import 'package:youtube_music/screens/common/custom_appbar.dart';
import 'package:youtube_music/screens/common/custom_bottom_navigationbar.dart';
import 'package:youtube_music/screens/home/components/home_category.dart';
import 'package:youtube_music/screens/home/components/home_explore.dart';
import 'package:youtube_music/screens/home/components/home_playlist.dart';
import 'package:youtube_music/screens/home/components/home_recommand_list.dart';
import 'package:youtube_music/screens/home/components/home_singer_theme.dart';
class HomeScreen extends StatefulWidget {
HomeScreen();
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final ScrollController _scrollController = ScrollController();
bool _isSliverAppBarVisible = true;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
// Calculate the position of SliverAppBar based on scroll offset
setState(() {
_isSliverAppBarVisible = _scrollController.hasClients &&
_scrollController.offset >
kToolbarHeight; // Adjust this value as needed
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
body: SafeArea(
child: Container(
decoration: BackgroundBox(),
child: CustomScrollView(
controller: _scrollController,
slivers: [
CustomAppBar(),
HomeCategory(isSliverAppBarVisible: _isSliverAppBarVisible),
SliverList(
delegate: SliverChildListDelegate([
HomePlaylist(),
HomeSingerTheme(),
HomeRecommendList(),
HomeExplore(),
]),
),
],
),
),
),
bottomNavigationBar: CustomBottomNavigationBar());
}
BoxDecoration BackgroundBox() {
return !_isSliverAppBarVisible
? BoxDecoration(
gradient: LinearGradient(
begin: FractionalOffset(0.3, -0.9), //,
end: FractionalOffset(0.4, 0.32), //Alignment.topCenter,
colors: [
Colors.orange,
Colors.black,
// Colors.orange,
],
// stops: [0.1, 0.5, 0.5],
tileMode: TileMode.clamp),
)
: BoxDecoration(color: Colors.black);
}
}
- 리스트뷰의 scrollDirection을 Axis.horizontal로 하여 가로 방향으로 스크롤이 가능하도록 하였다.
playList 구현
PlayList 부분은 하나의 페이지 단위로 화면에 보여주기 때문에 ListView가 아닌 PageView를 사용하였다.
import 'package:flutter/material.dart';
import 'package:youtube_music/constants.dart';
import 'package:youtube_music/model/home.dart';
import 'package:youtube_music/screens/home/components/home_playlist_card.dart';
class HomePlaylistBody extends StatelessWidget {
const HomePlaylistBody({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: homePadding,
child: SizedBox(
height: 300,
child: PageView.builder(
scrollDirection: Axis.horizontal,
itemCount: songs.length % 4 == 0
? songs.length ~/ 4
: songs.length ~/ 4 + 1,
itemBuilder: (context, index) {
List<Song> ls = songs.sublist(index * 4,
index * 4 + 4 <= songs.length ? index * 4 + 4 : songs.length);
return Column(
children: [
for (int i = 0; i < ls.length; i++) ...[
HomePlayListCard(
title: ls[i].title,
singer: ls[i].singer,
imageUrl: ls[i].imgUrl),
SizedBox(
height: 15,
)
]
],
);
},
),
));
}
}
- 한 페이지당 4개의 카드를 가지고 있어야 하기 때문에 위와 같이 itemCout와 itemBuilder에 연산식을 넣어 주었다.
- 아래 PageView를 return하는 컬럼을 확인하면 for () ... [ ] 형태의 식을 볼 수 있는데, 이와 같은 형태로 반복문 또는 조건문을 사용해줄 수 있다. ( 조건문일 경우 if () ... [] )
bottomNavgationBar 구현
bottomNavigationBar는 Scaffold의 bottomNavigationBar 속성에서 구현하였다. bottomNavigationBar는 기본적으로 flutter에서 제공되는 위젯이 훌륭하기 때문에 구현의 난이도가 굉장히 낮은 편에 속했다.
import 'package:flutter/material.dart';
class CustomBottomNavigationBar extends StatefulWidget {
const CustomBottomNavigationBar({Key? key}) : super(key: key);
@override
State<CustomBottomNavigationBar> createState() =>
_CustomBottomNavigationBarState();
}
class _CustomBottomNavigationBarState extends State<CustomBottomNavigationBar> {
int _selectedIndex = 0;
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: _selectedIndex == 0
? Icon(Icons.home_filled)
: Icon(Icons.home_outlined),
label: '홈',
),
BottomNavigationBarItem(
icon: _selectedIndex == 1
? Icon(Icons.play_arrow)
: Icon(Icons.play_arrow_outlined),
label: '샘플',
),
BottomNavigationBarItem(
icon: _selectedIndex == 2
? Icon(Icons.send_and_archive)
: Icon(Icons.send_and_archive_outlined),
label: '둘러보기',
),
BottomNavigationBarItem(
icon: _selectedIndex == 3
? Icon(Icons.library_music)
: Icon(Icons.library_music_outlined),
label: '보관하기',
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.white,
onTap: _onItemTapped,
type: BottomNavigationBarType.fixed,
backgroundColor: Color.fromARGB(255, 14, 14, 14),
showUnselectedLabels: true,
unselectedItemColor: Colors.white,
selectedLabelStyle: TextStyle(color: Colors.white),
);
}
}
- 현재 탭일 경우 아이콘 변경 (outline -> filled)
- 대부분의 앱에서 BottomNavigationBar와 Device의 하단 system버튼들의 background 색상을 동일하게 맞춰주기 때문에 main에서 SystemChrome을 사용하여 Device 버튼 배경색을 변경해 주었다.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:youtube_music/constants.dart';
import 'package:youtube_music/screens/home/home_screen.dart';
void main() {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: Color.fromARGB(255, 14, 14, 14)));
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Youtube Music',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark().copyWith(
primaryColor: primaryColor,
scaffoldBackgroundColor: bgColor,
canvasColor: bgColor,
),
home: HomeScreen(),
);
}
}
마무리
첫 번째 프로젝트로 Youtube Music앱을 따라만들어 보았다. 반응형도 아니고, 에뮬레이터 기준으로 만들었기 때문에 실제 상용될 수 있는 UI를 만들었다기 보다는, Sliver를 이용해서 UI를 그리는 연습을 한 셈이다. 다음 번 프로젝트에서는 조금 더 완성도 있는 결과물을 만들어낼 예정이다.
'Flutter' 카테고리의 다른 글
[Flutter] UI(4) - ExpansionTile, Listile 아이콘 앞에 위치시키기(animation) (0) | 2023.12.25 |
---|---|
[Flutter] The parameter can’t have a value of ‘null’ because of its type in Dart 에러 해결 (1) | 2023.12.21 |
[Flutter] UI(3) - systemNavigationBar 색깔 바꾸기 (Device NavigationBar (0) | 2023.12.15 |
[Flutter] Widget(7) - FutureBuilder (0) | 2023.12.13 |
[Flutter] Widget(6) - Form (0) | 2023.12.12 |