[Android] ActionBar에 메뉴 넣기

ActionBar에 메뉴를 하나 넣어보자. 먼저 넣을 Activity의 onCreate를 추가한다.

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

    supportActionBar?.setDisplayHomeAsUpEnabled(true)
}

메뉴에 대한 리소스를 추가한다. (위치는 res/menu)

<?xml version="1.0" encoding="utf-8"?>
<menu 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">
    <item
        app:showAsAction="always|withText"
        android:icon="@drawable/ic_add"
        android:id="@+id/menu_add"
        android:title="추가" />
</menu>

물론 위에 메뉴에 대한 이미지 리소스를 추가해야 한다. (위치는 res/drawable)

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
  <path
      android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"
      android:fillColor="#ffffff"/>
</vector>

메뉴 리소스를 엑티비티에 반영한다.

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    getMenuInflater().inflate(R.menu.data_collecting_list_menu, menu) ;
    return true
}

메뉴 터치시 실행되는 코드를 작성한다.

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.getItemId()) {
        R.id.menu_add -> {
            // At Here!
        }
    }

    return super.onOptionsItemSelected(item)
}

그러면 다음과 같이 메뉴가 추가된다.

[Android] Activity 상태 변경에 대한 Event 호출 순서

Activity를 사용자가 조작하면서 그 상태가 변경되는데, 그 변경에서 발생하는 이벤트에 대한 정리입니다. 다양한 상태 변화가 존재하지만 이 글에서는 2가지의 경우로 나눠 기술합니다.

첫번째는 activity를 실행하고 바로 뒤로 가기 버튼을 눌러 종료할때에 대한 이벤트 호출 순서입니다.

두번째는 activity를 실행하고 단말기를 회전하여 activity를 회전시켰을때에 대한 이벤트 호출 순서입니다.

Kotlin 고유의 표준함수 (let, also, apply, with, run, …)

다시 고등학생이 된 기분입니다. let, also, with 등 매우 기초적인 영어 단어를 외우는 기분인데요. 원래는 표준함수로 불리지만 Kotlin에서 고유하게 제공하므로 고유의 표준함수라는 제목을 붙였습니다. 이 함수들을 사용하여 코드를 좀더 최적화 시켜줄 수 있으며 불필요한 코딩량을 줄일 수 있습니다.

let

예제 코드는 다음과 같습니다.

data class WHAT(val name: String, val age: Int)

fun main() {
    println("Case 1")

    var w: WHAT? = null

    val v = w?.let {
        println(it)
    } ?: "Known"

    println(v)

    println("Case 2")

    var W: WHAT? = WHAT("Dip2K", 43)

    val V = W?.let {
        println(it)
    } ?: "Known"

    println(V)
}

결과는 다음과 같다.

Case 1
Known
Case 2
WHAT(name=Dip2K, age=43)
kotlin.Unit

let은 받은 객체(w)를 람다 함수 내부에서 it으로 받아 사용할 수 있습니다. 반환값은 람다 함수의 가장 마지막 코드의 반환값이 됩니다.

also

예제 코드는 다음과 같습니다.

data class WHAT(val name: String, val age: Int)

fun main() {
    println("Case 1")

    var w: WHAT? = null

    val v = w?.also {
        println(it)
    } ?: "Known"

    println(v)

    println("Case 2")

    var W: WHAT? = WHAT("Dip2K", 43)

    val V = W?.also {
        println(it)
    } ?: "Known"

    println(V)
}

결과는 다음과 같다.

Case 1
Known
Case 2
WHAT(name=Dip2K, age=43)
WHAT(name=Dip2K, age=43)

also 함수는 받은 객체를 it으로 받아 사용할 수 있으며, 받은 객체를 그대로 그대로 반환합니다.

apply

예제 코드는 다음과 같습니다.

data class WHAT(var name: String, var age: Int)

fun main() {
    val w = WHAT("Jackass", 16)

    val r = w.apply {
        name = "Dip2K"
        age = 43
    }

    println(w)
    println(r)
}

결과는 다음과 같다.

WHAT(name=Dip2K, age=43)
WHAT(name=Dip2K, age=43)

apply는 받은 객체를 람다 함수 내부에서 it이 아닌 this로 처리합니다. 받은 객체를 그대로 반환합니다.

with

예제 코드는 다음과 같습니다.

package with

data class WHAT(var name: String, var age: Int)

fun main() {
    val w = WHAT("Jackass", 16)

    val r = w?.let {
        with(it) {
            name = "Dip2K"
            age = 43
            "Good day!"
        }
    }

    println(w)
    println(r)
}

결과는 다음과 같다.

WHAT(name=Dip2K, age=43)
Good day!

with 함수는 인자로 받은 객체를 람다 함수 안에서 this로 사용할 수 있습니다. 반환값은 람다 함수를 구성하는 가장 마지막 코드입니다.

run

예제 코드는 다음과 같습니다.

data class WHAT(var name: String, var age: Int)

fun main() {
    val v = run {
        val w = WHAT("Jackass", 16)
        w
    }

    println(v)

    val vv = v.run {
        name = "Dip2K"
        age = 43

        "Good day !"
    }

    println(v)
    println(vv)
}

결과는 다음과 같다.

WHAT(name=Jackass, age=16)
WHAT(name=Dip2K, age=43)
Good day !

run은 객체를 전달받지 않고 독립적으로 실행하는 경우와 개체를 전달받아 처리하는 경우로 구분됩니다. 객체를 전달받아 처리하는 경우 전달받은 객체를 람다 함수 내부에서 this로 받아 사용할 수 있습니다. 반환값은 람다 함수의 마지막 코드입니다.

use

예제 코드는 다음과 같습니다.

import java.io.File

fun main() {
    val f = File("d:/a.txt")
    f.bufferedWriter().use {
        it.appendln("Hello, GIS Developer.")
        it.append("Good Day !")
    }
}

use 함수는 전달 받은 객체를 it으로 람다 함수 내부에서 사용하며, 람다 함수 종료시 close 함수를 자동으로 호출해 줍니다.

takeIf, takeUnless

예제 코드는 다음과 같습니다.

data class WHAT(var name: String, var age: Int)

fun main() {
    val w = WHAT("Jackass", 16)

    val v = w?.takeIf { it.age < 20 }
    //val v = w?.takeUnless { it.age >= 20 } <- 위의 코드와 동일함
    println(v)

    var r = v?.apply {
        name = "Dip2K"
    }
    println(r)
}

결과는 다음과 같다.

WHAT(name=Jackass, age=16)
WHAT(name=Dip2K, age=16)

takeIf 함수는 받은 객체를 람다 함수 내부에서 it으로 처리하며, 반환값이 true일때 받은 객체를 반환하고, false일때 null을 반환합니다.

measureTimeMillis

예제 코드는 다음과 같습니다.

import kotlin.system.measureTimeMillis

fun main() {
    val time = measureTimeMillis {
        Thread.sleep(10000)
    }

    println(time)
}

결과는 다음과 같다.

10000

measureTimeMillis은 어떤 코드를 실행하는데 소요되는 시간을 측정하는데 사용됩니다.

[Android] 사진 찍은 후 해당 사진을 파일로 저장해 표시하기

이 글은 안드로이드에서 사진을 찍은 후 원본 이미지를 파일로 저장하고 ImageView에 해당 사진 이미지를 표시하는 예제이다.

먼저 카메라 기능 및 외부 저장소에 대한 읽기/쓰기 퍼미션을 지정하기 위해 AndroidManifest.xml에 다음 코드를 추가한다.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera2" android:required="true" />

이왕 AndroidManifest.xml 파일을 편집하는 김에 Provider를 추가하자. Provider를 추가하는 이유는 외부 카메라 앱을 연동해서 사진을 찍을 건데, 카메라 앱이 사진을 찍은 후 지정된 파일에 사진 이미지를 저장하도록 하기 위해 파일에 대한 Provider가 필요하기 때문이다. 추가한 코드는 다음과 같다.

<application ... >

    <provider
        android:authorities="com.example.myapplication.fileprovider"
        android:name="androidx.core.content.FileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>

    ...

위의 Provider 코드 중 android:authorities 속성값에는 App의 Package명에 “.fileprovider”를 붙여 지정하였다. 파일 Provider가 외부의 앱에 공유하고자 하는 디렉토리를 meta-data에 지정하고 있는데 xml 파일은 file_paths에 관련 정보를 지정했으며 다음과 같다. 리소스에 xml 폴더를 만든 후에 file_paths.xml 파일을 추가한 뒤 아래의 내용을 입력한다.

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="files" path="Android/data/com.example.myapplication/files" />
</paths>

위의 내용은 이 앱이 접근할 수 있는 디렉토리인 Android/data/com.example.myapplication/files의 하위 폴더 전체를 공유할 수 있다.

카메라를 실행하고 사진을 찍어 그 내용을 표시하는 UI에 대한 레이아웃은 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="100dp"
        android:layout_height="58dp"
        android:id="@+id/btn_photo"
        android:layout_gravity="center"
        android:text="사진찍기" />


    <ImageView
        android:id="@+id/iv_photo"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

이제 이 레이아웃에 대한 Activity 코드는 다음과 같다.

package com.example.myapplication

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.widget.Button
import android.widget.ImageView
import androidx.core.app.ActivityCompat
import androidx.core.content.FileProvider
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : AppCompatActivity() {
    val TAG = "DIP2K"
    lateinit var btn_photo: Button
    lateinit var iv_photo: ImageView

    var m_imageFile: File? = null
    val REQUEST_TAKE_PHOTO = 100

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

        iv_photo = findViewById(R.id.iv_photo)
        btn_photo = findViewById(R.id.btn_photo)

        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if(checkSelfPermission(Manifest.permission.CAMERA) 
                == PackageManager.PERMISSION_GRANTED 
                && checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) 
                    == PackageManager.PERMISSION_GRANTED) {
            } else {
                ActivityCompat.requestPermissions(this,
                    arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
            }
        }

        btn_photo.setOnClickListener {
            val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            if(takePictureIntent.resolveActivity(getPackageManager()) != null) {
                createImageFile()?.let {
                    val photoURI = FileProvider.getUriForFile(this,
                        "com.example.myapplication.fileprovider", it)

                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                    startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO)

                    m_imageFile = it
                }
            }

        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if(grantResults[0] == PackageManager.PERMISSION_GRANTED 
            && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
            Log.d(TAG, "Permisson: " + permissions[0] + " was " + grantResults[0])
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(requestCode == REQUEST_TAKE_PHOTO) {
            if(resultCode == RESULT_OK) {
                m_imageFile?.let {
                    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        val source = ImageDecoder.createSource(contentResolver, Uri.fromFile(it))
                        ImageDecoder.decodeBitmap(source)?.let {
                            iv_photo.setImageBitmap(it)
                        }
                    } else {
                        MediaStore.Images.Media.getBitmap(contentResolver, Uri.fromFile(it))?.let {
                            iv_photo.setImageBitmap(it)
                        }
                    }
                }
            }
        }
    }

    private fun createImageFile(): File {
        val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val imageFileName = "PHOTO_${timeStamp}.jpg"
        val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        return File(storageDir, imageFileName)
    }
}

실행 결과는 다음과 같다.