"부장님 추가하기" 기능을 구현할 차례.
문제 상황
우리가 구상한 방식은 스와이프 방식의 설문지 형식이었고, 다른 방식으로의 타협은 없었다.
이 기능만큼은 무조건 한 문항씩 답변하고 다음 문항으로 스와이프하는 형식이어야만 했다.
그런데 이게 말이 쉽지, 막상 구현하려니 제일 애먹은 부분 중 하나였다.
뭐라고 검색해야 할지조차 모르겠어서 더 어려웠다.
어찌저찌 생각나는 단어 다 열거해서 검색해보고,
(동적 layout 추가 삭제, fragment 슬라이드, 레이아웃 재사용, programmatically include layout, ...)
여러 깃허브 프로젝트들 기웃거리면서 며칠간 헤매다가 드디어 원하던걸 찾았다.
내가 필요했던건 ViewPager!!!!!🤪
처음엔 시간에 쫓겨서 일단 어떻게든 돌아가도록 아무렇게나 만들었다...
얼마나 아무렇게였냐면 그저 fragment로 다 때려박고 밑에 navigation을 만들어놨다.
그리고 fragment끼리는 정보 전달이 너무 힘드니까, 각 fragment마다 작업이 끝나면 바로바로 DB에 저장해버렸다.
원래는 1~5번 문항을 모두 대답한 뒤에 한꺼번에 정보를 DB에 넣는게 맞는데,
솔직히 너무 어렵고 귀찮아서 일단 그렇게 만들었다. ㅎ...😞
근데 아무리 봐도 이건 아니다 싶었고, 교수님도 왜 이렇게 만들었냐고 구박 많이 하셨다. ㅠㅠ
그래서 그 이후에 조금 수정한 버전이 ViewModel을 이용해서 데이터를 저장하고
마지막 fragment에서 데이터를 한꺼번에 DB에 저장하는 방식이었는데,
그 것도 결국엔 큰 틀을 바꾸진 않고 fragment를 이용한거라 UX가 정말 말도 아니었다...
...... 🤦🏻♀️
이렇게 하니까 fragment끼리 연결이 안 돼서 각각 독립적으로 행동했고,
상식적으로 well made UI/UX 라고 할 수 없었으며,
전 문항으로 되돌아가면 내가 입력했던 값이 보존되어 있지도 않았다.
그도 그럴것이 밑에 navigation을 클릭하면 매번 fragment를 새로 띄우는 방식이니까ㅜㅜ
이건 절대 우리가 추구하던 방식이 아니었고
결국 제일 이상적인 방법은 한 activity 내에서 모든 작업을 처리하게끔 만드는건데,
기한 안에 앱을 완성하려면 어쩔 수 없이 이대로 넘어가고 다른 기능을 만드는데 집중해야했다.
하지만 이럴바에는 스와이프 고집 버리고 그냥 한 페이지에 모든 문항을 한꺼번에 보여주는게 훨씬 낫지...
아무리 봐도 이건 아닌 거 같아서 주말에 각 잡고 붙들어매서 해결!
해결 방법
1. Layout
- activity layout에 ViewPager를 적용시키기
<?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="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/addBujangTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:textColor="@color/titleGray"
android:textSize="28sp"
android:fontFamily="@font/tmoneybold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="부장님 정보 등록" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/mViewPager"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
<!-- indicator -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/viewPagerIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<ImageView
android:id="@+id/indicator0_iv_main"
android:layout_width="10dp"
android:layout_height="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/shape_circle_purple" />
<ImageView
android:id="@+id/indicator1_iv_main"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginLeft="5dp"
app:layout_constraintBottom_toBottomOf="@+id/indicator0_iv_main"
app:layout_constraintStart_toEndOf="@+id/indicator0_iv_main"
app:layout_constraintTop_toTopOf="@+id/indicator0_iv_main"
app:srcCompat="@drawable/shape_circle_gray" />
<ImageView
android:id="@+id/indicator2_iv_main"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginLeft="5dp"
app:layout_constraintBottom_toBottomOf="@+id/indicator1_iv_main"
app:layout_constraintStart_toEndOf="@+id/indicator1_iv_main"
app:layout_constraintTop_toTopOf="@+id/indicator1_iv_main"
app:srcCompat="@drawable/shape_circle_gray" />
<ImageView
android:id="@+id/indicator3_iv_main"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginLeft="5dp"
app:layout_constraintBottom_toBottomOf="@+id/indicator2_iv_main"
app:layout_constraintStart_toEndOf="@+id/indicator2_iv_main"
app:layout_constraintTop_toTopOf="@+id/indicator2_iv_main"
app:srcCompat="@drawable/shape_circle_gray" />
<ImageView
android:id="@+id/indicator4_iv_main"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginLeft="5dp"
app:layout_constraintBottom_toBottomOf="@+id/indicator3_iv_main"
app:layout_constraintStart_toEndOf="@+id/indicator3_iv_main"
app:layout_constraintTop_toTopOf="@+id/indicator3_iv_main"
app:srcCompat="@drawable/shape_circle_gray" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
- 전환시킬 페이지에 해당하는 layout들 만들기
문항이 5개니까 총 5가지의 layout을 만들었다.
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:paddingRight="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="100">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40">
</LinearLayout>
<TextView
style="@style/parent.TextLayout"
android:textSize="40sp"
android:fontFamily="@font/tmoneyregular"
android:text="Q5. \n부장님의 주량은? " />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="15"
android:gravity="center"
android:orientation="vertical"
android:weightSum="10">
<SeekBar
android:id="@+id/drinkbar"
style="@style/Widget.AppCompat.SeekBar.Discrete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_weight="4"
android:max="4"
android:progress="2" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="6"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingLeft="7dp"
android:text="논알콜"
android:textSize="22dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="right"
android:paddingRight="5dp"
android:text="약주 두병 UP"
android:textSize="22dp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="25" >
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/btn_before_drink"
android:background="@drawable/before"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:layout_weight="1">
<Button
android:id="@+id/btn_save_drink"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="제출하기" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
2. Activity
PagerAdapter로 ViewPager 생성.
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager.widget.ViewPager
import com.google.firebase.auth.FirebaseAuth
import com.stopmeifyoucan.makneya.Data.InDB
import kotlinx.android.synthetic.main.activity_addbujang.*
import kotlinx.android.synthetic.main.layout_add_bujangdrink.*
import kotlinx.android.synthetic.main.layout_add_bujangggondae.*
import kotlinx.android.synthetic.main.layout_add_bujanghurry.*
import kotlinx.android.synthetic.main.layout_add_bujangname.*
import kotlinx.android.synthetic.main.layout_add_bujangspicy.*
import okhttp3.MediaType
import okhttp3.RequestBody
import org.json.JSONObject
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class AddBujang : AppCompatActivity() {
var viewList = ArrayList<View>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_addbujang)
viewList.add(layoutInflater.inflate(R.layout.layout_add_bujangname, null))
viewList.add(layoutInflater.inflate(R.layout.layout_add_bujangggondae, null))
viewList.add(layoutInflater.inflate(R.layout.layout_add_bujanghurry, null))
viewList.add(layoutInflater.inflate(R.layout.layout_add_bujangspicy, null))
viewList.add(layoutInflater.inflate(R.layout.layout_add_bujangdrink, null))
mViewPager.adapter = CustomPagerAdapter()
mViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
// This method will be invoked when the current page is scrolled, either as part of
// a programmatically initiated smooth scroll or a user initiated touch scroll.
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
addBujangTitle.text = "부장님 정보 등록"
btn_save_name.setOnClickListener {
Log.d("부장 이름은", bujang_nickname.text.toString())
mViewPager.setCurrentItem(position + 1, true)
}
btn_before_ggon.setOnClickListener {
mViewPager.setCurrentItem(position - 1, true)
}
btn_save_ggon.setOnClickListener {
Log.d("꼰대력은", (ggonbar.progress + 1).toString())
mViewPager.setCurrentItem(position + 1, true)
}
if (position == 2) {
btn_before_hurry.setOnClickListener {
mViewPager.setCurrentItem( position - 1, true)
}
btn_save_hurry.setOnClickListener {
Log.d("성질머리는", (hurrybar.progress + 1).toString())
mViewPager.setCurrentItem(position + 1, true)
}
}
if (position == 3) {
btn_before_spicy.setOnClickListener {
mViewPager.setCurrentItem(position - 1, true)
}
btn_save_spicy.setOnClickListener {
Log.d("매운음식 선호도는", (spicybar.progress + 1).toString())
mViewPager.setCurrentItem(position + 1, true)
}
}
if (position == 4) {
btn_before_drink.setOnClickListener {
mViewPager.setCurrentItem(position - 1, true)
}
btn_save_drink.setOnClickListener {
Log.d("주량은", (drinkbar.progress + 1).toString())
// 데이터 처리
val intent = Intent(this@AddBujang, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
}
}
}
// This method will be invoked when a new page becomes selected.
override fun onPageSelected(position: Int) {
indicator0_iv_main.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.shape_circle_gray, null))
indicator1_iv_main.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.shape_circle_gray, null))
indicator2_iv_main.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.shape_circle_gray, null))
indicator3_iv_main.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.shape_circle_gray, null))
indicator4_iv_main.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.shape_circle_gray, null))
when(position) {
0 -> indicator0_iv_main.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.shape_circle_purple, null))
1 -> indicator1_iv_main.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.shape_circle_purple, null))
2 -> indicator2_iv_main.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.shape_circle_purple, null))
3 -> indicator3_iv_main.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.shape_circle_purple, null))
4 -> indicator4_iv_main.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.shape_circle_purple, null))
}
}
// Called when the scroll state changes. Useful for discovering when the user begins
// dragging, when the pager is automatically settling to the current page,
// or when it is fully stopped/idle.
override fun onPageScrollStateChanged(state: Int) {
Log.d("TAG", "onPageScrollStateChanged : $state")
}
})
}
inner class CustomPagerAdapter : PagerAdapter() {
// 사용 가능한 뷰의 갯수 리턴
override fun getCount(): Int {
return viewList.size
}
// instantiateItem에서 만든 객체를 사용할지 판단
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
// position에 해당하는 페이지 생성
override fun instantiateItem(container: ViewGroup, position: Int): Any {
mViewPager.addView(viewList[position])
return viewList[position]
}
// position에 해당하는 페이지 제거
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
mViewPager.removeView(`object` as View)
}
}
}
Log를 찍어서 확인해보면 페이지를 넘길 때 데이터가 손실되지 않는다.
앞으로 갔다가 다시 돌아와도 데이터가 그대로 유지된다.
Fragment의 늪에서 벗어나 장족의 발전을 이뤄내서 너무 감격스럽다...😂
사용자들이 '부장님 정보 등록'의 진행 상황을 직관적으로 파악할 수 있도록
밑에 indicator까지 달아줬다.
Indicator에 해당하는 부분은 onPageSelected() 안에 넣어주면 된다.
문제가 하나 있었는데, 처음에는 버튼 클릭 이벤트들을 그냥 쭉 늘어놨더니
NullPointerException: Attempt to invoke virtual method 'void android.widget.ImageView
라는 에러메세지가 떴다.
확인해보니 btn_hurry_before 의 클릭 이벤트 부분에서부터 에러가 났다.
왜 하필 거기서부터 null pointer 에러가 떴는지는 잘 모르겠다.
뜰거면 그 전에 btn_ggon_before 부터 에러가 뜨는게 맞지 않나?
아무튼 이 에러는 위처럼 조건문을 달아주니까 금방 해결됐다.
아 그리고 잠깐 헤맸던 부분이 있었는데
PagerAdapter 상속 받는 부분에서 `object` 의 작은따옴표는 ' 이 아니고 ` 이다.
(키보드에서 숫자 1 왼쪽에 위치한 키)
'object'라고 썼다가 빨간줄 떠서 뭐가 틀린건지 한참 찾았던 기억이 나서 적어놓음. 😂😂
참고자료
https://ddolcat.tistory.com/572
https://blog.yena.io/studynote/2019/11/13/Android-View-Pager-Basic.html
https://blog.mindorks.com/android-viewpager-in-kotlin
❤와 댓글은 큰 힘이 됩니다. 감사합니다 :-)
'Devlog > Android' 카테고리의 다른 글
[Android: Kotlin] ScrollView에 CardView 여러개 추가하기 / 동적으로 CardView 추가하기 (0) | 2021.10.02 |
---|---|
[Android: Kotlin] Bottom Navigation (화면 맨 아래 탭 기능) 구현하기 (0) | 2021.09.06 |
[Android] 폴더 생성해서 프로젝트 구조 깔끔하게 정리하기 (0) | 2021.08.29 |
[Android] 자주 쓰는 안드로이드 스튜디오 단축키 (0) | 2021.07.01 |
[Android] Emulator 문제 해결방법 (0) | 2021.06.29 |