Activity를 사용자가 조작하면서 그 상태가 변경되는데, 그 변경에서 발생하는 이벤트에 대한 정리입니다. 다양한 상태 변화가 존재하지만 이 글에서는 2가지의 경우로 나눠 기술합니다.
첫번째는 activity를 실행하고 바로 뒤로 가기 버튼을 눌러 종료할때에 대한 이벤트 호출 순서입니다.

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


공간정보시스템 / 3차원 시각화 / 딥러닝 기반 기술 연구소 @지오서비스(GEOSERVICE)
Activity를 사용자가 조작하면서 그 상태가 변경되는데, 그 변경에서 발생하는 이벤트에 대한 정리입니다. 다양한 상태 변화가 존재하지만 이 글에서는 2가지의 경우로 나눠 기술합니다.
첫번째는 activity를 실행하고 바로 뒤로 가기 버튼을 눌러 종료할때에 대한 이벤트 호출 순서입니다.

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

다시 고등학생이 된 기분입니다. let, also, with 등 매우 기초적인 영어 단어를 외우는 기분인데요. 원래는 표준함수로 불리지만 Kotlin에서 고유하게 제공하므로 고유의 표준함수라는 제목을 붙였습니다. 이 함수들을 사용하여 코드를 좀더 최적화 시켜줄 수 있으며 불필요한 코딩량을 줄일 수 있습니다.
예제 코드는 다음과 같습니다.
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으로 받아 사용할 수 있습니다. 반환값은 람다 함수의 가장 마지막 코드의 반환값이 됩니다.
예제 코드는 다음과 같습니다.
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으로 받아 사용할 수 있으며, 받은 객체를 그대로 그대로 반환합니다.
예제 코드는 다음과 같습니다.
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로 처리합니다. 받은 객체를 그대로 반환합니다.
예제 코드는 다음과 같습니다.
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로 사용할 수 있습니다. 반환값은 람다 함수를 구성하는 가장 마지막 코드입니다.
예제 코드는 다음과 같습니다.
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로 받아 사용할 수 있습니다. 반환값은 람다 함수의 마지막 코드입니다.
예제 코드는 다음과 같습니다.
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 함수를 자동으로 호출해 줍니다.
예제 코드는 다음과 같습니다.
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을 반환합니다.
예제 코드는 다음과 같습니다.
import kotlin.system.measureTimeMillis
fun main() {
val time = measureTimeMillis {
Thread.sleep(10000)
}
println(time)
}
결과는 다음과 같다.
10000
measureTimeMillis은 어떤 코드를 실행하는데 소요되는 시간을 측정하는데 사용됩니다.
이 글은 안드로이드에서 사진을 찍은 후 원본 이미지를 파일로 저장하고 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)
}
}
실행 결과는 다음과 같다.

Spatialite에서는 테이블 생성시 바로 공간 데이터 필드를 추가할 수 없다. 먼저 공간 데이터 필드를 제외하고 테이블을 생성한다.
CREATE TABLE main_item (
id INTEGER PRIMARY KEY AUTOINCREMENT,
layer TEXT,
title TEXT,
feature_id INTEGER
);
이제 공간 데이터 필드를 추가한다.
SELECT AddGeometryColumn('main_item', 'geometry', -1, 'GEOMETRY', 'XY');
AddGeometryColumn 함수의 3번째는 EPSG 코드값이며, 4번째는 POINT, LINESTRING, POLYGON, MULTIPOINT 등이 올 수 있다. GEOMETRY는 모든 타입의 공간 데이터를 받을 수 있다.
공간 데이터 필드에 공간 인덱스를 건다.
SELECT CreateSpatialIndex('main_item', 'geometry');
끝으로 Row를 추가하는 SQL문은 다음과 같다.
INSERT INTO main_item (layer, title, feature_id, geometry)
VALUES ('layer1', 'title1', 100, ST_GEOMFROMTEXT('POINT(128.32132 37.34322)', -1));
화면 터치가 가능한 모바일 단말기에서 터치를 통한 UI의 조작은 매우 효과적입니다. 이러한 터치 기반의 UI의 활용에 대해 자연스러운 사용은 사용자에게 프로그램의 친밀도를 높여줍니다. 화면 터치에 대한 조작 중 Swiping은 사용자가 화면을 스치듯이 상하좌우로 쓸어넘기는 행위입니다. 이러한 Swiping 중 좌우에 대한 이벤트를 처리하기 위한 클래스는 다음과 같습니다.
package geoservice.nexgen
import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
abstract class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
companion object {
private const val SWIPE_DISTANCE_THRESHOLD = 100
private const val SWIPE_VELOCITY_THRESHOLD = 100
}
private val gestureDetector: GestureDetector
abstract fun onSwipeLeft()
abstract fun onSwipeRight()
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return gestureDetector.onTouchEvent(event)
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
return true
}
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
val distanceX = e2.x - e1.x
val distanceY = e2.y - e1.y
if (Math.abs(distanceX) > Math.abs(distanceY)
&& Math.abs(distanceX) > SWIPE_DISTANCE_THRESHOLD
&& Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (distanceX > 0) onSwipeRight() else onSwipeLeft()
return true
}
return false
}
}
init {
gestureDetector = GestureDetector(context, GestureListener())
}
}
위 클래스를 실제 View에 적용하는 코드의 예는 다음과 같습니다.
llListScroll.setOnTouchListener(object: OnSwipeTouchListener(context) {
override fun onSwipeLeft() { btnNext.performClick() }
override fun onSwipeRight() { btnPrevious.performClick() }
})
실제 위의 코드는 모바일 기반의 GIS 솔루션인 Mobile NexGen에 반영된 코드인데요. 위의 코드와 연관된 기능에 대한 시연 영상은 아래와 같습니다.