SOPT 2차 세미나 과제
Bottom Navigation : 하단 네비게이션 뷰, 하단에 있는 아이콘 클릭을 통해 이동
ViewPager : 데이터를 페이지 단위로 표시하고 화면을 쓸어 넘기는 동작인 swipe를 통해 페이지 전환을 할 수 있는 컨테이너, 자체적으로 그리는 기능이 있지 않고 위젯을 배치하여 viewPager의 각 페이지를 구성
-
라이브러리 등록 - build.gradle(app) 수정
제일 상단에 아래 코드 추가
apply plugin: "kotlin-kapt"
dependencies 안에 아래 코드 추가
implementation 'androidx.recyclerview:recyclerview:1.1.0' //recyclerview를 다루기 위한 라이브러리 implementation "com.google.android.material:material:1.2.0-alpha05" //BottomNavigation 등 구글에서 제공하는 디자인 라이브러리 implementation "com.github.bumptech.glide:glide:4.10.0" kapt "com.github.bumptech.glide:compiler:4.10.0" //이미지 url을 로딩시켜 뷰에 연결해주는 라이브러리 implementation 'de.hdodenhof:circleimageview:3.1.0' //동그란 이미지 커스텀 뷰 라이브러리
-
custom UI 적용
A. custom UI 적용 - styles.xml 수정
<!-- 상단 바 기본 UI를 없애고 커스텀 UI 적용하는 부분 --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- Customize your theme here. --> </style>
B. 기본 색상 설정 - colors.xml 수정
<resources> <!-- 기본 색상 설정 --> <color name="colorPrimary">#073392</color> <color name="colorPrimaryDark">#08338d</color> <color name="colorAccent">#ff0000</color> <color name="white">#ffffff</color> </resources>
C. custom 상단 바 설정 - activity_main.xml 수정
<!-- custom 상단 바를 넣은 부분, ConstraintLayout의 자식 --> <com.google.android.material.appbar.AppBarLayout android:id="@+id/appBarLayout" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent"> <androidx.appcompat.widget.Toolbar android:id="@+id/main_toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorPrimary"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:maxEms="15" android:text="@string/app_name" android:textColor="@color/white" android:textSize="18sp" android:textStyle="bold" /> </androidx.appcompat.widget.Toolbar> </com.google.android.material.appbar.AppBarLayout>
D. Bottom Navigation / ViewPager layout 설정 - activity_main.xml 수정
<com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottomNavigationView" android:layout_width="0dp" android:layout_height="60dp" android:background="@color/colorPrimary" <!-- background 속성을 이용하여 아까 설정한 색 적용 --> app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> <androidx.viewpager.widget.ViewPager android:id="@+id/main_viewPager" android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/appBarLayout" app:layout_constraintVertical_bias="0.344" />
여기까지 세팅 완료! 이제 하단 네비게이션 메뉴를 만들어 보아요
E. 하단 네비게이션 메뉴 생성
-
res - new - android resource directory 선택해서 menu directory 생성
-
만든 menu folder - new - menu resource file 선택해서 파일 생성한 후 메뉴 설정
[navigation.xml]
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/menu_home" android:icon="@drawable/ic_home_white" android:title="Home"/> <item android:id="@+id/menu_book" android:icon="@drawable/ic_book_white" android:title="Book"/> <item android:id="@+id/menu_person" android:icon="@drawable/ic_person_white" android:title="MyPage"/> </menu>
F. vector asset 생성
- drawable - new - vector asset 선택해서 원하는 아이콘과 이름 설정 후 Finish
G. 메뉴가 클릭 되었을 때 / 되지 않았을 때의 색상 설정
-
res - new - android resource directory 선택해서 color directory 생성
-
만든 color folder - new - color resource file 선택해서 파일 생성한 후 색상 설정
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@color/white" android:state_checked="true" /> <item android:color="#d9d9d9" android:state_checked="false" /> </selector>
H. 생성한 메뉴 및 아이콘과 텍스트 색상 적용 - activity_main.xml 수정
<com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottomNavigationView" android:layout_width="0dp" android:layout_height="60dp" android:background="@color/colorPrimary" <!-- background 속성을 이용하여 아까 설정한 색 적용 --> app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:itemIconTint="@color/bottom_selector" app:itemTextColor="@color/bottom_selector" app:menu="@menu/navigation" /> <!-- 메뉴 및 아이콘 텍스트의 색깔 적용 -->
-
-
Fragment 생성
Fragment란? Activity 내에서 화면 UI 일부를 나타내고 여러 개의 fragment를 조합하여 Activity가 출력하는 한 화면의 UI 표현 가능, Activity 실행 중에도 화면에 동적으로 추가되거나 다른 fragment로 교체 가능
-
new - Fragment - Fragment(Blank) 선택해서 이름 설정하고 include 선택지 선택 취소한 후 Finish
[HomeFragment.kt]
class HomeFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_home, container, false) } }
[LibraryFragment.kt]
class LibraryFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_library, container, false) } }
[MypageFragment.kt]
class MypageFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_mypage, container, false) } }
[fragment_home.xml]
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".HomeFragment"> <!-- TODO: Update blank fragment layout --> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="홈 화면입니다." /> </FrameLayout>
[fragment_library.xml]
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".LibraryFragment"> <!-- TODO: Update blank fragment layout --> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="서재 화면입니다." /> </FrameLayout>
[fragment_mypage.xml]
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MypageFragment"> <!-- TODO: Update blank fragment layout --> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="마이페이지 화면입니다." /> </FrameLayout>
각 페이지에 해당하는 layout을 activity 하나가 아닌 여러 fragment로 분리하여 관리
이제 Adapter를 사용해서 ViewPager와 Fragment를 연결 !!
-
-
ViewPager에 Fragment 연결
A. Adapter 생성
Adapter란? 보여지는 view와 그 view에 보여줄 data를 연결하는 일종의 bridge
-
new - Kotlin File/Class 선택해서 Adapter class 생성
[MainPagerAdapter.kt]
class MainPagerAdapter(fm:FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT){ override fun getItem(position: Int): Fragment { return when(position) { 0 -> HomeFragment() //index가 0일때 1 -> LibraryFragment() else -> MypageFragment() } } override fun getCount() = 3 }
B. ViewPager에 Adapter 적용 - MainActivity.kt 수정
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) main_viewPager.adapter = MainPagerAdapter(supportFragmentManager) main_viewPager.offscreenPageLimit = 2 //현재 페이지가 2라면 | 0 1 2 3 4 까지 잡아놓아, 앞 뒤로 2 페이지씩 } }
-
-
Bottom Navigation의 메뉴 클릭 시 해당하는 fragment가 호출되어 화면에 표시되도록 설정 - MainActivity.kt 수정
main_viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { //ViewPager의 이동-> -> 하단 탭의 체크 상태 변화 override fun onPageScrollStateChanged(state: Int) { } override fun onPageScrolled( position: Int, positionOffset: Float, positionOffsetPixels: Int ) { } override fun onPageSelected(position: Int) { //navigation 메뉴 아이템 체크 bottomNavigationView.menu.getItem(position).isChecked = true } }) bottomNavigationView.setOnNavigationItemSelectedListener { //하단 탭의 체크 이벤트 -> 해당하는 페이지로 이동 when (it.itemId) { R.id.menu_home -> main_viewPager.currentItem = 0 R.id.menu_book -> main_viewPager.currentItem = 1 R.id.menu_person -> main_viewPager.currentItem = 2 } true }
RecyclerView : RecyclerView는 사용자가 관리하는 많은 수의 데이터 집합을 개별 item 단위로 구성하여 화면에 출력하는 ViewGroup이며, 한 화면에 표시되기 힘든 많은 수의 데이터를 scroll이 가능한 list로 표시해주는 위젯
동일한 형태를 가진 뷰 !! 틀은 같고 그 자리에 들어가는 데이터만 다르다
-
반복될 View 하나 생성
-
Layout - new - layout resource file 선택해서 새로운 layout file 생성
[item_insta.xml]
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:clipToPadding="false" > <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/constraintLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorPrimary" android:paddingHorizontal="24dp" android:paddingVertical="8dp" app:layout_constraintTop_toTopOf="parent" > <de.hdodenhof.circleimageview.CircleImageView android:id="@+id/img_profile" android:layout_width="48dp" android:layout_height="48dp" android:src="@drawable/profile" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv_username" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:text="TextView" android:textColor="@color/white" android:textSize="16sp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="@+id/img_profile" app:layout_constraintStart_toEndOf="@+id/img_profile" app:layout_constraintTop_toTopOf="@+id/img_profile" android:layout_marginLeft="8dp" /> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="@+id/tv_username" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/tv_username" app:srcCompat="@drawable/ic_more" tools:ignore="VectorDrawableCompat" /> </androidx.constraintlayout.widget.ConstraintLayout> <ImageView android:id="@+id/img_contents" android:layout_width="0dp" android:layout_height="0dp" android:scaleType="centerCrop" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintDimensionRatio="1" app:layout_constraintTop_toBottomOf="@+id/constraintLayout" app:srcCompat="@drawable/ic_launcher_background" tools:ignore="VectorDrawableCompat" /> </androidx.constraintlayout.widget.ConstraintLayout>
-
-
배치 방향 설정 - fragment_home.xml 수정
<androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_home" android:layout_width="match_parent" android:layout_height="wrap_content" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" android:orientation="vertical" tools:listitem="@layout/item_insta" android:clipToPadding="false" />
LayoutManager - LinearLayoutManager : 세로/가로 방향 배치 - GridLayoutManager : 바둑판 형식 배치
-
어디에 데이터를 넣을지, 어떤 데이터를 넣을지 설정
데이터가 있으면 Adapter가 데이터 리스트 중 하나를 ViewHolder에게 전달하고 ViewHolder는 받은 데이터를 View에 뿌려준다 !!
A. 데이터 형태 결정
[InstaData.kt]
data class InstaData ( val userName : String, val img_profile : String, val img_content : String )
B. ViewHolder 생성
ViewHolder란? 동일한 형태의 view 하나에 대한 데이터 넣을 위치 정보를 알고 있는 녀석
[InstaViewHolder.kt]
class InstaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val tv_username = itemView.findViewById<TextView>(R.id.tv_username) val img_profile = itemView.findViewById<ImageView>(R.id.img_profile) val img_contents = itemView.findViewById<ImageView>(R.id.img_contents) fun bind(instaData : InstaData) { tv_username.text = instaData.userName Glide.with(itemView).load(instaData.img_profile).into(img_profile) Glide.with(itemView).load(instaData.img_content).into(img_contents) } }
C. Adapter 생성
[InstaAdapter.kt]
class InstaAdapter(private val context : Context) : RecyclerView.Adapter<InstaViewHolder>() { var datas : MutableList<InstaData> = mutableListOf<InstaData>() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstaViewHolder { val view = LayoutInflater.from(context).inflate(R.layout.item_insta, parent, false) return InstaViewHolder(view) } override fun getItemCount(): Int { return datas.size } override fun onBindViewHolder(holder: InstaViewHolder, position: Int) { holder.bind(datas[position]) } }
D. RecyclerView에 Adpater 적용 - HomeFragment.kt 수정
class HomeFragment : Fragment() { lateinit var instaAdapter: InstaAdapter lateinit var layoutManager: LinearLayoutManager val datas : MutableList<InstaData> = mutableListOf<InstaData>() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_home, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initRcv(view) loadDatas() //데이터를 임의로 생성하고 어댑터에 전달해주겠습니다! } private fun initRcv(view : View) { instaAdapter = InstaAdapter(view.context) rv_home.adapter = instaAdapter //recyclerView의 어댑터를 instaAdapter로 지정 rv_home.apply { layoutManager = LinearLayoutManager(view.context) addItemDecoration(InstaItemDecoration(view.context)) } } private fun loadDatas() { datas.apply { add(InstaData( userName = "", img_profile = "", img_content = "" )) } //AndroidMenifest.xml에서 uses-permission INTERNET 설정을 해줘야만 이미지 로드 가능 instaAdapter.datas = datas instaAdapter.notifyDataSetChanged() //데이터가 갱신되었음을 Adapter에게 알려주는 역할 } }
실습 결과
itemDecoration과 clipToPadding의 기능이 무엇인지 요약하기
itemDecoration : itemDecoration를 통해 item 사이의 간격을 설정 할 수 있다.
clipToPadding
: top 혹은 bottom에 padding을 주었을 경우
android:clipToPadding="false"
를 해줘야 scroll하면서
설정한 padding 부분이 사라지지 않는다.