TAE

[Android/Kotlin] 커스텀 앨범 만들기 - 커스텀 갤러리(2) 본문

android/코드

[Android/Kotlin] 커스텀 앨범 만들기 - 커스텀 갤러리(2)

tg-world 2023. 3. 17. 22:38
반응형

안드로이드 앱 개발을 하다 보면 앨범에서 사진 선택 등 앨범에 접근하는 기능이 필요합니다.

기본 갤러리에 접근하여 미디어 파일을 가져올 수도 있지만 UI/UX 변경이 불가능합니다.

내부 저장소에서 미디어 파일을 가지고 와서 앨범을 커스텀을 하게 되면 디자인 변경이 가능하여 커스텀 앨범을 만들어 보겠습니다.

추가로 커스텀 작업을 하면 최초 앨범 선택 하는 팝업 없이 바로 커스텀 앨범 띄우는 것도 가능합니다

실행동작


코드

미디어 파일에 접근하려면 저장소 권한을 승인 작업이 필요합니다

권한 승인 관련 포스팅은 아래에서 확인 가능합니다

2023.03.16 - [android/코드] - [Android/Kotlin] 갤러리 접근 권한(Premission) 설정하기 - 커스텀 갤러리(1)

 

[Android/Kotlin] 갤러리 접근 권한(Premission) 설정하기 - 커스텀 갤러리(1)

안드로이드 앱을 개발하다 보면 특정 권한을 얻어야지만 사용이 가능한 기능들이 있습니다. Android6.0 (API 수준 23) 마쉬멜로우 이전 버전은 메니페스트에 권한을 넣어주면 사용이 가능했지만, 마

tg-world.tistory.com

 

접근 권한 작업을 마쳤으면 우선 MediaStore에서 파일을 가져오는 작업을 하여야 한다.

MediaStore

* CustomAlbumActivity.kt

  @SuppressLint("Range")
    private fun initData() {

        val projection = arrayOf(
            MediaStore.Files.FileColumns._ID,
            MediaStore.Files.FileColumns.DATE_ADDED,
            MediaStore.Files.FileColumns.MEDIA_TYPE,
            MediaStore.Files.FileColumns.DURATION
        )
        var selection: String?
        val queryUri = MediaStore.Files.getContentUri("external")

        selection =
            (MediaStore.Files.FileColumns.MEDIA_TYPE + "=" + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
                    + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=" + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)

        //이런 방법도 있다~
//            cursorLoader = CursorLoader(
//                this@CustomAlbumActivity,
//                queryUri,
//                projection,
//                selection,
//                null,  // Selection args (none).
//                MediaStore.Files.FileColumns.DATE_ADDED + " DESC" // Sort order.
//            )
//            cursor = cursorLoader.loadInBackground()

        var cursor: Cursor? = this@CustomAlbumActivity.contentResolver.query(
            queryUri,
            projection,
            selection,
            null,
            MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
        )

        if (cursor != null && cursor.moveToFirst()) {
            do {
                val data = ItemGallery()
                data.id =
                    cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID))
                data.mediaType =
                    cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns.MEDIA_TYPE))
                data.duration =
                    cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns.DURATION))

                if (data.mediaType == 1) {
                    data.mediaData = "content://media/external/images/media/" + data.id
                } else if (data.mediaType == 3) {
                    data.mediaData = "content://media/external/video/media/${data.id}"
                }
                listOfPhotos.add(data)
            } while (cursor.moveToNext())

        }

        cursor!!.close()
        customAlbumAdapter.setData(listOfPhotos)
    }

미디어 스토리지에 저장 시간을 내림차순(DESC)으로 정렬하여, 미디어 아이디와, 미디어 타입, 영상 지속시간?(영상 시간) 을 가져와서 listOfPhotos 리스트에 add 해준 후 Adapter에 연결하여 줍니다.

미디어 타입이 1일 경우 : 이미지

미디어 타입이 3일 경우 : 비디오

로 구분하여 data에 mediaData를 추가하여 어뎁터에서 타입에 맞는 url을 이용하여 이미지를 보여줍니다.

MeidaStore란?

- 안드로이드 시스템에서 제공하는 미디어 데이터 DB

- 미디어 파일(이미지, 동영상)을 쿼리문을 사용하여 사용가능

MediaStore.Files

- 파일을 다루기 위해 추가된 API

- getCountUri() 메서드를 통하여 쿼리 할 Uri를 얻을 수 있으며, 인자로는 Internal, external을 넘겨줘야 하며, 이 값으로 스토리지 영역을 구분한다

MediaStore.Files.FileColumns

- 미디어 DB에서 파일과 관련된 칼럼을 나타내는 상수값이 정의된 인터페이스

* https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns

 

MediaStore.Files.FileColumns  |  Android Developers

 

developer.android.com

 

RecyceView Adapter연결

customAlbumAdapter = CustomAlbumAdapter(listOfPhotos)
binding.photoRV.adapter = customAlbumAdapter

Adapter구성

* CustomAlbumAdapter.kt

class CustomAlbumAdapter(private var itemList: ArrayList<ItemGallery>) :
    RecyclerView.Adapter<CustomAlbumAdapter.MyViewHolder>() {

    inner class MyViewHolder(val binding: ItemPhotoBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: ItemGallery) {
            binding.tag = item

            binding.layoutCL.setOnClickListener {
                item.isSelected = !binding.selectRatioBT.isChecked
                binding.selectRatioBT.isChecked = item.isSelected
            }

        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val listItemBinding = ItemPhotoBinding.inflate(inflater, parent, false)
        return MyViewHolder(listItemBinding)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(itemList[position])
    }

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

    fun setData(itemList: ArrayList<ItemGallery>) {
        this.itemList = itemList
        notifyDataSetChanged()
    }
}

CustomAlbumActivity에서 가져온 스토리지 데이터를 adapter에 연결 하여 줍니다.

layout을 클릭하게 되면 isSelected를 변경하며, UI 구성을 하였습니다

UI 구성을 DataBinding을 활용하였으며, DataBinding에 포스팅은 따로 하도록 하겠습니다.


Data Class

*ItemGallery.kt

class ItemGallery {

    var mediaData: String = ""
    var id: Long = -1
    var isSelected = false
    var mediaType: Int// 사진: 1, 동영상: 3
    var duration: Int// 동영상 재생시간


    companion object {

        @JvmStatic
        @BindingAdapter("imageUrl")
        fun loadImage(view: ImageView, url: String) {
            Glide.with(view.context).load(url).into(view)
        }

        @JvmStatic
        @BindingAdapter("duration")
        fun setDuration(view: TextView, duration: Long) {
            if (duration == 0L) {
                view.visibility = View.GONE
            } else {
                view.visibility = View.VISIBLE
                view.text = Utils.getPrettyDuration(duration)
            }
        }

        @JvmStatic
        @BindingAdapter("radioButton")
        fun setRadioButton(view: RadioButton, isSelected: Boolean) {
            view.isChecked = isSelected
        }
    }

    constructor() {
        mediaData = ""
        id = -1
        isSelected = false
        mediaType = -1
        duration = 0
    }

}

DataBinding을 활용하기 위하여 BindingAdapter 을 사용하였습니다.

사용법도 따로 포스팅 하도록 하겠습니다


layout xml

*activity_custom_album.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>

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

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/headerLL"
            android:layout_width="0dp"
            android:layout_height="48dp"
            android:orientation="horizontal"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <ImageView
                android:id="@+id/closeTV"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="10dp"
                android:src="@drawable/ic_baseline_arrow_back_24"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <TextView
                android:id="@+id/headerTitleTV"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent"
                android:text="커스텀 앨범"
                android:textColor="@color/black"
                android:textSize="18dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/confirmTV"
                app:layout_constraintStart_toEndOf="@id/closeTV"
                app:layout_constraintTop_toTopOf="parent" />

            <TextView
                android:id="@+id/confirmTV"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="10dp"
                android:background="@android:color/transparent"
                android:text="확인"

                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>


        <ImageView
            android:id="@+id/imageV"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/headerLL"
            android:src="@mipmap/ic_launcher_round"
            android:visibility="gone"
            />

        <androidx.recyclerview.widget.RecyclerView
            android:paddingTop="10dp"
            android:id="@+id/photoRV"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/headerLL"
            app:spanCount="3"
            tools:listitem="@layout/item_photo" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

 

* item_photo.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="tag"
            type="com.example.videoselectthumbnail.ItemGallery" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:id="@+id/layoutCL"
        android:layout_height="150dp">


        <ImageView
            android:id="@+id/iv_profile"
            imageUrl="@{tag.mediaData}"
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:scaleType="centerCrop"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:src="@mipmap/ic_launcher_round" />

        <TextView
            android:id="@+id/durationTV"
            duration="@{tag.duration}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="5dp"
            android:layout_marginBottom="5dp"
            android:background="#B3252525"
            android:drawableLeft="@drawable/ic_baseline_play_arrow_24"
            android:drawablePadding="2dp"
            android:paddingLeft="5dp"
            android:paddingRight="5dp"
            android:shadowColor="#66000000"
            android:shadowRadius="4.0"
            android:textColor="@color/white"
            android:textSize="10dp"
            android:visibility="gone"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            tools:text="0:00" />

        <RadioButton
            android:id="@+id/selectRatioBT"
            android:layout_width="wrap_content"
            android:minWidth="0dp"
            android:minHeight="0dp"
            radioButton="@{tag.selected}"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

이상 안드로이드 코틀린 커스텀 앨범 만들기 포스팅이었습니다.

궁금하신 점이나 잘못된 게 있다면 댓글 달아주시면 감사하겠습니다!

반응형
Comments