앱 프로젝트 - 02 - 3 (로또 번호 추첨기) - NumberPicker, ContextCompat을 통해 drawable에 접근하기, lazy init, 리스너설정에 대한 방법론

소개

6자리의 로또 번호를 랜덤으로 만드는 앱

드로버블에 대한 공식 설명 : https://developer.android.com/guide/topics/resources/drawable-resource?hl=ko

--> 이중에서 Shape Drawable을 사용할 것임
--> Shape Drawable에 대한 자세한 설명은 02 - 1 글에 있음


레이아웃 소개

노란 원의 숫자는 직접 선택한 것, 나머지 숫자는 자동생성으로 생성한 것


코드 소개

activity_main.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="match_parent"
    tools:context=".MainActivity">

    <NumberPicker
        android:id="@+id/numberPicker"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!--
    번호를 선택하기 위한 NumberPicker위젯
 -->

    <Button
        android:id="@+id/addButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="16dp"
        android:text="@string/addNumber"
        app:layout_constraintEnd_toStartOf="@+id/clearButton"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/numberPicker" />

    <Button
        android:id="@+id/clearButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/clear"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/addButton"
        app:layout_constraintTop_toTopOf="@id/addButton" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:gravity="center"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/addButton">

        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="1"
            android:background="@drawable/circle_blue"
            android:gravity="center"
            android:textColor="@color/white"
            android:textSize="18sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:visibility="visible" />
            
        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="1"
            android:background="@drawable/circle_yellow"
            android:gravity="center"
            android:textColor="@color/white"
            android:textSize="18sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/textView3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="1"
            android:background="@drawable/circle_red"
            android:gravity="center"
            android:textColor="@color/white"
            android:textSize="18sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/textView4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="1"
            android:background="@drawable/circle_gray"
            android:gravity="center"
            android:textColor="@color/white"
            android:textSize="18sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/textView5"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="1"
            android:background="@drawable/circle_gray"
            android:gravity="center"
            android:textColor="@color/white"
            android:textSize="18sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/textView6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="1"
            android:background="@drawable/circle_gray"
            android:gravity="center"
            android:textColor="@color/white"
            android:textSize="18sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:visibility="visible" />


    </LinearLayout>

    <Button
        android:id="@+id/runButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:text="@string/CreateStart"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

위 코드에서 주목할 부분

NumberPicker 위젯

번호를 선택하기 위한 NumberPicker위젯
--> 숫자의 범위에 대한 부분은 kotlin코드에서

private val numberPicker: NumberPicker by lazy {
        findViewById<NumberPicker>(R.id.numberPicker)
    }

        numberPicker.minValue = 1
        numberPicker.maxValue = 45

와 같이 범위를 선택할 수 있다.
( 위 코드처럼 설정할 경우 1~45사이의 숫자를 선택할 수 있다. )

tools를 사용하여 코딩에 도움 받기

        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="1"
            android:background="@drawable/circle_blue"
            android:gravity="center"
            android:textColor="@color/white"
            android:textSize="18sp"
            android:textStyle="bold"
            android:visibility="gone"           <ㅡㅡ 이 부분
            tools:visibility="visible" />       <ㅡㅡ 이 부분

tools로 사용하는 속성앱 실행시에는 적용되지 않고, 안드로이드 스튜디오에서만 적용된다.

위의 경우처럼

앱 실행시에 visibility속성를 gone으로 하고 싶은 경우, 이렇게만 하면 안드로이드 스튜디오내에서도 gone으로 적용되기 때문에 해당 컴포넌트가 보이지 않는다.
이것은 코딩하는데 있어서 불편함으로 작용한다.

하지만 android를 사용한 속성으로 visibility를 gone으로 하고, tools를 사용한 속성으로 visibility를 visible로 설정하면
안드로이드 스튜디오에서는 visible로 적용되고, 앱 실행시에는 gone으로 적용되어서 안드로이드 스튜디오에서 코딩하는데 애로사항이 없어진다.


MainActivity.kt

package fastcampus.aop.part1.aop_part2_chapter02

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.NumberPicker
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible

class MainActivity : AppCompatActivity() {

    private val clearButton: Button by lazy{
        findViewById<Button>(R.id.clearButton)
    }

    private val addButton: Button by lazy {
        findViewById<Button>(R.id.addButton)
    }

    private val runButton: Button by lazy {
        findViewById<Button>(R.id.runButton)
    }

    private val numberPicker: NumberPicker by lazy {
        findViewById<NumberPicker>(R.id.numberPicker)
    }

    private val numberTextViewList: List<TextView> by lazy {
        listOf<TextView>(
            findViewById<TextView>(R.id.textView1),
            findViewById<TextView>(R.id.textView2),
            findViewById<TextView>(R.id.textView3),
            findViewById<TextView>(R.id.textView4),
            findViewById<TextView>(R.id.textView5),
            findViewById<TextView>(R.id.textView6)
        )
    }

    private var didRun = false
    // runButton이 눌렸는지 확인하는 변수 --> 눌렸으면 addButton이 실행되면 안되기 때문
    
    private val picNumberSet = hashSetOf<Int>()


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        numberPicker.minValue = 1
        numberPicker.maxValue = 45
        // 해당 NumberPicker로 선택할 수 있는 최소값과 최대값을 지정
        // 이제 이 사이의 값이 NumberPicker로 선택할 수 있는 범위가 됨

        initRunButton()
        initAddButton()
        initClearButton()
    }

    private fun initRunButton(){
        runButton.setOnClickListener{
            val list = getRandomNumber()
            didRun = true
            
            list.forEachIndexed { index, number ->
                val textView = numberTextViewList[index]

                textView.text = number.toString()
                textView.isVisible = true

                setNumberBackground(number,textView)

            }
            // forEach와 같이 앞쪽의 Collection의 내용들에 대해 하나하나씩 파라미터로 가져와서 뒤의 블록의 내용을 실행시키지만,
            // forEachIndexed의 경우 현재 내용이 몇번째 내용( index )인지에 대해서 까지 파라미터로 넘겨줌
        }
    }

    private fun initAddButton(){
        addButton.setOnClickListener {
            if(didRun){
                Toast.makeText(this,"초기화 후에 사용해주세요",Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            if (picNumberSet.size >= 5){
                Toast.makeText(this,"번호는 5개까지만 선택할 수 있습니다.",Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            if (picNumberSet.contains(numberPicker.value)){
                //numberPicker.value --> NumberPicker에 현재 선택된 값 리턴
                Toast.makeText(this,"이미 선택한 번호입니다.",Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            val textView = numberTextViewList[picNumberSet.size]
            textView.isVisible = true
            // 해당 textView를 visible하게 만듬
            textView.text = numberPicker.value.toString()


            setNumberBackground(numberPicker.value,textView)


            picNumberSet.add(numberPicker.value)

        }
    }

    private fun setNumberBackground(number: Int, textView: TextView){
        when (number){
            in 1..10 -> textView.background = ContextCompat.getDrawable(this, R.drawable.circle_yellow)
            in 11..20 -> textView.background = ContextCompat.getDrawable(this, R.drawable.circle_blue)
            in 21..30 -> textView.background = ContextCompat.getDrawable(this, R.drawable.circle_red)
            in 31..40 -> textView.background = ContextCompat.getDrawable(this, R.drawable.circle_gray)
            else -> textView.background = ContextCompat.getDrawable(this, R.drawable.circle_green)
        }
    }

    private fun initClearButton(){
        clearButton.setOnClickListener{
            picNumberSet.clear()
            numberTextViewList.forEach {
                it.isVisible = false
            }
            //forEach문으로 for문과 비슷함 
            // 앞에 Collection의 내용을 하나하나씩 파라미터로 가져와서 뒤의 블럭의 내용을 실행,, 현재 반복되고 있는 데이터는 it을 통해 접근 가능

            didRun = false
        }

    }

    private fun getRandomNumber(): List<Int>{

        val numberList = mutableListOf<Int>()
            .apply {
                for( i in 1..45){
                    if (picNumberSet.contains(i)){
                        continue
                    }
                    this.add(i)
                }

            }

        numberList.shuffle()
        val newList = picNumberSet.toList() + numberList.subList(0,6 - picNumberSet.size)

        return newList.sorted()
        // sorted()확장함수를 사용해주면 오름차순으로 정렬해줌

    }
}

위 코드에서 주목할 부분

lazy init을 이용해서 초기화하고 있는 부분

    private val clearButton: Button by lazy{
        findViewById<Button>(R.id.clearButton)
    }
    

--> Button 위젯을 lazy init을 통해 가져오고 초기화하고 있다.


여러 쓰임새가 같은 여러 위젯들을 xml에서 가져와서 사용하는 부분


   private val numberTextViewList: List<TextView> by lazy {
        listOf<TextView>(
            findViewById<TextView>(R.id.textView1),
            findViewById<TextView>(R.id.textView2),
            findViewById<TextView>(R.id.textView3),
            findViewById<TextView>(R.id.textView4),
            findViewById<TextView>(R.id.textView5),
            findViewById<TextView>(R.id.textView6)
        )
    }

--> 이 부분에서 해당 TextView들을 list에 한번에 가져오고 있다.

--> 이후 반복문 등을 사용하여 한번에 해당 위젯들을 제어하는 것

[ 해당 TextView 들 ]


NumberPicker를 가져와서 초기화하는 부분

    private val numberPicker: NumberPicker by lazy {
        findViewById<NumberPicker>(R.id.numberPicker)
    }

    
        numberPicker.minValue = 1
        numberPicker.maxValue = 45

--> 해당 NumberPicker로 선택할 수 있는 최소값과 최대값을 지정
이제 이 사이의 값이 NumberPicker로 선택할 수 있는 범위가 됨 ( 위의 경우 1~45사이의 숫자 )


각 위젯의 리스너설정을 모두 외부 함수로 빼서 사용 --> 표현함수로 사용 ( 리턴이 없는 함수 )

        initRunButton()
        initAddButton()
        initClearButton()
        

--> main 함수의 해당 코드들을 통해 버튼 위젯의 리스너 설정에 대한 함수를 적용시킴

--> 즉, 클릭 리스너 위한 표현함수를 만들어서 적용함 ( 가독성을 높임 )


forEach와 forEachIndexed

forEach 부분

 numberTextViewList.forEach {
                it.isVisible = false
            }

--> 해당 Collection의 데이터를 하나하나 가져와 뒤의 블록부분의 람다함수를 실행시키고 있음
( 가져온 데이터는 it으로 접근할 수 있음 )

forEachIndexed 부분

            list.forEachIndexed { index, number ->
                val textView = numberTextViewList[index]

                textView.text = number.toString()
                textView.isVisible = true

                setNumberBackground(number,textView)

            }
                 

--> 해당 Collection의 데이터를 하나하나 가져와 뒤의 블록부분의 람다함수를 실행시키는 것은 forEach와 동일하지만,
해당 데이터가 몇번째 순서인지를 나타내는 파라미터도 같이 받고 있음


ContextCompat.getDrawable()을 통해 res폴더의 drawable폴더에 접근하는 부분

  when (number){
            in 1..10 -> textView.background = ContextCompat.getDrawable(this, R.drawable.circle_yellow)
            in 11..20 -> textView.background = ContextCompat.getDrawable(this, R.drawable.circle_blue)
            in 21..30 -> textView.background = ContextCompat.getDrawable(this, R.drawable.circle_red)
            in 31..40 -> textView.background = ContextCompat.getDrawable(this, R.drawable.circle_gray)
            else -> textView.background = ContextCompat.getDrawable(this, R.drawable.circle_green)

--> drawable 값을 가져오는 부분인데 drawable이 안드로이드 앱에 저장된 것이기 때문에 Context에서 가져올 필요가 있음

1. ContextCompat을 이용해서 Context에 접근

2. 이후 앱의 Drawable값을 가져와야 하므로 getDrawable()함수를 이용해서 Drawable에 접근

getDrawable() 함수
첫번째 파라미터로 현재 위치를 받고,
두번째 파라미터로 DrawableResourceID 가 필요하므로 R에 drawable항목에 저장되어 있는 해당 ShapeDrawable의 주소를 넣어준다.

--> 현재 해당 코드에서 가져오고 있는 xml파일은 아래에 있는 Shape Drawable로 만든 파일이다.


apply 확장함수를 이용해서 초기화하는 부분

        val numberList = mutableListOf<Int>()
            .apply {
                for( i in 1..45){
                    if (picNumberSet.contains(i)){
                        continue
                    }
                    this.add(i)
                }

            }

--> apply() 확장함수는 앞에 객체를 뒤에 있는 블록으로 불러와 사용할 수 있게 해주는 함수이다.
또한 불러온 객체는 this 키워드를 통해접근할 수 있다.

--> 그래서 위의 코드에서는 mutableList를 블록으로 가져와서 for in 문을 이용하여
데이터를 넣어주고 있는 상황이다.


res폴더 안에 있는 drawable폴더 안에 있는 circle_blue.xml 파일

<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="oval"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="#5fb5db"/>
    <size android:height="44dp"
        android:width="44dp"/>

</shape>

Shape Drawable 방식으로 그린 파일이다.

--> Shape Drawable은 이런 간단한 background를 그리는 데 쓰인다.

--> 위에 ContextCompat.getDrawable()을 통해 불러온 파일 중 하나이며,
나머지는 색만 다르게 해서 만들어 놓았다.

아래와 같은 모양이다.

좋은 웹페이지 즐겨찾기