[Android] XML로부터 View 생성하기

안드로이드는 UI의 구성을 XML로 정의하여 생성한다. 아래는 UI를 위한 XML인 map_legend_item.xml 파일이다.

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

    <LinearLayout
        android:paddingHorizontal="15dp"
        android:layout_width="match_parent"
        android:layout_height="54dp"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <Switch
            android:id="@+id/swLayerVisibility"
            android:layout_weight="0"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <geoservice.nexgen.maplegend.LegendSingleSymbol
            android:id="@+id/lssItem"
            android:layout_width="36dp"
            android:layout_height="36dp" />

        <Space
            android:layout_width="5dp"
            android:layout_height="1px" />

        <TextView
            android:layout_weight="1"
            android:id="@+id/tvLayerName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="@dimen/normal_text_size"
            android:textStyle="bold"
            android:text="LayerName" />


    </LinearLayout>
</LinearLayout>

위의 XML을 통해 View를 생성하는 코드는 다음과 같다.

for( ... ) {
    val itemLayout = inflater.inflate(R.layout.map_legend_item, null, false)
    itemLayout.findViewById<TextView>(R.id.tvLayerName).setText(title)

    ...

    mainLayout.addView(itemLayout)
}

위의 코드 중 inflater는 다음 3가지 방식 중 하나를 통해 생성된다.

val inflater = layoutInflater
val inflater = LayoutInflater.from(this)
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater

위의 코드를 통한 실제 결과는 다음과 같다.

[Android] 카메라로 찍은 이미지 올바른 방향으로 회전시켜 보여주기

안드로이드의 폰에서 찍은 사진은 내부적으로 카메라의 회전 정보가 담겨 있습니다. 폰으로 찍은 사진을 화면에 표시할때 이 화전 정보를 반영하여 촬영된 이미지를 표시해야 자연스럽습니다. 아래의 코드는 이미지 파일에 대한 회전 정보를 얻는 코드입니다.

val imageFilePath = "...... . jpg"
val ei = ExifInterface(imageFilePath)
val orientation: Int = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
val angle = when(orientation) {
    ExifInterface.ORIENTATION_ROTATE_90 -> 90f
    ExifInterface.ORIENTATION_ROTATE_180 -> 180f
    ExifInterface.ORIENTATION_ROTATE_270 -> 270f
    else -> 0f
}

위의 코드를 통해 이미지의 회전 각도르 얻어올 수 있고, 이를 토대로 이미지를 실제로 회전시키는 코드는 다음과 같습니다.

private fun resizeBitmap(src: Bitmap, size: Float, angle: Float): Bitmap {
    val width = src.width
    val height = src.height

    var newWidth = 0f
    var newHeight = 0f

    if(width > height) {
        newWidth = size
        newHeight = height.toFloat() * (newWidth / width.toFloat())
    } else {
        newHeight = size
        newWidth = width.toFloat() * (newHeight / height.toFloat())
    }

    val scaleWidth = newWidth.toFloat() / width
    val scaleHeight = newHeight.toFloat() / height

    val matrix = Matrix()

    matrix.postRotate(angle);
    matrix.postScale(scaleWidth, scaleHeight)

    val resizedBitmap = Bitmap.createBitmap(src, 0, 0, width, height, matrix, true)
    return resizedBitmap
}

위의 함수는 카메라로 찍은 이미지를 회전시켜 주는 것뿐만 아니라 이미지의 크기를 화면에 표시하기에 적당한 크기를 인자로 받아 줄여줍니다. 실제 사용하는 코드는 다음과 같습니다.

val imageFilePath = "...... . jpg"
val file = File(imageFilePath)

val angle = ...

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    val source = ImageDecoder.createSource(contentResolver, Uri.fromFile(file))
    ImageDecoder.decodeBitmap(source)?.let {
        imageView.setImageBitmap(resizeBitmap(it, 900f, 0f))
    }
} else {
    MediaStore.Images.Media.getBitmap(contentResolver, Uri.fromFile(file))?.let {
        imageView.setImageBitmap(resizeBitmap(it, 900f, 0f))
    }
}

[Android] RecycleView 추가하기

몇번을 사용해도 맨날 잊는다. 추후 다시 RecycleView를 사용할때 참조하기 위해 정리해 둔다.

먼저 레이아웃 요소로서 추가한다.

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rvCollectingData"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

RecycleView는 유사한 레이아웃을 갖는 내용만 다른 항목에 대한 리스트 UI인데, 먼저 항목을 구성하는 데이터 클래스 정의가 필요하다.

package geoservice.nexgen.collectingdata.datalistactivity

import geoservice.nexgen.collectingdata.DataCollectingDB

data class DataCollectingListItem(
    val id: Int,
    val link_id: Int,
    val type: DataCollectingDB.Type,
    val data: String,
    val title: String,
    val date: String
)

항목에 대한 레이아웃도 필요한데 다음과 같다. 파일명은 data_collection_list_recycler_view_item.xml로 지정한다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="44dp"
    android:paddingHorizontal="10dp"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/ivIcon"
        android:src="@drawable/ic_layers_black"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:tint="#444444" />

    <TextView
        android:textSize="@dimen/normal_text_size"
        android:id="@+id/tvCaption"
        android:layout_marginLeft="10dp"
        android:layout_width="match_parent"
        android:layout_weight="1"
        android:layout_height="wrap_content" />

    <TextView
        android:textSize="@dimen/small_text_size"
        android:id="@+id/tvDate"
        android:layout_width="100dp"
        android:textAlignment="center"
        android:layout_height="wrap_content" />
</LinearLayout>

이제 위에서 정의한 코드와 레이아웃이 적용된 Adapter 클래스를 추가한다.

package geoservice.nexgen.collectingdata.datalistactivity

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import geoservice.nexgen.R
import geoservice.nexgen.collectingdata.DataCollectingDB

class DataCollectingListRecyclerViewAdapter(val items: ArrayList<DataCollectingListItem>) 
    : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    internal inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val ivIcon = itemView.findViewById<ImageView>(R.id.ivIcon)
        val tvCaption = itemView.findViewById<TextView>(R.id.tvCaption)
        val tvDate = itemView.findViewById<TextView>(R.id.tvDate)
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): RecyclerView.ViewHolder {
        val view: View = LayoutInflater.from(viewGroup.context)
                .inflate(R.layout.data_collection_list_recycler_view_item, viewGroup, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, i: Int) {
        val holder = viewHolder as ViewHolder
        val item = items[i]

        when(item.type) {
            DataCollectingDB.Type.FORM -> holder.ivIcon.setImageResource(R.drawable.ic_form_black)
            DataCollectingDB.Type.MOVIE -> holder.ivIcon.setImageResource(R.drawable.ic_movie_black)
            DataCollectingDB.Type.PHOTO -> holder.ivIcon.setImageResource(R.drawable.ic_photo_camera_black)
        }

        holder.tvCaption.text = item.title

        holder.tvDate.text = item.date
    }

    override fun getItemCount(): Int {
        return items.size
    }
}

이제 Activity에서 RecycleView에 대한 설정 코드를 작성하는데, 먼저 레이아웃을 잡는다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    rvCollectingData.layoutManager = LinearLayoutManager(this)
}

다음은 RecycleView에 표시될 데이터 항목을 구성하고 Adapter로 지정하는 코드이다.

private fun setupCollectionDataRecycleView(id: Int) {
    var items: ArrayList<DataCollectingListItem>? = null
    val job = GlobalScope.launch(Dispatchers.IO) {
        items = MainActivity.dataCollectingDB.getSubItems(id)
    }

    GlobalScope.launch(Dispatchers.Main) {
        job.join()
        items?.let {
            rvCollectingData.adapter = DataCollectingListRecyclerViewAdapter(it)
        }
    }
}

좀더 이해를 돕고자, 위의 코드 중 MainActivity.dataCollectingDB.getSubItems 함수는 다음과 같다.

fun getSubItems(link_id: Int): ArrayList<DataCollectingListItem> {
    val result = ArrayList<DataCollectingListItem>()
    val sql = "SELECT id, type, data, title, date FROM sub_item WHERE link_id = $link_id"
    Log.v("DIP2K", sql)
    val stmt = db.prepare(sql)
    while (stmt.step()) {
        val id = stmt.column_int(0)
        val type = stmt.column_string(1)
        val data = stmt.column_string(2)
        val title = stmt.column_string(3)
        val date = stmt.column_string(4)

        val item = DataCollectingListItem(id, link_id, Type.valueOf(type), data, title, date)
        result.add(item)
    }

    return result
}

[Android] 나머지 폭 차지하기

UI를 배치할때 나머지 공간을 차지하게 하고 싶은 경우가 있다. 예를들면 아래와 같이..

사진이라는 문자열이 들어간 TextView는 가로폭의 길이가 정해져 있지 않고 나머지 공간을 모두 차지한다. 코드는 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="44dp"
    android:paddingHorizontal="10dp"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/ivIcon"
        android:src="@drawable/ic_layers_black"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:tint="#444444" />

    <TextView
        android:textSize="@dimen/normal_text_size"
        android:id="@+id/tvCaption"
        android:layout_marginLeft="10dp"
        android:layout_width="match_parent"
        android:layout_weight="1"
        android:layout_height="wrap_content" />

    <TextView
        android:textSize="@dimen/small_text_size"
        android:id="@+id/tvDate"
        android:text="2020/12/31 12:13:13"
        android:layout_width="100dp"
        android:textAlignment="center"
        android:layout_height="wrap_content" />

</LinearLayout>

사진에 해당하는 TextView의 layout_weight를 1로 지정하고 있다는게 핵심이다.