NumberPicker와 Collection 사용한 로또 번호 추첨기
이 포스팅은 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 안에 번호 초기화.
자동 생성 시작 버튼으로 남은 번호를 랜덤으로 생성한다.
- Kotlin 문법
- Random
- apply (람다함수 자세히 보기)
- Collection (Set, List ..)
- Layout
- ConstraintLayout (ConstraintLayout 자세히 보기)
- NumberPicker
- ShapeDrawable 사용
알고리즘 결정
로또 번호 생성기의 가장 핵심 로직은 랜덤한 수를 생성하는 것이다.
일단은 코딩을 바로 들어가기 전에 로또 번호를 추첨할 때 어떤 알고리즘으로 번호를 랜덤 생성할지 결정해보자.
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)
}
}
- 이미 번호 자동 생성했을 시 → 토스트 메시지와 리턴
- 직접 선택으로 번호를 6개 골랐을 시 → 토스트 메시지, 리턴
- 이미 선택한 번호를 또 직접 선택 시 → 토스트 메시지, 리턴
만약 위 세가지 경우가 아니라면 추가해 주어야 한다.
이미 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)
}
}
}
문제점
- 현재 직접 선택했을 때 로또 번호가 순서가 이상하게 된다. 43, 38 , 1 을 선택했을 때 선택하면서 번호가 오름차순으로 정렬되는 것이 아니라 선택한 순서로 화면에 뜨고 있다. 이를 고쳐야 한다.
- NumberPicker 에 현재 수를 터치했을 시 직접 입력을 할 수 있게 되는데 이 직접 입력을 한후 바로 번호 추가하기 버튼을 눌렀을 시 입력한 수가 추가되지 않고 입력 전 수가 추가된다.
이 문제도 해결하여 추가로 업로드할 예정이다. (2022.08.18)