Devlog/Android

[Android: Kotlin] ViewPager로 fragment 스와이프 - 옆으로 밀어서 넘기는 단일문항 설문지 기능 구현하기

FATKITTY 2021. 10. 2. 14:37
반응형

"부장님 추가하기" 기능을 구현할 차례.

 

문제 상황

 

우리가 구상한 방식은 스와이프 방식의 설문지 형식이었고, 다른 방식으로의 타협은 없었다.

이 기능만큼은 무조건 한 문항씩 답변하고 다음 문항으로 스와이프하는 형식이어야만 했다.

그런데 이게 말이 쉽지, 막상 구현하려니 제일 애먹은 부분 중 하나였다.

뭐라고 검색해야 할지조차 모르겠어서 더 어려웠다.

어찌저찌 생각나는 단어 다 열거해서 검색해보고,

(동적 layout 추가 삭제, fragment 슬라이드, 레이아웃 재사용, programmatically include layout, ...)

여러 깃허브 프로젝트들 기웃거리면서 며칠간 헤매다가 드디어 원하던걸 찾았다.

 

내가 필요했던건 ViewPager!!!!!🤪

 

처음엔 시간에 쫓겨서 일단 어떻게든 돌아가도록 아무렇게나 만들었다...

얼마나 아무렇게였냐면 그저 fragment로 다 때려박고 밑에 navigation을 만들어놨다.

그리고 fragment끼리는 정보 전달이 너무 힘드니까, 각 fragment마다 작업이 끝나면 바로바로 DB에 저장해버렸다.

원래는 1~5번 문항을 모두 대답한 뒤에 한꺼번에 정보를 DB에 넣는게 맞는데,

솔직히 너무 어렵고 귀찮아서 일단 그렇게 만들었다. ㅎ...😞

 

근데 아무리 봐도 이건 아니다 싶었고, 교수님도 왜 이렇게 만들었냐고 구박 많이 하셨다. ㅠㅠ

그래서 그 이후에 조금 수정한 버전이 ViewModel을 이용해서 데이터를 저장하고

마지막 fragment에서 데이터를 한꺼번에 DB에 저장하는 방식이었는데,

그 것도 결국엔 큰 틀을 바꾸진 않고 fragment를 이용한거라 UX가 정말 말도 아니었다...

 

기존의 view model을 이용한 fragment 덕지덕지 방식.

 

아,, 글씨 크기부터 킹받음 ㅠㅠ

...... 🤦🏻‍♀️

이렇게 하니까 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>

ViewPager

 

- 전환시킬 페이지에 해당하는 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>

마지막 문항 layout

 

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

https://yuuj.tistory.com/181

https://yuuj.tistory.com/39

https://www.flaticon.com/free-icon/next_591855?term=arrow&page=1&position=47&page=1&position=47&related_id=591855&origin=search 

 

 

  ❤와 댓글은 큰 힘이 됩니다. 감사합니다 :-)  

반응형