1) Activity Life Cycle

화면을 전환하니 cm 와 m 가 변경된다.

 

화면전환 시 데이터를 유지하기 위해서는

Activity Life Cycle 을 알아보아야 한다.

 

 

각 콜백이 언제 호출되는 지

여러가지 동작을 해보면서

앱을 껐다가 키거나

홈 버튼을 눌러서 다른 앱을 실행했다가 다시 오거나

뒤로가기 버튼을 누를 때

여러 화면을 전환할 때

방향을 전환 할 때

 

등등 다양한 활동을 할 때

어떻게 콜백이 호출되는 지 알아야

어떤 시점에 어떤 처리를 할 지 결정할 수 있다.

 

 

화면 방향 전환에 따른 Life Cycle 을 체킹하고

적절한 조치를 취함으로써 데이터를 유지하는 작업을 해보자.


2) Activity State 변경 처리

2-2) 구성 변경 발생 ( configuration change )

세로 모드와 가로 모드 간 방향 변경이 있을 때.

원래 Activity Instance 에서는 onPause(), onStop(), onDestroy() 콜백이 호출된다.

 

Activity의 새로운 인스턴스가 생성되면

이 인스턴스에는 onCreate(), onStart(), onResume() 콜백이 호출된다.


2-2) 임시 UI 상태 저장 및 복원

임시 UI 상태 저장하는 방법에는 3가지가 있다.

ViewModel, onSaveInstanceState(), 로컬 저장소 결합

 

 


2-2-1) UI 상태 저장 - onSaveInstanceState()

onSaveInstanceState() 를 이용해보자.

 

outState.putBoolean("cmToM", cmToM)

    // UI 상태 저장
    override fun onSaveInstanceState(outState: Bundle) {
        // outState 에 값 저장
        outState.putBoolean("cmToM", cmToM)
        super.onSaveInstanceState(outState,)
    }

2-2-2) UI 상태 복원

cmToM = savedInstanceState.getBoolean("cmToM")

    // UI 샹태 복원
    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        cmToM = savedInstanceState.getBoolean("cmToM")

        // m/cm 글자 변경
        binding.inputUnitTextView.text = if(cmToM) "cm" else "m"
        binding.outputUnitTextView.text = if(cmToM) "m" else "cm"

        super.onRestoreInstanceState(savedInstanceState)
    }

숫자와 단위 모두 그대로 유지됨을 확인할 수 있다.

 

 


3) 전체 코드

MainActivity.kt

package com.part1.chapter3

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.PersistableBundle
import android.util.Log
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import com.part1.chapter3.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    // 바인딩 변수 선언
    private lateinit var binding : ActivityMainBinding
    var inputNumber : Int = 0  // 클래스 멤버 변수
    var cmToM = true  // 클래스 멤버 변수

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // binding 변수에 instance 할당
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        // 뷰 참조 변수들 선언
        val outputTextView = binding.outputTextView
        val outputUnitTextView = binding.outputUnitTextView
        val inputEditText = binding.inputEditText
        val inputUnitTextView = binding.inputUnitTextView
        val swapImageButton = binding.swapImageButton

        // 문자열이 변경되면 알려주는 리스너
        inputEditText.addTextChangedListener { text ->
            inputNumber = if(text.isNullOrEmpty()) {
                0
            } else {
                text.toString().toInt()
            }

            // cm -> m
            if(cmToM) {
                outputTextView.text = inputNumber.times(0.01).toString()
            }else{
                outputTextView.text = inputNumber.times(100).toString()
            }
        }

        // 단위 변환 버튼 클릭 시 처리
        swapImageButton.setOnClickListener {
            cmToM = cmToM.not() // !cmToM

            if (cmToM) {
                inputUnitTextView.text = "cm"
                outputUnitTextView.text = "m"
                outputTextView.text = inputNumber.times(0.01).toString()
            } else {
                inputUnitTextView.text = "m"
                outputUnitTextView.text = "cm"
                outputTextView.text = inputNumber.times(100).toString()
            }
        }
    }

    // UI 상태 저장
    override fun onSaveInstanceState(outState: Bundle) {
        // outState 에 값 저장
        outState.putBoolean("cmToM", cmToM)
        super.onSaveInstanceState(outState)
    }

    // UI 상태 복원
    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        cmToM = savedInstanceState.getBoolean("cmToM")

        // m/cm 글자 변경
        binding.inputUnitTextView.text = if(cmToM) "cm" else "m"
        binding.outputUnitTextView.text = if(cmToM) "m" else "cm"

        super.onRestoreInstanceState(savedInstanceState)
    }
}

 

 

 

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

    <!-- 값 입력받는 EditText -->
    <EditText
        android:id="@+id/inputEditText"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:hint="자연수를 입력해주세요"
        android:maxLength="7"
        android:textColorHint="@color/purple_200"
        android:textSize="30sp"
        android:textColor="@color/purple_500"
        android:textStyle="italic"
        android:gravity="end"
        android:inputType="number"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.4"
        app:layout_constraintHorizontal_bias="0.1"
        />

    <!-- 입력받은 값을 처리하여 화면에 보여주는 TextView -->

    <!-- inputEditText 오른쪽의 단위 표시할 TextView -->
    <TextView
        android:id="@+id/outputTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="28dp"
        android:gravity="end"
        android:text="100"
        android:textColor="@color/black"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="@+id/inputEditText"
        app:layout_constraintStart_toStartOf="@+id/inputEditText"
        app:layout_constraintTop_toBottomOf="@id/inputEditText" />

    <TextView
        android:id="@+id/inputUnitTextView"
        android:text="cm"
        android:textSize="20sp"
        android:layout_marginStart="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toEndOf="@id/inputEditText"
        app:layout_constraintTop_toTopOf="@id/inputEditText"
        app:layout_constraintBaseline_toBaselineOf="@id/inputEditText"
        />

    <!-- outputTextView 오른쪽의 단위 표시할 TextView -->
    <TextView
        android:id="@+id/outputUnitTextView"
        android:text="m"
        app:layout_constraintStart_toEndOf="@id/outputTextView"
        app:layout_constraintBaseline_toBaselineOf="@id/outputTextView"
        android:textSize="20sp"
        android:layout_marginStart="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

    <ImageButton
        android:id="@+id/swapImageButton"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginStart="8dp"
        android:src="@drawable/ic_baseline_swap_vert_24"
        app:layout_constraintBottom_toBottomOf="@id/outputTextView"
        app:layout_constraintStart_toEndOf="@id/inputUnitTextView"
        app:layout_constraintTop_toTopOf="@id/inputEditText" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

 

build.gradle (:app)

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.part1.chapter3'
    compileSdk 33

    defaultConfig {
        applicationId "com.part1.chapter3"
        minSdk 21
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    viewBinding {
        enabled = true
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

1) 입력값 받아오기 (AddTextChangedListener)

문자열이 변경되면 알려주는 리스너인 AddTextChangedListener 를 불러오자.

	      // 문자열이 변경되면 알려주는 리스너
        inputEditText.addTextChangedListener { text ->
            inputNumber = text.toString().toInt()
            Log.d("inputNumber", inputNumber.toString())
        }

text.toString().toInt()

 

String 을 Int 로 변경하는 코드에서 오류 발생.

 


1-1) 예외처리 ( int 의 범위 )

try ~ catch 문을 사용할 수도 있지만

EditText 의 속성인 maxLength 를 지정해주도록 하자.

 

int 의 범위는 대락 2억 (10자리 수) 까지 이지만

맨 앞자리 수가 2보다 큰 수, 예를 들어 9가 된다면 오류가 발생하므로

9자리 숫자까지로 제한해주자.

 

activity_main.xml

    <EditText
       ...
        android:maxLength="9"

1-2) 예외처리 ( 빈 문자열 )

빈 문자열을 toString().toInt() 로 변환하는 데 오류가 발생한다.

따라서 아래와 같이, 입력값이 “빈 문자열” 인경우에 대한 예외처리를 해주자.

inputEditText.addTextChangedListener { text ->
    inputNumber = if(text.isNullOrEmpty()) {
        0
    } else {
        text.toString().toInt()
    }
    ...
}

2) 변환 기능 구현

2-1) 숫자 변환

cm 를 m 로 변환하는 기능을 구현해보자.

// 문자열이 변경되면 알려주는 리스너
inputEditText.addTextChangedListener { text ->

    // cm -> m
    var outputNumber = inputNumber.times(0.01)
    if(cmToM) {
        outputTextView.text = inputNumber.times(0.01).toString()
    }else{
        outputTextView.text = inputNumber.times(100).toString()
    }
}

swap 이미지버튼을 누르면 단위가 변경된다.

이때, m를 cm 로 변경하는데 EditText 의 maxLength 가 9였으므로

100을 곱한 cm 변환 결과는 11자리 수가 된다.

 

이는 정수 범위를 벗어난 것이므로

간단히 maxLength 를 7로 수정해준다.


2-2) 단위 텍스트 swap

이제 swap 버튼을 눌렀을 때

inputUnitTextView 와 outputTextView 의 text 를 변경해보자.

 

swap 이미지 버튼을 누르면

cm 를 m 로, m 를 cm 로 변경한다.

var cmToM = true
swapImageButton.setOnClickListener {
    cmToM = cmToM.not() // !cmToM

    if (cmToM) {
        inputUnitTextView.text = "cm"
        outputUnitTextView.text = "m"
        outputTextView.text = inputNumber.times(0.01).toString()
    } else {
        inputUnitTextView.text = "m"
        outputUnitTextView.text = "cm"
        outputTextView.text = inputNumber.times(100).toString()
    }
}

3) 전체 코드

 

 

MainActivity.kt

package com.part1.chapter3

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import com.part1.chapter3.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    // 바인딩 변수 선언
    private lateinit var binding : ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // binding 변수에 instance 할당
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

//         id 값은 유일하지 않다.
//         activity_main 이 아닌 다른 xml 파일에서도
//         outputTextView 를 id 로 하는 뷰를 생성할 수 있다.
//         따라서 id 가 중복이 되는 이슈

        val outputTextView = binding.outputTextView
        val outputUnitTextView = binding.outputUnitTextView
        val inputEditText = binding.inputEditText
        val inputUnitTextView = binding.inputUnitTextView
        val swapImageButton = binding.swapImageButton
        var inputNumber : Int = 0
        var cmToM = true

        // 문자열이 변경되면 알려주는 리스너
        inputEditText.addTextChangedListener { text ->
            inputNumber = if(text.isNullOrEmpty()) {
                0
            } else {
                text.toString().toInt()
            }

            // cm -> m
            var outputNumber = inputNumber.times(0.01)
            if(cmToM) {
                outputTextView.text = inputNumber.times(0.01).toString()
            }else{
                outputTextView.text = inputNumber.times(100).toString()
            }
        }


        swapImageButton.setOnClickListener {
            cmToM = cmToM.not() // !cmToM

            if (cmToM) {
                inputUnitTextView.text = "cm"
                outputUnitTextView.text = "m"
                outputTextView.text = inputNumber.times(0.01).toString()
            } else {
                inputUnitTextView.text = "m"
                outputUnitTextView.text = "cm"
                outputTextView.text = inputNumber.times(100).toString()
            }
        }


    }
}

 

 

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

    <!-- 값 입력받는 EditText -->
    <EditText
        android:id="@+id/inputEditText"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:hint="자연수를 입력해주세요"
        android:maxLength="7"
        android:textColorHint="@color/purple_200"
        android:textSize="30sp"
        android:textColor="@color/purple_500"
        android:textStyle="italic"
        android:gravity="end"
        android:inputType="number"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.4"
        app:layout_constraintHorizontal_bias="0.1"
        />

    <!-- 입력받은 값을 처리하여 화면에 보여주는 TextView -->

    <!-- inputEditText 오른쪽의 단위 표시할 TextView -->
    <TextView
        android:id="@+id/outputTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="28dp"
        android:gravity="end"
        android:text="100"
        android:textColor="@color/black"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="@+id/inputEditText"
        app:layout_constraintStart_toStartOf="@+id/inputEditText"
        app:layout_constraintTop_toBottomOf="@id/inputEditText" />

    <TextView
        android:id="@+id/inputUnitTextView"
        android:text="cm"
        android:textSize="20sp"
        android:layout_marginStart="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toEndOf="@id/inputEditText"
        app:layout_constraintTop_toTopOf="@id/inputEditText"
        app:layout_constraintBaseline_toBaselineOf="@id/inputEditText"
        />

    <!-- outputTextView 오른쪽의 단위 표시할 TextView -->
    <TextView
        android:id="@+id/outputUnitTextView"
        android:text="m"
        app:layout_constraintStart_toEndOf="@id/outputTextView"
        app:layout_constraintBaseline_toBaselineOf="@id/outputTextView"
        android:textSize="20sp"
        android:layout_marginStart="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

    <ImageButton
        android:id="@+id/swapImageButton"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginStart="8dp"
        android:src="@drawable/ic_baseline_swap_vert_24"
        app:layout_constraintBottom_toBottomOf="@id/outputTextView"
        app:layout_constraintStart_toEndOf="@id/inputUnitTextView"
        app:layout_constraintTop_toTopOf="@id/inputEditText" />

</androidx.constraintlayout.widget.ConstraintLayout>

ViewBinding

counter 구현 시, UI 요소를 가져올때 findViewByID 를 사용했었다.

이번에는 viewBinding 을 사용해볼 것이다.

viewBinding은 findViewByID 를 대체한다.

 

 


1) findViewByID 와 ViewBinding

1-1) findViewByID

뷰의 id 값은 한 파일 내에서는 유일하지만

여러 파일 범위에서는 유일하지 않다.

 

 

activity_main 이 아닌 다른 xml 파일에서도

textView 를 id 로 하는 뷰를 생성할 수 있다.

따라서 id 가 중복이 되는 이슈가 있다.

 

 

main_activity.xml 이 아닌

second_activity.xml 에 textView 를 생성하자.

 

그리고 MainActivity.kt 에서 아래와 같은 코드를 작성하고 Run 한다.

val textView = findViewById<TextView>(R.id.textView)
textView.text = "안녕하세요."

 

Run 결과, 아래와 같이 앱이 crash 가 난다.

Activity 를 시작할 수 없고, 그 이유는 NullPointException 이라는 오류가 발생.

reference 할 object 가 없기 때문에 setText 를 할 수 없다.

 

정확히 아래 부분에서 에러가 발생했다고 알려주지만

빨간줄이 쳐져있지 않은 것을 보아

컴파일 단계에서 디버깅이 되지 않았다.

 

textView 는 R 파일에 id 로 선언이 되어 있지만

activity_main 이라는 xml 에 선언되어있는 UI 요소가 아니다.

따라서 접근이 불가능하다.

id 값으로 UI 요소를 가져오면, 위와 같은 위험요소가 발생한다.

 

 


1-2) ViewBinding

반면, viewBinding 을 사용하면?

선언했던 UI 요소를 바로 가져올 수 있다.

선언하지 않은 UI 요소들은 뜨지도 않는다.

 

 

장점

findViewByID 에서 발생할 수 있는 NPE 를 방지하고

UI 요소 값을 기억하지 않고도 바로 쉽게 가져올 수 있다.

 

 


2) 사용 순서

2-0) gradle 파일에 viewBinding 사용 선언.

2-1) binding 변수를 선언하여, layoutInflator 로 view를 생성한 후

2-2) view 를 가져와 변수에 담는다.

2-3) setContentView 로 view 세팅.

inflate : 불러 일으키다. 터트리다. 화면을 만들겠다.

binding 변수의 layoutInflater 가 뷰를 생성한다.

 


2-0) gradle : app

android { } 이하 수준에서

viewBinding 을 사용하겠다고 선언하자.

viewBinding {
        enabled = true
    }

그래들 파일을 수정하게 되면

프로젝트가 그래들 파일과 sync 를 맞춘 이후로

그래들 파일이 변경되었으므로, Sync Now 를 클릭한다.


2-1) binding 변수 선언

binding 을 변수로 선언하자.

activity class 에서만 사용할 것이니 private 으로.

 

viewbinding 을 사용하겠다고 하면,

레이아웃을 생성함과 동시에

레이아웃의 이름에 따라 binding class 가 자동으로 생성된다.

 

activity main 이라고 이름을 정했으므로

ActivityMainBinding 이라는 클래스가 자동 생성된다.

 

 

아래와 같이 코드를 작성하면

   	// 바인딩 변수 선언
    private var binding : ActivityMainBinding
    {

    }

ActivityMainBinding 이 초기화가 되어야한다는 오류가 발생한다.

 

 

따라서 나중에 초기화 하겠다는 lateinit 을 설정한다.

lateinit 설정 시, type 을 반드시 지정해주어야 한다.

// 바인딩 변수 선언
private lateinit var binding : ActivityMainBinding

 


2-2) view 가져오기

val view = binding.root

activity_main layout의 가장 상단에 있던 부모 레이아웃이

ConstraintLayout 이었음을 재확인할 수 있다.


2-3) setContentView

setContentView 로 view 를 셋팅한다.

setContentView(view)


전체코드

package com.part1.chapter3

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import com.part1.chapter3.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    // 바인딩 변수 선언
    private lateinit var binding : ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // binding 변수에 instance 할당
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

//         id 값은 유일하지 않다.
//         activity_main 이 아닌 다른 xml 파일에서도
//         outputTextView 를 id 로 하는 뷰를 생성할 수 있다.
//         따라서 id 가 중복이 되는 이슈

        val outputTextView = binding.outputTextView
        val outputUnitTextView = binding.outputUnitTextView
        val inputEditText = binding.inputEditText
        val inputUnitTextView = binding.inputUnitTextView
    }
}

1) 입력값을 입력받는 EditText

사용자의 입력값을 입력받을 뷰인 textView 입니다.

<EditText
		    android:id = "+@id/inputEditText"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:text="자연수를 입력해주세요"

        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.4"/>

 

 

1-1) TextView 를 상속받는 EditText

EditText 가 어떻게 만들어졌는지 알아보자.

<EditText 의 글자에서 “command” 키 를 누른 채로 클릭해보자.

윈도우에서는 alt + 클릭.

EditText 는 TextView 를 상속받았다. 는 것을 알 수 있다.

이 말은, EditText 가 TextView 에서 설정했던 속성값들을 모두 사용할 수 있다는 것이다.

따라서 EditText 에서도 TextView 의 속성인

textSize, textColor, textStyle, gravity 값을 설정할 수 있다.

android:textSize="30sp"
android:textColor="@color/purple_500"
android:textStyle="italic"
android:gravity="center"


1-2) 입력 타입 제한하기 ( inputType )

입력 형태를 제한해주고 싶으면 (문자 제외 숫자만 등)

inputType 을 설정해주자.

inputType 에 어떤 것들이 있는지 알고 싶다면

command + 클릭을 해보자.

signed number 라는 설명을 확인할 수 있다.

 

 

만약 정수만 입력받고 싶다면

inputType을 number 로 설정해주자.

android:inputType="number"

 

이렇게 설정하면

  • 한국어/영어 자판이 아닌 숫자 자판으로 변경되고
  • ‘.’ 문자를 입력할 수 없다. (정수이므로)

 

만약 소수점까지 입력받고 싶다면

inputType을 numberDecimal 로 설정해주자.

 

어떤 속성이 있는지 파일을 살펴보며

속성값들을 하나하나 다 확인해보는 것이 좋다.


1-3) 입력 시 text 사라지게 하고 싶다면? ( Hint )

hint 속성을 사용하자.

android:text="자연수를 입력해주세요" 대신

android:hint="자연수를 입력해주세요" 를 사용하자.

hint 의 문자 색을 변경하고 싶다면

textColorHint 를 설정해주자.

android:textColorHint="@color/black"

 

2) 결과를 출력하는 TextView

입력받은 자연수의 단위를 변환한 결과를 출력하는 TextView 를 만들어보자.

2-1) constraint

1번 에서 만든 EditText 와 TextView 가 함께 움직이게 하고 싶다면

TextView의 왼쪽 constraint를, EditText 의 왼쪽 끝에,

TextView의 오른쪽 constraint를, EditText 의 오른쪽 끝으로 지정한다.

app:layout_constraintEnd_toEndOf="@id/inputEditText"
app:layout_constraintStart_toStartOf="@+id/inputEditText"

 

 

2-2) margin

위아래 간격을 두기 위해 marginTop 을 설정하자.

android:layout_marginTop="30dp"

 

 

2-3) width = 0 ( 종속조건 )

TextView 의 width 를 0dp 로 설정하면

상위뷰인 EditText와 똑같은 width 를 가진 뷰가 된다.

android:layout_width="0dp"

이러한 종속조건을 통해서 길이값을 지정할 수 있다.

 

 

2-4) gravity

EditText 와 TextView 의 gravity 를 각각 end 로 설정하자.

그러면 오른쪽 정렬이 된다.

android:gravity="end"

 

 

2-5) baseline

bottom 을 constraint 로 지정하는 방법도 있지만

app:layout_constraintBottom_toBottomOf="@id/inputEditText"

baseline 을 constraint 로 지정하는 방법도 있다.

app:layout_constraintBaseline_toBaselineOf="@id/outputTextView"


3) ImageButton

AndroidStudio 의 File > New > Vector Asset

clip icon > swap vert 검색

벡터 에셋을 만들면 아래와 같이

res > drawable 에 파일이 생성됨을 확인할 수 있다.

 

ImageButton 에서 벡터 에셋을 사용하고자할 때

src (source) 를 사용한다.

android:src="@drawable/ic_baseline_swap_vert_24"

4) 전체코드

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

    <!-- 값 입력받는 EditText -->
    <EditText
        android:id="@+id/inputEditText"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:hint="자연수를 입력해주세요"
        android:textColorHint="@color/purple_200"
        android:textSize="30sp"
        android:textColor="@color/purple_500"
        android:textStyle="italic"
        android:gravity="end"
        android:inputType="number"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.4"
        app:layout_constraintHorizontal_bias="0.1"
        />

    <!-- 입력받은 값을 처리하여 화면에 보여주는 TextView -->

    <!-- inputEditText 오른쪽의 단위 표시할 TextView -->
    <TextView
        android:id="@+id/outputTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="28dp"
        android:gravity="end"
        android:text="100"
        android:textColor="@color/black"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="@+id/inputEditText"
        app:layout_constraintStart_toStartOf="@+id/inputEditText"
        app:layout_constraintTop_toBottomOf="@id/inputEditText" />

    <TextView
        android:id="@+id/inputUnitTextView"
        android:text="cm"
        android:textSize="20sp"
        android:layout_marginStart="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toEndOf="@id/inputEditText"
        app:layout_constraintTop_toTopOf="@id/inputEditText"
        app:layout_constraintBaseline_toBaselineOf="@id/inputEditText"
        />

    <!-- outputTextView 오른쪽의 단위 표시할 TextView -->
    <TextView
        android:id="@+id/outputUnitTextView"
        android:text="m"
        app:layout_constraintStart_toEndOf="@id/outputTextView"
        app:layout_constraintBaseline_toBaselineOf="@id/outputTextView"
        android:textSize="20sp"
        android:layout_marginStart="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

    <ImageButton
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginStart="8dp"
        android:src="@drawable/ic_baseline_swap_vert_24"
        app:layout_constraintBottom_toBottomOf="@id/outputTextView"
        app:layout_constraintStart_toEndOf="@id/inputUnitTextView"
        app:layout_constraintTop_toTopOf="@id/inputEditText" />

</androidx.constraintlayout.widget.ConstraintLayout>

https://bluefootedbooby.tistory.com/133

 

mp3 파일의 앨범 이미지 가져오기

음악 재생 앱을 만드는 중, 앨범 이미지를 어떻게 로드할 지 고민하게 되었습니다.   MediaStore.Albums 에는 ALBUM_ART 라고, 안드로이드의 MediaStore 데이터베이스에 저장된 앨범의 커버 이미지(아

bluefootedbooby.tistory.com

 

 

 

 

getContentReesolver()의 존재를 알게 된 후,

이전보다 이미지 로드 시간이 단축되었습니다.

 

// 커서로 전달받은 데이터를 꺼내서 저장
query?.use { cursor ->
    var index: Int = 0
    val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
    val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
    val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)
    val albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
    val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA) // 로컬 PATH
    val durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)

    while (cursor.moveToNext()) {
        val title = cursor.getString(titleIndex)
        val artist = cursor.getString(artistIndex)
        val album = cursor.getString(albumIndex)
        val path = cursor.getString(dataIndex)
        val albumId = cursor.getLong(albumIdIndex)

        val albumUri = ContentUris.withAppendedId(
            MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId
        )
        val albumArt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            try {
                val thumbnail = getContentResolver().loadThumbnail(
                    albumUri,
                    Size(300,300),
                    null
                )
                Bitmap.createScaledBitmap(thumbnail, 600, 600, true) // 강제 리사이징
            } catch(e:FileNotFoundException){
                val decodedBitmap = BitmapFactory.decodeResource(applicationContext.resources, R.drawable.img_bg_album)
                Bitmap.createScaledBitmap(decodedBitmap, 600, 600, true)
            }
        } else {
            getAlbumArt(path)
        }
        val duration = cursor.getLong(durationIndex) // milisec 단위임
        getMusicList().add(MusicFile(index, title, artist, album, albumArt, path, duration))
        index++
    }
}
Toast.makeText(applicationContext, "총 음악 개수 : ${getMusicList().size}", Toast.LENGTH_SHORT).show()

250227 이미지 로드 시간 단축

https://bluefootedbooby.tistory.com/187

 

음악 재생 앱을 만드는 중, 

앨범 이미지를 어떻게 로드할 지 고민하게 되었습니다.

 

 

 

MediaStore.Albums 에는 ALBUM_ART 라고,

 

안드로이드의 MediaStore 데이터베이스에 저장된 앨범의

 

커버 이미지(아트워크)를 가리키는 필드 - 가 있습니다.

 

위와 같이 이미지를 가져오려고 했으나,

아래와 같이 deprecated 로, 권장하지 않는 방식임을

 

컴파일 단계에서 알게 되었습니다.

 

 

 

 

그럼 MediaStore.Audio 에는 앨범정보가 있을까요?

 

https://developer.android.com/reference/android/provider/MediaStore.Audio.AlbumColumns

아쉽게도 앨범이름, 앨범 아이디, 앨범 아티스트 이름, 앨범 키 밖에 없네요.

 

 

 

이런 상황에서는 mp3 파일의 메타데이터비트맵으로 변환해주는

오픈소스 라이브러리를 사용할 수 있습니다.

 

저는 wseeman 씨의 라이브러리를 사용해보았습니다.

 

https://github.com/wseemann/FFmpegMediaMetadataRetriever/commits?author=wseemann

 

GitHub - wseemann/FFmpegMediaMetadataRetriever: FFmpegMediaMetadataRetriever provides a unified interface for retrieving frame a

FFmpegMediaMetadataRetriever provides a unified interface for retrieving frame and meta data from an input media file. - wseemann/FFmpegMediaMetadataRetriever

github.com

 

 

 

먼저,

아래와 같이 FFMPEG 미디어 메타 데이터 리트리버 라이브러리를

앱 수준 그래들 파일에서 implement 해줍니다.

 

 

implementation (libs.ffmpegmediametadataretriever)

 

 

아래는 Activity 에서 mp3 파일 경로로 앨범 이미지를 추출해

 

이미지를 비트맵으로 반환 하는 함수입니다.

 

// ■ 앨범 이미지 추출 (파일 경로로 이미지 추출, ffmpeg - 다익스트라)
    private fun getAlbumArt(filePath: String): Bitmap? {
        val mmr = FFmpegMediaMetadataRetriever()
        try {
            mmr.setDataSource(filePath)
            val artBytes = mmr.embeddedPicture
            if (artBytes != null) {
                return BitmapFactory.decodeByteArray(artBytes, 0, artBytes.size)
            }
        } catch (ex: Exception) {
            ex.printStackTrace()
        } finally {
            mmr.release()
        }
        return null
    }

 

 

이 함수의 호출은 아래와 같이 했습니다.

path는 mp3파일의 경로입니다.

 

musicList 에서 음악 데이터를 담고 있는데요.

이에 MusicFile 데이터 클래스 객체를 담아주었습니다.

 

val albumArt = getAlbumArt(path)
musicList.add(MusicFile(index, title, artist, album, albumArt, path, duration))

 

 

UI 업데이트는 아래와 같이 했습니다.

binding.ivAlbum.setImageBitmap(musicFile.albumArt)

 

 

 

결과는?

 

이렇게 화면에 잘 표시됨을 확인할 수 있습니다.

 

 

감사합니다.

기본적으로 이미 각 뷰들이 서로의 위치에 따라서 정렬되어 있으나,

이들을 부모 레이아웃과의 관계에서 중앙에 위치시키려면

 

추가적인 제약 설정이 필요 합니다.

 

 

 

app:layout_constraintHorizontal_chainStyle="packed"

 

위 요소를 "packed"로 지정하면

모든 연결된 요소들을 함께 묶어서

 

중앙에 위치하도록 합니다.

https://blog.naver.com/manhdh/220254628215

 

디버깅 기기가 offline으로 뜰 때 해결 방법 4가지

잘되던 연결이 갑자기 안된다하면,  클립스든, 인텔리 툴이든 둘다 적용되는 4가지 방법은 ...

blog.naver.com

 

내가 선택한 방식은 케이블 변경

충전 케이블에서 고속충전 데이터 케이블로 변경했다.

 

데이터 전송이 지원되지 않아서 

디바이스 인식이 PC에서는 되지만

안드로이드 스튜디오 디바이스 매니저에서는 되지 않는 것이었다.

 

 

다이소 3000원.

+ Recent posts