Android/TOYTOY

NumberPicker와 Collection 사용한 로또 번호 추첨기

sh1mj1 2022. 9. 1. 23:18

뷰 레이아웃 구상

이 포스팅은 https://fastcampus.co.kr/dev_online_iosappfinal 을 참고하여 만들어졌습니다.

참고한 내용

https://developer.android.com/reference/android/widget/NumberPicker

https://aries574.tistory.com/217

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.random/-random/

https://kotlinlang.org/docs/collections-overview.html

 

 

NumberPicker 와 Collection 을 사용하여 로또 번호 추첨기를 만든다.

0 개에서 5개까지 번호를 수동 선택 가능하도록 구현했다. 수동 선택한 번호를 제외하고 나머지 번호는 랜덤으로 표시된다.

안드로이드에서 제공하는 NumberPicker 라는 클래스를 사용하여 사용자가 직접 번호를 선택하여 추가할 수 있게 만든다. 번호는 중복되면 안된다.

만약 직접 번호를 선택하지 않고 자동으로 번호를 생성하고 싶다면 랜덤한 수를 생성한다. 이 때 많은 로직이 필요할 것이다.

번호 추가 버튼으로 번호를 추가하고 초기화 버튼으로 LinearLayout 안에 번호 초기화.

자동 생성 시작 버튼으로 남은 번호를 랜덤으로 생성한다.

알고리즘 결정

로또 번호 생성기의 가장 핵심 로직은 랜덤한 수를 생성하는 것이다.

일단은 코딩을 바로 들어가기 전에 로또 번호를 추첨할 때 어떤 알고리즘으로 번호를 랜덤 생성할지 결정해보자.

random 이라는 코틀린 추상 클래스를 사용할 것이다.

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.random/-random/

[Random - Kotlin Programming Language

kotlinlang.org](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.random/-random/)

코틀린 정식 문서에서는 아래처럼 말한다.

An abstract class that is implemented by random number generator algorithms.

The companion object Random.Default is the default instance of Random.

To get a seeded instance of random generator use Random function.

랜덤 수 생성 알고리즘에 의해 구현된 추상 클래스이다.

companion object인 Random.Default 가 Random의 기본 인스턴스이다.

랜덤 생성기의 시드 인스턴스를 가져오려면 랜덤 함수를 사용한다.

또한 랜덤 수를 생성해내는 방식은 나노 세컨드 단위로 seed 값을 받고 그 값으로 난수를 만들어 낸다고 합니다..

1. for 반복문을 사용.

import java.util.*

fun main() {

    val random = Random()

    // 이 때 중복 숫자가 나오는 문제 발생
    for (i in 1..6){
        println("${random.nextInt(45) + 1}")        
    }

결과

4 30 29 23 15 35

하지만 물론 코드를 Run 할 때마다 결과가 달라서 위 경우에는 중복되지 않았지만 중복 숫자가 나올 수 있다.

2. List 로 구현

빈 List 을 만들고 그 List 에 반복문을 이용해서 하나씩 넣어준다.

이 때 list 에 이미 중복되는 숫자가 있다면 넣어주지 않는다. 이것은 조건문과 continue 을 사용한다.

// list는 중복 허용, But Set 은 중복 허용하지 않음. 
    val list = mutableListOf<Int>()
    while(list.size <6) {
        val randomNumber = random.nextInt(45) + 1
        if(list.contains(randomNumber)) {
            continue
        }
        list.add(randomNumber)
    }
    println("List 로 표현: $list")
}

3. Set 으로 구현

Set 으로 구현했을 경우, Set 은 그 자체로 중복을 허용하지 않기 때문에 조건문이 하나 더 필요하지 않다는 장점이 있다.

    // Set 으로 구현한다면
    val numberSet =  mutableSetOf<Int>()

    while(numberSet.size < 6){
        val randomNumber = random.nextInt(45) + 1
        numberSet.add(randomNumber)
    }
    println("Set 으로 표현: $numberSet")

4. List의 shuffle 과 subList 을 사용해서 구현

List 에 1부터 46까지 넣어넣고 List 자체를 섞은 후 앞에 6개만 가져가는 방식도 사용할 수 있다.

    val list2 = mutableListOf<Int>().apply{
        for (i in 1..45){
            this.add(i)
        }
    }
    list2.shuffle()
    println(list2.subList(0,6))

알고리즘은 무조건 좋은 것은 없다.

더 최적화된 알고리즘이 있지만 보기 어려운 코드일 수도 있고, 더 보기 쉬운 코드지만 더 시간이 오래 걸릴 수도 있다. 항상 상황에 맞는 알고리즘을 사용하고 목적에 맞게 Refactoring 해야 함을 잊지 말자.

이번에는 3번을 채택하여 구현할 것이다.

NumberPicker

A widget that enables the user to select a number from a predefined range.
미리 정의된 범위로부터 사용자가 수를 선택할 수 있는 위젯이다. 공식문서에서는 LinearLayout 을 상속받고 있다고 한다.

 

 

화면 구성

ConstraintLayout 안에 NumberPicker, Button, LinearLayout 을 사용하였다.

LinearLayout 안에는 TextView 을 두었고 해당 Button의 background 속성으로 커스텀하여 만든 drawable을 지정해주었다.

화면 구성

아래 화면 구성 XML 코드 보기 

더보기

 

```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/numberPickerNp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.2"
        />

    <Button
        android:id="@+id/addBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="10dp"
        android:text="@string/addNum"
        app:layout_constraintEnd_toStartOf="@+id/clearBtn"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/numberPickerNp" />

    <Button
        android:id="@+id/clearBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="@string/clear"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/addBtn"
        app:layout_constraintTop_toBottomOf="@+id/numberPickerNp" />

    <Button
        android:id="@+id/runBtn"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="10dp"
        android:text="@string/autoGenNum"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"

        />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="8dp"
        android:gravity="center"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/addBtn">

        <TextView
            android:id="@+id/generated_Num0_Tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:background="@drawable/circle_blue"
            android:gravity="center"
            android:textSize="20sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:text="25"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/generated_Num1_Tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:background="@drawable/circle_gray"
            android:gravity="center"
            android:textSize="20sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:text="25"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/generated_Num2_Tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:background="@drawable/circle_red"
            android:gravity="center"
            android:textSize="20sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:text="25"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/generated_Num3_Tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:background="@drawable/circle_green"
            android:gravity="center"
            android:textSize="20sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:text="25"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/generated_Num4_Tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:background="@drawable/circle_yellow"
            android:gravity="center"
            android:textSize="20sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:text="25"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/generated_Num5_Tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:gravity="center"
            android:background="@drawable/circle_yellow"
            android:textSize="20sp"
            android:textStyle="bold"
            android:visibility="gone"
            tools:text="25"
            tools:visibility="visible" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
```

 

각 로또 번호가 표시되는 TextView은 따로 shapeDrawable 을 만들어준다.

res/drawable 안에 만들어준다.

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

    <solid android:color="@color/blue" />

    <size
        android:width="44dp"
        android:height="44dp" />

</shape>

아래는 textView 의 배경을 조건에 따라 설정해주는 함수이다.

프로그래밍적으로 TextView의 background을 drawable 로 설정하기

private fun setNumBackground(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)
            }

    }

 

 

LifeCycle

numberPicker 의 최소, 최대값을 설정해준다. 로또 번호는 1 ~ 45 까지임.

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

        numberPicker.minValue = 1
        numberPicker.maxValue = 45
        initRunBtn()
        initAddBtn()
        initClearBtn()
    }

initRunBtn(), initAddBtn(), initClearBtn() 으로 각 버튼에 대한 이벤트를 onCreate() 시 설정해준다.

 

 

함수 설정

initRunBtn()

// Mark -Run Button/////////////////////////////////////////
    private fun initRunBtn() {
        runBtn.setOnClickListener {
            val list = getRandomNumber()
            didRun = true
            list.forEachIndexed { index, num ->
                val textView = numberTextViewList[index]

                textView.text = num.toString()
                textView.isVisible = true
                setNumBackground(num, textView)
            }
            Log.d("MainActivity", list.toString())
        }
    }

    private fun getRandomNumber(): List<Int> {
        val numberList = mutableListOf<Int>().apply {
            for (i in 1..45) {
                if(pickedNumSet.contains(i)){
                    continue
                }
                this.add(i)
            }
        }
        numberList.shuffle()
        val newList = pickedNumSet.toList() + numberList.subList(0, 6- pickedNumSet.size)
//        newList.sort  // sort은 어떤 collection 일 때만 가능한 건가? sorted 은 되는데 sort 은 안댐.
        return newList.sorted()
    }

getRandomNumber() 함수는 List을 리턴.

먼저 numberList 라는 리스트에 1부터 45까지의 수를 집어넣는다. 그런데 만약 이전에 추가한 번호가 있다면 그 번호는 건너 뛴다.

그리고 나서 이 리스트를 랜덤 순서로 섞는다.

그 후 이미 선택한 번호들을 List로 바꾸고, 방금 만든 numberList 을 0부터 몇 개를 List로 뽑아서 합친다. 이 때 리턴되는 newList는 6개의 요소만을 가져야 하므로

 numberList.subList(0, 6 - pickedNumSet.size)

initRun() 함수는 리턴이 없다.

runBtn 을 누르면 위 함수를 실행. 반환된 list을 인덱스에 맞춰 해당 textView 에 텍스트를 넣어주고, 배경색도 수에 맞춰서 설정해준다.

initAddBtn()

private fun initAddBtn() {
        addBtn.setOnClickListener {
            if (didRun) {
                Toast.makeText(this, "초기화 후 시도해 주세요", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            if (pickedNumSet.size >= 6) {
                Toast.makeText(this, "번호는 여섯개까지 선택 가능합니다.", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            if (pickedNumSet.contains(numberPicker.value)) {
                Toast.makeText(this, "이미 선택한 번호입니다..", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            val textView = numberTextViewList[pickedNumSet.size]
            textView.isVisible = true
            textView.text = numberPicker.value.toString()
            pickedNumSet.add(numberPicker.value)

            setNumBackground(numberPicker.value, textView)

        }
    }
  1. 이미 번호 자동 생성했을 시 → 토스트 메시지와 리턴
  2. 직접 선택으로 번호를 6개 골랐을 시 → 토스트 메시지, 리턴
  3. 이미 선택한 번호를 또 직접 선택 시 → 토스트 메시지, 리턴

만약 위 세가지 경우가 아니라면 추가해 주어야 한다.

이미 pickedNumSet 에 추가된 직접 선택 번호 수를 계산해서 그 다음 textView 에 수를 추가해주고, pickedNumSet 에 방금 직접 선택한 번호를 추가해준다. 그리고 배경도 번호에 맞춰 설정해줌.

initClearBtn()

private fun initClearBtn() {
        clearBtn.setOnClickListener {
            numberTextViewList.forEach {
                it.isVisible = false
            }
            pickedNumSet.clear()

            didRun = false
        }
    }

전체 Textview을 안 보이게 하고, 집합 비우기, 그리고 didRun = false 해준다.

 

  • MainActivity 전체 코드
package com.example.lotto_02

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
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
import org.w3c.dom.Text

class MainActivity : AppCompatActivity() {

    // Mark -Properties/////////////////////////////////////////
    private val clearBtn: Button bylazy{
findViewById<Button>(R.id.clearBtn)
}
private val addBtn: Button bylazy{
findViewById(R.id.addBtn)
}
private val runBtn: Button bylazy{
findViewById(R.id.runBtn)
}
private val numberPicker: NumberPicker bylazy{
findViewById(R.id.numberPickerNp)
}
private var didRun = false
    private val pickedNumSet =hashSetOf<Int>()
//    private var pickedNumList = List<Int>()
    private val numberTextViewList: List<TextView> bylazy{
listOf<TextView>(
            findViewById(R.id.generated_Num0_Tv),
            findViewById(R.id.generated_Num1_Tv),
            findViewById(R.id.generated_Num2_Tv),
            findViewById(R.id.generated_Num3_Tv),
            findViewById(R.id.generated_Num4_Tv),
            findViewById(R.id.generated_Num5_Tv)

        )
}

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

        numberPicker.minValue= 1
        numberPicker.maxValue= 45
        initRunBtn()
        initAddBtn()
        initClearBtn()
    }

    // Mark -Run Button/////////////////////////////////////////
    private fun initRunBtn() {
        runBtn.setOnClickListener{
val list = getRandomNumber()
            didRun = true
            list.forEachIndexed{index, num->
val textView = numberTextViewList[index]

                textView.text= num.toString()
                textView.isVisible= true
                setNumBackground(num, textView)
}
Log.d("MainActivity", list.toString())
}
}

    private fun getRandomNumber(): List<Int> {
        val numberList =mutableListOf<Int>().apply{
for (i in 1..45) {
                if(pickedNumSet.contains(i)){
                    continue
                }
                this.add(i)
            }
}
numberList.shuffle()
        val newList = pickedNumSet.toList() + numberList.subList(0, 6- pickedNumSet.size)
//        newList.sort  // sort은 어떤 collection 일 때만 가능한 건가? sorted 은 되는데 sort 은 안댐.
        return newList.sorted()
    }

    // Mark -Add Button/////////////////////////////////////////
    private fun initAddBtn() {
        addBtn.setOnClickListener{
if (didRun) {
                Toast.makeText(this, "초기화 후 시도해 주세요", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            if (pickedNumSet.size >= 6) {
                Toast.makeText(this, "번호는 여섯개까지 선택 가능합니다.", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            if (pickedNumSet.contains(numberPicker.value)) {
                Toast.makeText(this, "이미 선택한 번호입니다..", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            val textView = numberTextViewList[pickedNumSet.size]
            textView.isVisible= true
            textView.text= numberPicker.value.toString()
            pickedNumSet.add(numberPicker.value)

            // can set backgroundColor in kotlin file
//            textView.background = ContextCompat.getDrawable(this, R.drawable.circle_blue)
            setNumBackground(numberPicker.value, textView)

}
}

    // Mark -Clear Button/////////////////////////////////////////
    private fun initClearBtn() {
        clearBtn.setOnClickListener{
numberTextViewList.forEach{
                it.isVisible= false
}
pickedNumSet.clear()

            didRun = false
}
}

    private fun setNumBackground(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)
            }

    }
}

 

 

문제점

  1. 현재 직접 선택했을 때 로또 번호가 순서가 이상하게 된다. 43, 38 , 1 을 선택했을 때 선택하면서 번호가 오름차순으로 정렬되는 것이 아니라 선택한 순서로 화면에 뜨고 있다. 이를 고쳐야 한다.
  2. NumberPicker 에 현재 수를 터치했을 시 직접 입력을 할 수 있게 되는데 이 직접 입력을 한후 바로 번호 추가하기 버튼을 눌렀을 시 입력한 수가 추가되지 않고 입력 전 수가 추가된다.

이 문제도 해결하여 추가로 업로드할 예정이다. (2022.08.18)