Kotlin

UMC project 1주차(23/01/02~23/01/10)

취업하고싶다! 2023. 1. 11. 20:04

UMC 3기 프로젝트를 시작했다.

안드로이드 프론트(코틀린) 팀장으로 참여했고 우리 팀이 개발할 어플은 '강아지 건강 관련 어플'이다.

우선, 내가 한 주간 맡은 부분은 시작화면과 반려견 등록 화면이다.

 

첫 화면, 반려견 등록 화면

첫 화면 구현은 어렵지 않게 해결했다.

레이아웃 디자인은 constraintLayout을 사용했다.

 

문제는 반려견 등록하기 화면에서 나타났다.

화면의 견종, 강아지 성별, 나이 밑을 보면 드롭다운 형식으로 항목들을 선택하게 하는 스피너를 구현해야 한다.

코틀린 공부하면서 스피너를 접한 적이 없어서 스피너에 대해 따로 찾아보았다.

 


스피너란?

값 집합에서 하나의 값을 선택할 수 있는 빠른 방법을 제공하고 기본 상태의 스피너는 현재 선택된 값을 표시한다.

스피너를 터치하면 기타 모든 사용 가능한 값을 포함하는 드롭다운 메뉴 혹은 다이얼로그가 표시되며, 여기서 새 값을 선택할 수 있다.

스피너는 다음과 같이 구현할 수 있다.

<Spinner
    android:id="@+id/planets_spinner"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

선택 항목 목록으로 스피너를 채우려면 Activity 또는 Fragment 소스 코드에 SpinnerAdapter를 지정해야 한다.

주요 클래스는 다음과 같다.

 

스피너에 제공하는 선택 항목은 어떠한 소스에서든 가져올 수 있지만, SpinnerAdapter를 통해 제공되어야 한다. 예를 들어 선택 항목을 배열에서 사용할 수 있는 경우에는 ArrayAdapter, 선택 항목을 데이터베이스 쿼리에서 사용할 수 있는 경우에는 CursorAdapter를 통해 제공한다.

예를 들어, 스피너에 사용할 수 있는 선택 항목이 사전 결정된 경우에는 문자열 리소스 파일에 정의된 문자열 배열을 사용하여 이러한 선택 항목을 제공할 수 있다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="planets_array">
        <item>Mercury</item>
        <item>Venus</item>
        <item>Earth</item>
        <item>Mars</item>
        <item>Jupiter</item>
        <item>Saturn</item>
        <item>Uranus</item>
        <item>Neptune</item>
    </string-array>
</resources>

이러한 배열과 함께 Activity 또는 Fragment에 다음 코드를 사용하여 ArrayAdapter의 인스턴스를 통해 스피너에 이러한 배열을 제공할 수 있다.

val spinner: Spinner = findViewById(R.id.spinner)
// Create an ArrayAdapter using the string array and a default spinner layout
ArrayAdapter.createFromResource(
        this,
        R.array.planets_array,
        android.R.layout.simple_spinner_item
).also { adapter ->
    // Specify the layout to use when the list of choices appears
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
    // Apply the adapter to the spinner
    spinner.adapter = adapter
}

createFromResource() 메서드를 사용하면 문자열 배열에서 ArrayAdapter를 생성할 수 있다. 이 메서드의 세 번째 인수는 선택된 항목이 스피너 컨트롤에 나타나는 방식을 정의하는 레이아웃 리소스이다. simple_spinner_item 레이아웃은 플랫폼에서 제공하며 스피너의 모양에 관해 자체적인 레이아웃을 직접 정의하고자 하지 않을 경우 사용해야 하는 기본 레이아웃이다.

그런 다음, setDropDownViewResource(int)를 호출하여 어댑터가 스피너 선택 항목의 목록을 표시하는 데 사용해야 하는 레이아웃을 지정해야 한다(simple_spinner_dropdown_item은 플랫폼에서 정의하는 또 다른 표준 레이아웃임).

setAdapter()를 호출하여 어댑터를 Spinner에 적용한다.

 

사용자 선택에 응답

사용자가 드롭다운에서 항목을 선택하면 Spinner 객체가 항목 선택 시 이벤트를 수신한다.

스피너에 관한 선택 이벤트 핸들러를 정의하려면 AdapterView.OnItemSelectedListener 인터페이스와 이에 상응하는 onItemSelected() 콜백 메서드를 구현한다. 예를 들어, 다음은 Activity의 인터페이스 구현이다.

class SpinnerActivity : Activity(), AdapterView.OnItemSelectedListener {

    override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
        // An item was selected. You can retrieve the selected item using
        // parent.getItemAtPosition(pos)
    }

    override fun onNothingSelected(parent: AdapterView<*>) {
        // Another interface callback
    }
}

 

AdapterView.OnItemSelectedListener에는 onItemSelected() onNothingSelected() 콜백 메서드가 필요하다.

그런 후 setOnItemSelectedListener()를 호출하여 인터페이스 구현을 지정해야 한다.

val spinner: Spinner = findViewById(R.id.spinner)
spinner.onItemSelectedListener = this

Activity 또는 Fragment를 사용하여 AdapterView.OnItemSelectedListener 인터페이스를 구현하는 경우(위의 예시 참조) this를 인터페이스 인스턴스로 전달하면 된다.


스피너를 사용해서 먼저 다음과 같이 화면 디자인을 했다.

디자이너가 스피너 모양을 위의 사진과 같이 모서리가 둥글게 디자인해달라고 요청해서 frame.xml 파일을 따로 만들고 스피너의 background에 넣어주고 spinnerMode를 dropdown으로 설정하였다. 

frame.xml 파일로 background값을 넣으니 기본 스피너에서 보이던 화살표 버튼이 보이지 않아, frame.xml 파일에 @drawable/select_button 아이템을 넣어주고 gravity값을 center|end로 설정해 오른쪽 끝 중앙에 배치하였다.

//견종 스피너//
<Spinner
    android:id="@+id/breed_spinner"
    android:layout_width="370dp"
    android:layout_height="50dp"
    android:background="@drawable/dog_register_frame"
    android:spinnerMode="dropdown"
    android:layout_marginTop="8dp"
    android:layout_marginLeft="5dp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/breed_title"
    />
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="#FFFFFF"/>
            <corners android:radius="12dp" />
            <padding android:right="12dp" />
            <stroke
                android:width="1dp"
                android:color="#979797" />
        </shape>
    </item>
    <item
        android:drawable="@drawable/select_button"
        android:gravity="center|end"
        android:width="10dp"
        android:height="10dp" />

</layer-list>

 

레이아웃 디자인은 끝내고 스피너 배열을 따로 만들어주었다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="spinner_breed">
        <item>허스키</item>
        <item>푸들</item>
        <item>셰퍼드</item>
        <item>맬러뮤트</item>
        <item>도베르만</item>
        <item>골든 리트리버</item>
        <item>래브라도 레트리버</item>
        <item>웰시코기</item>
        <item>사모예드</item>
        <item>시바 이누</item>
        <item>스피츠</item>
        <item>슈나우저</item>
        <item>비숑프리제</item>
        <item>시추</item>
        <item>포메라니안</item>
        <item>파피용</item>
        <item>요크세테리어</item>
        <item>말티즈</item>
        <item>닥스훈트</item>
        <item>치와와</item>
        <item>퍼그</item>
    </string-array>
</resources>

breed_array라는 xml 파일을 만들어주고 이름을 id를 spinner_breed로 설정했다.

 

스피너를 연결하기 위해 함수 두개를 작성하였다.

Activity에 다음 코드를 작성해 스피너를 연결해주었다. 

private fun setupBreedData() {
        val breedData = resources.getStringArray(R.array.spinner_breed)
        val breedAdapter = object : ArrayAdapter<String>(this, R.layout.breed_spinner) {

            override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {

                val v = super.getView(position, convertView, parent)
                return v
            }
        }
        //아이템 추가
        breedAdapter.addAll(breedData.toMutableList())

        //어댑터 연결
        viewBinding.breedSpinner.adapter = breedAdapter

        //droplist를 spinner와 간격을 두고 나오게 해줌
        //아이템 크기가 50dp 이므로 50dp 간격을 줌
        //이때 dp 를 px 로 변환해 주는 작업이 필요
        breed_spinner.dropDownVerticalOffset = dipToPixels(50f).toInt()
    }

    private fun setupBreedHandler() {

        viewBinding.breedSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
                when(position) {
                    0 -> {
                    }
                    else -> {
                    }
                }
            }
            override fun onNothingSelected(p0: AdapterView<*>?) {
            }
        }
    }

 

그런데,디자이너가 원한 화면은 Edittext의 hint처럼 보이게 하는 것이었다.

스피너

구글링을 해본 결과, 다음과 같이 Activity에 코드를 작성하면 Edittext의 hint처럼 만들 수 있다고하여서 따라해보았다.

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {

                val v = super.getView(position, convertView, parent)

                if (position == count) {
                    //마지막 포지션의 textView 를 힌트 용으로 사용
                    (v.findViewById<View>(R.id.tvBreedSpinner) as TextView).text = ""
                    //아이템의 마지막 값을 불러와 hint로 추가
                    (v.findViewById<View>(R.id.tvBreedSpinner) as TextView).hint = getItem(0)
                }

                return v
            }

            override fun getCount(): Int {
                //마지막 아이템은 힌트용으로만 사용하기 때문에 getCount에 1을 빼줌
                return super.getCount() - 1
            }

        }
        //아이템 추가
        breedAdapter.addAll(breedData.toMutableList())

        //힌트로 사용할 문구를 마지막 아이템에 추가
        breedAdapter.add("견종을 선택해주세요.")


        //어댑터 연결
        viewBinding.breedSpinner.adapter = breedAdapter

        //스피너 초기값을 마지막 아이템으로 설정(마지막 아이템이 힌트이므로)
        breed_spinner.setSelection(breedAdapter.count)

다음과 같이 코드를 적용하고 AVD를 돌려보니 힌트처럼 스피너 안에 잘 나왔지만 스피너를 눌렀을 때, 드롭다운의 시작이 맨 아래로 설정되어있었다. 

스피너 힌트

스피너를 눌렀을 때, 첫 번째 요소가 보여야하는데 맨 아래 요소가 보였다. 

아마 스피너 초기값을 마지막 요소로 설정해서 그런 것 같은데, 힌트처럼 보여주려면 견종 배열을 먼저 추가하고 맨 마지막에 힌트인 "견종을 선택해주세요"를 추가하고 getCount - 1 을 통해 마지막 요소로 들어간 힌트를 빼주는 것 같은데 이는 해결을 아직 못했다.

 

따라서 일단은 다음과 같이 먼저 힌트값을 추가하고 그 후에 견종 배열을 추가하고 getCount - 1 대신 getCount를 사용해 첫 번째 요소로 힌트값이 들어오게 설정했다. 그리고 첫 번째 요소인 "견종을 선택해주세요"는 아무것도 선택하지 않은 것이므로 선택해도 변화가 없게 설정하였다. 

    private fun setupBreedData() {
        val breedData = resources.getStringArray(R.array.spinner_breed)
        val breedAdapter = object : ArrayAdapter<String>(this, R.layout.breed_spinner) {

            override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {

                val v = super.getView(position, convertView, parent)

                if (position == 0) {
                    (v.findViewById<View>(R.id.tvBreedSpinner) as TextView).text = ""
                    (v.findViewById<View>(R.id.tvBreedSpinner) as TextView).hint = getItem(0)
                }
                return v
            }

            override fun getCount(): Int {
                return super.getCount()
            }
        }

        breedAdapter.add("견종을 선택해주세요.")
        breedAdapter.addAll(breedData.toMutableList())

        viewBinding.breedSpinner.adapter = breedAdapter

        breed_spinner.setSelection(0)
        breed_spinner.dropDownVerticalOffset = dipToPixels(50f).toInt()
    }

    private fun setupBreedHandler() {

        viewBinding.breedSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
                validSpinner1 = true
                when(position) {
                    0 -> {
                        next_page_btn.isEnabled = false
                        next_page_btn.setBackgroundResource(R.drawable.disabled_button)
                    }
                    else -> {
                    }
                }
            }
            override fun onNothingSelected(p0: AdapterView<*>?) {
                validSpinner1 = false
                next_page_btn.isClickable = false
                next_page_btn.isEnabled = false
            }
        }
    }
class DogRegisterActivity : AppCompatActivity() {

    var validSpinner1: Boolean= false
    var validSpinner2: Boolean= false
    var validSpinner3: Boolean= false

또한 디자인을 보면, 항목이 선택되지 않았을 때는 버튼이 비활성화 상태이고 모든 항목을 선택했을 때 버튼을 활성화해야하므로 validSpinner1 라는 Boolean 타입의 변수를 맨 위 상단에 선언해주고 0번째 아이템(힌트)을 제외한 아이템이 선택되었을 때 true값으로 바꾸어 주었다. 

해당 레이아웃은 Edittext 1개&Spinner 3개로 구성되어있으므로 validSpinner1이라는 변수 외에 다른 변수들도 만들어 Spinner가 모두 선택되고 Edittext안에 값이 있을 때 버튼을 활성화해주기 위해 코드를 다음과 같이 작성하였다.(Edittext는 아직 x)

또한 0번째 아이템(힌트)를 선택하였을 때는 아무것도 선택하지 않은 것이므로 버튼을 비활성화해주었다.

위의 코드를 적용하니 다음과 같이 스피너를 클릭했을 때 정상적으로 리스트의 상단목록이 나왔다.

첫 번째 요소인 견종을 선택해주세요를 선택해도

if (position == 0) {
                    (v.findViewById<View>(R.id.tvBreedSpinner) as TextView).text = ""
                    (v.findViewById<View>(R.id.tvBreedSpinner) as TextView).hint = getItem(0)
                }

이 코드로 인해 다시 회색 글씨의 견종을 선택해주세요가 뜨고 아무것도 선택하지 않은 것으로 만들었다.

 

하지만, 위처럼 강아지 성별, 나이도 함수를 두 개씩 만들었지만, 모든 스피너를 선택했을 때 버튼이 활성화되는 것을 구현하지 못했다.

if(validSpinner1==true&&validSpinner2==true&&validSpinner3==true) {
    next_page_btn.isEnabled
    next_page_btn.isClickable
    next_page_btn.setBackgroundResource(R.drawable.start_button)
}

validSpinner1, 2, 3 모두 기본값을 false로 설정하고 각각 0번째 아이템이 아닌 아이템들이 선택되었을 때 true값으로 바꿔주고 onCreate() 함수 내에 다음과 같이 validSpinner1, 2, 3이 모두 true일 때 버튼 활성화&클릭 가능&버튼 색 변경을 해주었는데 모든 스피너가 선택되어도 버튼이 활성화되지 않았다.

 

버튼 활성화 해결 x

 

견종 스피너를 눌렀을 때 검색하기 기능이 들어가면 좋겠다고 하여서 스피너 검색 기능을 찾아보았다. 

 

1. build.gradle에 implementation을 추가

dependencies {
	implementation 'com.toptoche.searchablespinner:searchablespinnerlibrary:1.3.1'
}

2. layout에 spinner 사용

<com.toptoche.searchablespinnerlibrary.Searchablespinner

/>

build.gradle에 해당 라이브러리를 추가하고 레이아웃에서 위의 Searchablespinner를 사용하면 된다고 나와있는데 더 이상 라이브러리 지원을 하지 않는건지 계속 레이아웃에서 해당 스피너가 오류가 났다(해결 x)

 

 

디자이너가 Edittext에 작성할 때 글씨 색이 바뀌면 좋겠다고 요청해서 다음 코드로 글자를 작성할 때는 검은색 글씨로 표시하고 글자가 완성이 되면 기존 텍스트 색상인 오렌지 색상으로 나오게끔 만들었다.

viewBinding.dogNameEdtText.addTextChangedListener(object : TextWatcher {
    override fun afterTextChanged(s: Editable?) {
    }

    override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
    }

    override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        p0?.let { highlightText(it as Editable) }
    }
})

Oncreate() 안에 다음과 같은 함수를 적어주었다.

TextWatcher라는 함수를 통해 텍스트를 쓰는 중, 텍스를 다 쓰고 난 후, 텍스트를 쓰기 전에 기능을 추가할 수 있다.

텍스트를 쓰는 중에 글자 색이 바뀌어야 하므로 onTextChanged() 함수 내에 highlightText라는 함수를 선언하고 해당 함수는 밖에 따로 선언해주었다.

private fun highlightText(text: Editable) {
    viewBinding.dogNameEdtText.text?.let { editable ->
        val spans = editable.getSpans(0, editable.length,
            ForegroundColorSpan::class.java)

        spans.forEach { span ->
            editable.removeSpan(span)
        }
    }

    val endIndex = text.length
    val startIndex = if (endIndex < 1) 0 else (endIndex-1)

    viewBinding.dogNameEdtText.text?.setSpan(
        ForegroundColorSpan(Color.BLACK),
        startIndex,
        endIndex,
        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
    )
}

endIndex를 텍스트의 길이로 설정하고 한 글자씩 입력할 때 마다 색상을 바꿔주기 위해 startIndex를 endIndex-1 로 설정하였다. 

다음과 같이 함수를 작성하였더니 글자색이 작성할 때 마다 잘 바뀌지만 마지막 글자색이 바뀌지 않았다.

글자 하이라이트

 

목요일(23/01/12)에 프론트엔드 구성원끼리 회의를 진행하기로 하여서 일단은 여기까지 코드를 작성하고 업로드하였다.


해결하지 못한 것

  • Edittext&Spinner 3개 모두 조건 충족되어야 버튼 활성화
  • Edittext 글자색 실시간으로 변경하는 것 마지막 글자색 변경
  • Spinner 검색 기능 구현
  • Spinner에 힌트값 보여주고 클릭했을 때 힌트값은 리스트에 없고 첫 번째 요소가 나오게 하는 것

'Kotlin' 카테고리의 다른 글

Kotlin 면접준비(1)  (0) 2023.06.04
Kotlin- 문법1  (0) 2023.06.02
클린 아키텍처(Clean Architecture)  (0) 2023.06.02
코틀린 컨벤션  (0) 2023.06.01
UMC 1주차  (0) 2022.12.30