1) 구현기능

  • 계산기 UI 구현
  • 두개의 자연수에 대한 연산
  • +, - 연산
  • 액션바 제거
  • 다크 모드에서 색상값 변경

2) 학습 목표

  • 계산기 UI 를 그리고, 간단한 계산 기능을 구현할 수 있다
    • TableLayout이 아닌, ConstraintLayout-Flow 로 계산기 UI 그리기
    • 다크/라이트 모드에 따른 UI 그리기
  • UI
    • ConstraintLayout - Flow
    • style
    • color (Light/Dark)
    • theme
  • Kotlin
    • when
    • StringBuilder

3) 한 걸음 더

  • String VS StringBuilder 언제 쓰는게 좋을지 학습해보세요.
  • 계산기의 다양한 기능을 구현해보세요.
    • 사칙연산순서 우선순위 지정
    • 괄호
    • 소수점 표현

이번 시간에는 앱의 UI 를 꾸며보자.

앱의 상단 바를 액션바 라고 부른다.

 

 

 

  1. 계산기를 구현할 때는 액션바 가 필요 없으므로 없애고
  2. 테마에 따른 색상 설정을 해보자.

Settings (설정) → Display (화면) → Dark theme (야간모드)


1) 액션바 없애기

액션바 는 우리가 설정하기 전에도 이미 적용이 되어 있다.

manifest 에서 테마 라는 값이 있다.

 

AndroidManifest.xml

 

cmd+click 또는 ctrl+click 하여

themes.xml 파일을 열어보자.

themes.xml 에서 액션바 를 제거하기 위해서 아래 코드를 수정하자.

 

  • 수정 전

<stylename="Theme.Chapter5"parent="Theme.MaterialComponents.DayNight.DarkActionBar">

  • 수정 후

<stylename="Theme.Chapter5"parent="Theme.MaterialComponents.DayNight.NoActionBar">

 

 

또, 야간모드에서도 액션바를 없애자.

res > values > themes > themes.xml (night) 수정

 


equationTextView 와 resultTextView의

textColor 를 white 로 변경하자.

 

야간모드 (다크모드)일 때는

검정 배경이라 흰 글씨가 잘 보이지만

주간모드 (라이트모드) 이면 글씨가 보이지 않는다.

 

테마에 따라 색 변경이 필요하다.


2) 야간모드

야간모드 용 color 값을 추가하자.

 

File name 은 주간모드와 동일하게 “colors” 로 하고

이때, Directory name 에 “-night” 을 붙인다.

 

주간 모드에서는 기본글씨색을 검정으로 지정하고

<color name="defaultTextColor">#FF000000</color>

 

야간 모드에서는 기본글씨색을 흰색으로 지정하자

<color name="defaultTextColor">#FFFFFFFF</color>

 

이후 activity_main.xml 파일에서

equationTextView 와 resultTextView 의

textColor 를 defaultTextColor 로 변경해주자.

몇 가지 예외사항을 처리해보자.

case 1) 정수 범위를 벗어났을 때 ( bigDecimal )

숫자를 많이 입력했을 때,

계산 결과가 정수 범위를 벗어났을 때

결과 확인 시, 앱이 종료됨

 

LogCat 을 살펴보자.

넘버포멧익셉션.

Caused by: java.lang.NumberFormatException: For input string: "123456789123456789" at java.lang.Integer.parseInt(Integer.java:618)

 

integer 의 범위를 벗어나는 문제를 해결하기 위해 사용할 수 있는 것이

bigDecimal 이다.

소수점이 잘 처리되지 않는 부분, 부동 소수점 관련은 bigDecimal

 

자바와 코틀린에서 소수점을 정확히 계산하려면

integer, long, float, doouble 같은 primitive type 이 아닌

bigDecimal 타입을 사용해야 한다.


 

bigDecimal 형은

무한대에 가까운 아주 큰 수를 입력 받아도 잘 계산하며

소수점 계산도 정확하다.

 


아래 MainActivity.kt 코드에서

toInt() 를 toBigDecimal() 로 수정하자.

val firstNumber = firstNumberText.toString().toInt()
val secondNumber = secondNumberText.toString().toInt()
val firstNumber = firstNumberText.toString().toBigDecimal()
val secondNumber = secondNumberText.toString().toBigDecimal()

 

case 2) 맨 앞 숫자가 0 & 세 자리마다 끊기 ( DecimalFormat )

맨 앞 숫자가 0이면 0이 사라져야하고

세 자리마다 콤마(,)를 넣어 시안성을 높여보자.

 

decimalFormat 변수 선언 및 초기화

private val decimalFormat = DecimalFormat("#,###") 
// #.## 소수점까지 포맷 지정 가능

 

 

ui 업데이트 함수 수정

updateEquationTextView()

// ui 업데이트 함수
private fun updateEquationTextView() {
    val firstFormattedNumber = if(firstNumberText.isNotEmpty()) decimalFormat.format(firstNumberText.toString().toBigDecimal()) else ""
    val secondFormattedNumber = if(secondNumberText.isNotEmpty()) decimalFormat.format(secondNumberText.toString().toBigDecimal()) else ""

    binding.equationTextView.text = "$firstFormattedNumber $operatorText $secondFormattedNumber"
}

 

equalClicked 함수의 result 수정

equalClicked()

val result = when(operatorText.toString()){
    "+" -> decimalFormat.format(firstNumber + secondNumber)
    "-" -> decimalFormat.format(firstNumber - secondNumber)
    else -> "잘못된 수식 입니다."
}

 

두 가지 예외 처리를 해보았다.

다음 단원에서는 UI를 조금 더 꾸며보도록 하자.


3) 코드 및 결과

MainActivity.kt

package com.part1.chapter5

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.Toast
import com.part1.chapter5.databinding.ActivityMainBinding
import java.text.DecimalFormat

class MainActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMainBinding
    private val firstNumberText = StringBuilder("")
    private val secondNumberText = StringBuilder("")
    private val operatorText = StringBuilder("")
    private val decimalFormat = DecimalFormat("#,###") // #.## 소수점까지

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    // xml 에서 접근할 수 있어야 하므로, public 으로 지정
    // 매개변수를 View 로 받으므로, 반드시 매개변수 쓰기
    fun numberClicked(view : View) {
        // Log.d("numberClicked", "one")
        // button 에서 누른 숫자 추출
        // 모든 view 가 text 라는 attribute 를 가지고 있지 않다.
        // view 가 버튼이라면, text를 string 으로 변환
        // view 가 Null 이 아니면, text 값을 받아 string 형으로 변환
        // 버튼이 아니면 빈문자열 ""
        val numberString = (view as? Button)?.text?.toString() ?: ""

        // 첫 번째 숫자일지, 두 번째 숫자일지 확인
        // 첫 번째 숫자와 연산자가 없으면, 첫 번째 숫자
        val numberText = if(operatorText.isEmpty()) firstNumberText else secondNumberText

        numberText.append(numberString)

        // ui 업데이트
        updateEquationTextView()
    }

    fun clearClicked(view : View) {
        firstNumberText.clear()
        secondNumberText.clear()
        operatorText.clear()
        updateEquationTextView() // ui 업데이트
        binding.resultTextView.text =""
    }

    fun equalClicked(view : View) {
        if(firstNumberText.isEmpty() || secondNumberText.isEmpty() || operatorText.isEmpty()) {
            Toast.makeText(this, "올바르지 않은 수식 입니다.", Toast.LENGTH_SHORT).show()
            return
        }
        val firstNumber = firstNumberText.toString().toBigDecimal()
        val secondNumber = secondNumberText.toString().toBigDecimal()

        val result = when(operatorText.toString()){
            "+" -> decimalFormat.format(firstNumber + secondNumber)
            "-" -> decimalFormat.format(firstNumber - secondNumber)
            else -> "잘못된 수식 입니다."
        }
        binding.resultTextView.text = result

        updateEquationTextView()
    }

    fun operatorClicked(view : View) {
        val operatorString = (view as? Button)?.text?.toString() ?: ""

        // 첫 번째 숫자를 입력받지 못하면,
        if(firstNumberText.isEmpty()) {
            Toast.makeText(this, "먼저 숫자를 입력해주세요.", Toast.LENGTH_SHORT).show()
            return
        }

        // 두 번째 숫자까지 입력받았으면,
        if(secondNumberText.isNotEmpty()){
            Toast.makeText(this, "1개의 연산자에 대해서만 연산이 가능합니다.", Toast.LENGTH_SHORT).show()
            return
        }

        operatorText.append(operatorString)
        updateEquationTextView() // ui 업데이트
    }

    // ui 업데이트 함수
    private fun updateEquationTextView() {
        val firstFormattedNumber = if(firstNumberText.isNotEmpty()) decimalFormat.format(firstNumberText.toString().toBigDecimal()) else ""
        val secondFormattedNumber = if(secondNumberText.isNotEmpty()) decimalFormat.format(secondNumberText.toString().toBigDecimal()) else ""

        binding.equationTextView.text = "$firstFormattedNumber $operatorText $secondFormattedNumber"
    }

}

1) 수식창과 결과창 UI

키패드 위의 버튼을 눌렀을 때 입력값이 나오는 수식창과 결과창을 UI 로 그려보자.

activity_main.xml

<TextView
        android:id="@+id/equationTextView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="equation"
        android:gravity="end"
        android:textSize="30sp"
        android:padding="16dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/resultTextView"
        />

    <TextView
        android:id="@+id/resultTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="result"
        android:gravity="end"
        android:padding="16dp"
        android:textSize="36sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toTopOf="@id/keyPadFlow"/>

2) 기능 구현

2-1) gradle : app

    viewBinding {
        enabled = true
    }

2-2) MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMainBinding
    private val firstNumberText = StringBuilder("")
    private val secondNumberText = StringBuilder("")
    private val operatorText = StringBuilder("")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

값이 많이 변경될 때는 StringBuilder 를 사용하는 것이 효과적이다.


2-3) activity_main.xml

이전 실습과 다르게, 코틀린 파일에서 setonClickListener 를 사용하는 대신

xml 파일에서 onClick 을 지정해보자.

줄이 그어져 있는 것은, deprecated, 즉 언젠가 없어질 요소라는 의미이다.

그러나 여전히 onClick 은 자주 사용되는 요소이므로 학습해보자.

 

 

activity_main.xml

    <Button
		    ...
        android:id="@+id/button1"
        android:text="1"
        android:onClick="numberClicked"
        ... />

android:onClick="numberClicked"


2-4) MainActivity.kt

숫자, 연산자, clear, equal 버튼 클릭 시

동작시킬 함수를 각각 구현해보자.

numberClicked

fun numberClicked(view : View) {
    // Log.d("numberClicked", "one")
    // button 에서 누른 숫자 추출
    // 모든 view 가 text 라는 attribute 를 가지고 있지 않다.
    // view 가 버튼이라면, text를 string 으로 변환
    // view 가 Null 이 아니면, text 값을 받아 string 형으로 변환
    // 버튼이 아니면 빈문자열 ""
    val numberString = (view as? Button)?.text?.toString() ?: ""

    // 첫 번째 숫자일지, 두 번째 숫자일지 확인
    // 첫 번째 숫자와 연산자가 없으면, 첫 번째 숫자
    val numberText = if(operatorText.isEmpty()) firstNumberText else secondNumberText

    numberText.append(numberString)

    // ui 업데이트
    updateEquationTextView()
}

 

 

fun numberClicked(view : View)

xml 에서 접근할 수 있어야 하므로, public 으로 지정

매개변수를 View 로 받으므로, 반드시 매개변수 쓰기

 

 

clearClicked

fun clearClicked(view : View) {
        firstNumberText.clear()
        secondNumberText.clear()
        operatorText.clear()
        updateEquationTextView() // ui 업데이트
        binding.resultTextView.text =""
    }

 

 

equalClicked

fun equalClicked(view : View) {
    if(firstNumberText.isEmpty() || secondNumberText.isEmpty() || operatorText.isEmpty()) {
        Toast.makeText(this, "올바르지 않은 수식 입니다.", Toast.LENGTH_SHORT).show()
        return
    }
    val firstNumber = firstNumberText.toString().toInt()
    val secondNumber = secondNumberText.toString().toInt()

    val result = when(operatorText.toString()){
        "+" -> firstNumber + secondNumber
        "-" -> firstNumber - secondNumber
        else -> "잘못된 수식 입니다."
    }.toString() // 한 번에 toString() 으로 받기
    binding.resultTextView.text = result

    updateEquationTextView()
}

 

 

operatorClicked

fun operatorClicked(view : View) {
    val operatorString = (view as? Button)?.text?.toString() ?: ""

    // 첫 번째 숫자를 입력받지 못하면,
    if(firstNumberText.isEmpty()) {
        Toast.makeText(this, "먼저 숫자를 입력해주세요.", Toast.LENGTH_SHORT).show()
        return
    }

    // 두 번째 숫자까지 입력받았으면,
    if(secondNumberText.isNotEmpty()){
        Toast.makeText(this, "1개의 연산자에 대해서만 연산이 가능합니다.", Toast.LENGTH_SHORT).show()
        return
    }

    operatorText.append(operatorString)
    updateEquationTextView() // ui 업데이트
}

 

 

ui업데이트함수

// ui 업데이트 함수
private fun updateEquationTextView() {
    binding.equationTextView.text = "$firstNumberText $operatorText $secondNumberText"
}

3) 전체코드

MainActivity.kt

package com.part1.chapter5

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.Toast
import com.part1.chapter5.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMainBinding
    private val firstNumberText = StringBuilder("")
    private val secondNumberText = StringBuilder("")
    private val operatorText = StringBuilder("")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    // xml 에서 접근할 수 있어야 하므로, public 으로 지정
    // 매개변수를 View 로 받으므로, 반드시 매개변수 쓰기
    fun numberClicked(view : View) {
        // Log.d("numberClicked", "one")
        // button 에서 누른 숫자 추출
        // 모든 view 가 text 라는 attribute 를 가지고 있지 않다.
        // view 가 버튼이라면, text를 string 으로 변환
        // view 가 Null 이 아니면, text 값을 받아 string 형으로 변환
        // 버튼이 아니면 빈문자열 ""
        val numberString = (view as? Button)?.text?.toString() ?: ""

        // 첫 번째 숫자일지, 두 번째 숫자일지 확인
        // 첫 번째 숫자와 연산자가 없으면, 첫 번째 숫자
        val numberText = if(operatorText.isEmpty()) firstNumberText else secondNumberText

        numberText.append(numberString)

        // ui 업데이트
        updateEquationTextView()
    }

    fun clearClicked(view : View) {
        firstNumberText.clear()
        secondNumberText.clear()
        operatorText.clear()
        updateEquationTextView() // ui 업데이트
        binding.resultTextView.text =""
    }

    fun equalClicked(view : View) {
        if(firstNumberText.isEmpty() || secondNumberText.isEmpty() || operatorText.isEmpty()) {
            Toast.makeText(this, "올바르지 않은 수식 입니다.", Toast.LENGTH_SHORT).show()
            return
        }
        val firstNumber = firstNumberText.toString().toInt()
        val secondNumber = secondNumberText.toString().toInt()

        val result = when(operatorText.toString()){
            "+" -> firstNumber + secondNumber
            "-" -> firstNumber - secondNumber
            else -> "잘못된 수식 입니다."
        }.toString() // 한 번에 toString() 으로 받기
        binding.resultTextView.text = result

        updateEquationTextView()
    }

    fun operatorClicked(view : View) {
        val operatorString = (view as? Button)?.text?.toString() ?: ""

        // 첫 번째 숫자를 입력받지 못하면,
        if(firstNumberText.isEmpty()) {
            Toast.makeText(this, "먼저 숫자를 입력해주세요.", Toast.LENGTH_SHORT).show()
            return
        }

        // 두 번째 숫자까지 입력받았으면,
        if(secondNumberText.isNotEmpty()){
            Toast.makeText(this, "1개의 연산자에 대해서만 연산이 가능합니다.", Toast.LENGTH_SHORT).show()
            return
        }

        operatorText.append(operatorString)
        updateEquationTextView() // ui 업데이트
    }

    // ui 업데이트 함수
    private fun updateEquationTextView() {
        binding.equationTextView.text = "$firstNumberText $operatorText $secondNumberText"
    }

}

 

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="com.part1.chapter5.MainActivity">

    <TextView
        android:id="@+id/equationTextView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="equation"
        android:gravity="end"
        android:textSize="30sp"
        android:padding="16dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/resultTextView"
        />

    <TextView
        android:id="@+id/resultTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="result"
        android:gravity="end"
        android:padding="16dp"
        android:textSize="36sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toTopOf="@id/keyPadFlow"/>



    <!-- Flow -->
    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/keyPadFlow"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintHeight_percent="0.7"
        app:layout_constraintVertical_bias="1"
        app:flow_maxElementsWrap="4"
        app:flow_wrapMode="chain"
        android:padding="8dp"
        app:flow_horizontalGap="8dp"
        app:constraint_referenced_ids="button1, button2, button3, buttonClear, button4, button5, button6, buttonPlus, button7, button8, button9, buttonMinus
        ,button0, buttonEqual"
        />


    <Button
        android:id="@+id/button1"
        style="@style/numKeyPad"
        android:text="1"
        android:onClick="numberClicked"
        tools:ignore="MissingConstraints"/>

    <Button
        android:id="@+id/button2"
        style="@style/numKeyPad"
        android:text="2"
        android:onClick="numberClicked"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button3"
        style="@style/numKeyPad"
        android:text="3"
        android:onClick="numberClicked"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button4"
        style="@style/numKeyPad"
        android:text="4"
        android:onClick="numberClicked"
        tools:ignore="MissingConstraints"/>

    <Button
        android:id="@+id/button5"
        style="@style/numKeyPad"
        android:text="5"
        android:onClick="numberClicked"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button6"
        style="@style/numKeyPad"
        android:text="6"
        android:onClick="numberClicked"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button7"
        style="@style/numKeyPad"
        android:text="7"
        android:onClick="numberClicked"
        tools:ignore="MissingConstraints"/>

    <Button
        android:id="@+id/button8"
        style="@style/numKeyPad"
        android:text="8"
        android:onClick="numberClicked"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button9"
        style="@style/numKeyPad"
        android:text="9"
        android:onClick="numberClicked"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button0"
        style="@style/numKeyPad"
        android:text="0"
        android:onClick="numberClicked"
        app:layout_constraintHorizontal_weight="1"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/buttonEqual"
        style="@style/operatorKeypad"
        android:text="="
        android:onClick="equalClicked"
        app:layout_constraintHorizontal_weight="3"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/buttonClear"
        style="@style/operatorKeypad"
        android:text="C"
        android:onClick="clearClicked"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/buttonPlus"
        style="@style/operatorKeypad"
        android:text="+"
        android:onClick="operatorClicked"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/buttonMinus"
        style="@style/operatorKeypad"
        android:text="-"
        android:onClick="operatorClicked"
        tools:ignore="MissingConstraints" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

Constraint Layout 의 Flow

chapter5 프로젝트를 생성하자.

계산기를 만들 때, 많은 reference 들이 Table Layout 을 사용한다.

 

그러나 이 챕터에서는 Constraint Layout 의 Flow 를 사용할 것이다.

인터넷에 검색할 때, 안드로이드 Flow 를 키워드로 하면

데이터 흐름을 관리하는 Flow 에 대한 설명을 찾을 수도 있다.

 

우리는 Constraint Layout의 Flow 에 대해서 이야기하고 있는 것이다.

안드로이드 공식문서에서 Flow 를 찾아보자.

키워드는 다음과 같다.

Virtual Layout

similar to chain

배치를 할 때, constraint_referenced_ids 를 사용


1) VirtualLayout 의 장점

hierarchy 가 flat 하다.

뷰 구조를 flat 하게 만들 수 있다.

 

뷰의 레이아웃 계층 구조가 flat 할 수록

렌더링하는 속도 ( UI를 그리는 속도 ) 가 빨라진다.

 

Linear Layout 을 사용하면, UI 를 편하게 그릴 수 있다.

Constraint Layout 처럼 Constraint 관계를 따로 가지지 않아서

특히 쌓는 구조를 그릴 때 편리하다.

 

단, flat 한 계층구조 권장사항에 위배되기 때문에

유혹을 떨치고 Constraint Layout 으로 돌아오게 된다.

 

그런데 Constraint Layout 의 Flow 를 사용하면

Linear Layout 의 장점을 가져갈 수 있다.


2) Three Buttons 배치

Flow 가 무엇인지 알아보기 위해

3개의 버튼을 만들어보자.

 

버튼 스타일 통일을 위해

res 우클릭 > New > Android Resource File 에서

styles.xml 를 만들자.

 

styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="numKeyPad">
        <item name="android:textSize">40sp</item>
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
    </style>
</resources>

 

 

activity_main.xml

    <Button
        android:id="@+id/button1"
        style="@style/numKeyPad"
        android:text="1" />

    <Button
        android:id="@+id/button2"
        style="@style/numKeyPad"
        android:text="2" />

    <Button
        android:id="@+id/button3"
        style="@style/numKeyPad"
        android:text="3" />

Button 배치할 때, Expand 또는 Chain Mode 를 사용할 수 있다.


2-1) [배치] Expand

Organize > Expand Horizontally

 

결과

각 Button 내부에 constraint 관련 코드가 몇 줄 추가된다.

 


2-2) [배치] Chains

요소들 드래그 > Chains > Create Horizontal Chain

 

결과

각 Button 내부에 constraint 관련 코드가 몇 줄 추가된다.


horizontal 로만 constraint 를 지정해주었기 때문에,

vertical constraint 도 지정을 해주어야 한다.

 

이런식으로 쌓게되는 구조임에도 불구하고

복잡한 UI 를 constraint 으로 만들 수 밖에 없다.

 

이때 유용하게 사용할 수 있는 것이 Flow 이다.

내부 값을 최대한 사용하기 위해서

layout_width 와 layout_height 를 0dp 로 지정했다.

 

3) Flow 코드 추가 ( constraint_referenced_ids )

<!-- Flow -->
<androidx.constraintlayout.helper.widget.Flow
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:constraint_referenced_ids="button1, button2, button3"
    />
    
    <Button
    android:id="@+id/button1"
    style="@style/numKeyPad"
    android:text="1" />

 

결과

Button 에서 constraint 를 따로 지정해주지 않았는데

Flow 코드 추가를 하였더니, Button 들이 배치가 되었고

각 Button 내부에 constraint 관련 코드가 없다.

 

코드가 간결해진 것이다.

 

Flow 를 사용하므로 constraint 를 추가하지 않아도 된다는 것을 알려주는

missing constraints 무시하는 코드를 추가하자.

tools:ignore="MissingConstraints"


✽ styles.xml 수정

버튼이 떨어져있지 않고 붙어있었으면 좋겠으므로

styles.xml 에서 layout_width 와 layout_height 를

각각 parent 에서 0dp 로 변경하자.

 

styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="numKeyPad">
        <item name="android:textSize">40sp</item>
        <item name="android:layout_width">0dp</item>
        <item name="android:layout_height">0dp</item>
    </style>
</resources>

✽456 / 789 추가 ( flow_maxElementsWrap, flow_wrapMode )

이전에 버튼 1, 버튼 2, 버튼 3을 복사/붙이기 해서

456/789 버튼들을 만들어보자.

 

이렇게 버튼을 만들게 되면 우려스러운 점이

계속 가로로 쌓을 수 있으려면

Flow 가 줄(행) 별로 있어야 한다.

 

Flow 는 순서에 따라 배치가 된다.

app:constraint_referenced_ids="button1, button3, button2"

이렇다면 버튼 순서는 1, 3, 2

 

이전에 버튼 1, 2, 3 만 flow 에 있었다면

버튼 4, 5, 6, 7, 8, 9 모두 flow 에 추가해준다.

 

app:constraint_referenced_ids="button1, button2, button3, button4, button5, button6, button7, button8, button9"

그럼 한 행에 최대 몇 개의 UI 요소를 배치할 지

지정하지 않았기 때문에 아래와 같이 보인다.

 

 

이제 한 행에 배치할 뷰의 최대 개수인 flow_maxElementsWrap와

이를 적용하기 위해 반드시 지정해야하는 flow_wrapMode 를 지정하자.

flow_wrapMode 의 기본값은 none 이다.

app:flow_maxElementsWrap="3"

app:flow_wrapMode="aligned"

 


[flow_wrapMode ] aligned 와 chain, 그리고 chain2

aligned 와 chain 의 차이를 알기 위해서

1번 버튼을 지워보았다.


✽ gap

우리는 aligned 를 옵션으로 할 것이다.

가로로 뷰들 사이 간격을 주기 위해서 horizontal gap 을 지정하자.

app:flow_horizontalGap="8dp"

 

 

계산기 키패드 높이를 줄여보자.

app:layout_constraintHeight_percent="0.7"

 

 

또 계산기 키패드를 아래로 내리자.

app:layout_constraintVertical_bias="1"

 

 

숫자 키패드 색을 변경해보자.

styles.xml

<item name="backgroundTint">@color/teal_200</item>

 

backgroundTint 를 이용하면, 원래 있던 버튼의 색깔만 변경할 수 있다.

<item name="backgroundColor" 값이 아닌

backgroundTint 를 지정해주었다.

 


✽ 나머지 버튼 추가

0버튼과 =버튼도 추가해보자.

=버튼 가로길이가 0버튼의 가로길이의 2배가 되도록 아래와 같은 옵션 추가.

layout_constraintHorizontal_weight
    <Button
		    ...
		    android:text="0"
        app:layout_constraintHorizontal_weight="1"/>

    <Button
        android:id="@+id/buttonEqual"
        style="@style/numKeyPad"
        android:text="="
        app:layout_constraintHorizontal_weight="2"
        tools:ignore="MissingConstraints" />

이제 clear버튼과 +버튼, -버튼을 추가해야한다.

 

위 버튼은 3버튼, 6버튼, 9버튼 오른쪽에 추가하고 싶으므로,

행 최고 요소 개수를 4로 변경하자.

app:flow_maxElementsWrap="4"

app:constraint_referenced_ids= "button1, button2, button3, buttonClear, 
button4, button5, button6, buttonPlus, 
button7, button8, button9, buttonMinus
,button0, buttonEqual"

 

 

행 내 최대 4개 요소로 변경되었으므로

weight 를 1:3 으로 변경하자.

    <Button
		    ...
		    android:text="0"
        app:layout_constraintHorizontal_weight="1"/>

    <Button
        android:id="@+id/buttonEqual"
        style="@style/numKeyPad"
        android:text="="
        app:layout_constraintHorizontal_weight="3"
        tools:ignore="MissingConstraints" />

 

 

마지막으로 연산자 버튼들만 색을 변경해보자.

styles.xml

<style name="operatorKeypad" parent="numKeyPad">
    <item name="backgroundTint">@color/teal_700</item>
</style>


3) 전체 코드

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">

    <!-- Flow -->
    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/keyPadFlow"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintHeight_percent="0.7"
        app:layout_constraintVertical_bias="1"
        app:flow_maxElementsWrap="4"
        app:flow_wrapMode="chain"
        android:padding="8dp"
        app:flow_horizontalGap="8dp"
        app:constraint_referenced_ids="button1, button2, button3, buttonClear, button4, button5, button6, buttonPlus, button7, button8, button9, buttonMinus
        ,button0, buttonEqual"
        />


    <Button
        android:id="@+id/button1"
        style="@style/numKeyPad"
        android:text="1"
        tools:ignore="MissingConstraints"/>

    <Button
        android:id="@+id/button2"
        style="@style/numKeyPad"
        android:text="2"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button3"
        style="@style/numKeyPad"
        android:text="3"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button4"
        style="@style/numKeyPad"
        android:text="4"
        tools:ignore="MissingConstraints"/>

    <Button
        android:id="@+id/button5"
        style="@style/numKeyPad"
        android:text="5"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button6"
        style="@style/numKeyPad"
        android:text="6"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button7"
        style="@style/numKeyPad"
        android:text="7"
        tools:ignore="MissingConstraints"/>

    <Button
        android:id="@+id/button8"
        style="@style/numKeyPad"
        android:text="8"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button9"
        style="@style/numKeyPad"
        android:text="9"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/button0"
        style="@style/numKeyPad"
        android:text="0"
        app:layout_constraintHorizontal_weight="1"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/buttonEqual"
        style="@style/operatorKeypad"
        android:text="="
        app:layout_constraintHorizontal_weight="3"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/buttonClear"
        style="@style/operatorKeypad"
        android:text="C"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/buttonPlus"
        style="@style/operatorKeypad"
        android:text="+"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/buttonMinus"
        style="@style/operatorKeypad"
        android:text="-"
        tools:ignore="MissingConstraints" />

</androidx.constraintlayout.widget.ConstraintLayout>

계산기 앱

+, - 연산 및 숫자 포맷팅

다크 테마 대응


1) 구현기능

  • 계산기 UI 구현
  • 두개의 자연수에 대한 연산
  • +, - 연산
  • 액션바 제거
  • 다크 모드에서 색상값 변경

2) 학습 목표

  • 계산기 UI 를 그리고, 간단한 계산 기능을 구현할 수 있다
    • TableLayout이 아닌, ConstraintLayout-Flow 로 계산기 UI 그리기
    • 다크/라이트 모드에 따른 UI 그리기
  • UI
    • ConstraintLayout - Flow
    • style
    • color (Light/Dark)
    • theme
  • Kotlin
    • when
    • StringBuilder

1) 학습내용

  • 다양한 위젯을 사용하고, 테마를 이용하여 일관성 있는 UI 를 그릴 수 있다.
    • ConstraintLayout 과 다양한 위젯을 활용
    • Style 을 통한 일관성 있는 UI 구성
    • Adapter 에 대한 이해
    • 간단한 데이터 저장
    • 다른 화면으로 이동
    • 다른 앱 실행
  • UI
    • ConstraintLayout
      • guideLine
    • Sytle
    • Theme
    • ImageView
    • RadioButton
    • CheckBox
    • Spinner
    • DatePickerDialog
  • Kotlin
    • const val
    • with
  • Android

2) 한 걸음 더

  1. Intent 로 할 수 있는 다양한 동작에 대해 학습해보세요
  2. Manifest 에 들어가는 다양한 값들에 대해 학습해보세요
  3. Kotlin 의 다양한 ScopeFunction 에 대해 학습해보세요
  4. 체크박스 선택 및 미선택 시 글자 위치 동일하게 만들기

 


Spinner
데이터가 컬렉선, 리스트로 이루어져있을 때
리스트에 있는 정보를 나타내기 위해
UI 요소를 하나하나 그리는 것이 아니고

데이터와 UI 요소를 합칠 수 있게
리스트가 잘 보일 수 있게 하는 
Adapter 를 사용해보았다.

 

const val
상수 선언

 

Manifest
안드로이드 앱의 개략적인 개요. Activity 는 꼭 추가해야 함

 

Intent
Activity 에서 다른 Activity 로 전환
데이터 전송
전화 앱 실행
암시적 Intent, 명시적 Intent

 

 

SharedPreference
SharedPreference 는 데이터를 저장하는 요소.
파일 형태로 데이터를 저장함

키는 파일 이름을 지정
다양한 primitive type ( String, Boolean, Integer)
다양한 array 값 저장 가능

저장한 후, 반드시 edit 하는 곳에서 apply() 또는 commit() 을 호출한다.
Thread 의 block 여부가
apply와 commit 의 차이.

 

 

Toast
warning 문구 또는 완료 상태를 알려주기 위해 text 를 보여준다.

1) phone 버튼 추가

MainActivity.kt

<TextView
    android:id="@+id/emergencyContactValueTextView"
    style="@style/Value"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="010-0000-0000"
    android:layout_marginEnd="8dp"
    app:layout_constraintBaseline_toBaselineOf="@id/emergencyContactTextView"
    app:layout_constraintEnd_toStartOf="@+id/phoneImageView"
    app:layout_constraintHorizontal_bias="0.0"
    app:layout_constraintStart_toStartOf="@id/guideLine" />

<ImageView
    android:id="@+id/phoneImageView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_baseline_call_24"
    app:layout_constraintBottom_toBottomOf="@id/emergencyContactValueTextView"
    app:layout_constraintEnd_toEndOf="@id/nameValueTextView"
    app:layout_constraintTop_toTopOf="@id/emergencyContactValueTextView" />
    

2) 레이어 추가

Add helpers > Layer

Layer 로 emergencyContactValueTextView 와 phoneImageView 묶기

<androidx.constraintlayout.helper.widget.Layer
    android:id="@+id/emergencyContactLayer"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:constraint_referenced_ids="emergencyContactValueTextView,phoneImageView"
    tools:ignore="MissingConstraints"
    tools:layout_editor_absoluteX="165dp"
    tools:layout_editor_absoluteY="247dp" />

3) 통화기능 추가 ( 암시적 intent )

명시적 intent

어느 화면으로 이동할 것인지, Activity 를 명명해준 경우

binding.goInputActivityButton.setOnClickListener 
    val intent = Intent(this, InputActivity::class.java)
    startActivity(intent)
}

암시적 intent

전화를 할 수 있는 앱은 여러가지가 있다.

따라서 전화를 할 수 있는 앱을 실행하도록 해줘. 라고 할 것이다.

binding.emergencyContactLayer.setOnClickListener {
        with(Intent(Intent.ACTION_VIEW)) {
            val phoneNumber = binding.emergencyContactTextView.text.toString()
                .replace("-","")
            data = Uri.parse("tel:$phoneNumber")
            startActivity(this)
        }
}

4) 실행화면

 

 

 


5) 추가구현할 내용

체크박스 선택 및 미선택 시 글자 위치 동일하게 만들기

+ Recent posts