Developer Geek

Android 복수 사진 첨부 From Activity In Kotlin 본문

안드로이드/Service

Android 복수 사진 첨부 From Activity In Kotlin

devGeek 2022. 4. 28. 09:00
반응형

개요

시나리오

Activity에서 갤러리 접근 버튼을 클릭 하면 접근 권한 확인 후 디바이스 갤러리에 접근한다. 사진 복수 개 선택 시, 해당 사진들을 RecyclerView에 보여준다. (단, 4개를 초과해서 선택 시 Toast 메시지를 통해 최대 4개임을 명시한다.)

실행 화면

프로젝트 구조

ViewBinding, Coil 사용 - In build.gradle(:app)

ViewBinding을 사용하기 위해 viewBinding { enabled = true }build.gradle(:app)에 추가했다.
ImageView에 이미지 첨부를 위해 Coil을 사용했고 의존성으로 implementation "io.coil-kt:coil:2.0.0-rc03"build.gradle(:app)에 추가했다.

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.example.showgalleryapplication"
        minSdk 26
        targetSdk 31
        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.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    // Coil
    implementation "io.coil-kt:coil:2.0.0-rc03"
}

Code

AndroidManifest.xml

갤러리 접근 권한을 얻기 위해 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />을 추가한다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.showgalleryapplication">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ShowGalleryApplication">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

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"
    android:padding="20dp"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="0dp"
        android:layout_height="500dp"
        android:background="#ffececec"
        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:spanCount="2"
        tools:listitem="@layout/list_item" />

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_show_gallery"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="#6492EF"
        android:text="사진첨부"
        android:textColor="@color/white"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

list_item.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="180dp"
    android:layout_height="180dp"
    android:padding="10dp">

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scaleType="fitXY"
        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" />

</androidx.constraintlayout.widget.ConstraintLayout>

MyListAdapter.kt - 리사이클러뷰 어댑터(ListAdapter 사용)

import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
import com.example.showgalleryapplication.databinding.ListItemBinding

class MyListAdapter:
    ListAdapter<Uri, MyListAdapter.ViewHolder>(diffUtil) {

    inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(uri: Uri) {
            binding.imageView.load(uri)
        }

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(ListItemBinding.inflate(LayoutInflater.from(parent.context),
            parent,
            false))
    }

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

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<Uri>() {
            override fun areItemsTheSame(oldItem: Uri, newItem: Uri): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: Uri, newItem: Uri): Boolean {
                return oldItem == newItem
            }
        }
    }
}

MainActivity.kt

showGallery(this@MainActivity, Intent.EXTRA_ALLOW_MULTIPLE)

private fun showGallery(activity: Activity, extra: String) {
    val intent = Intent(Intent.ACTION_PICK)
    intent.type = "image/*"
    intent.putExtra(extra, true)
    activity.startActivityForResult(intent, PICK_IMAGE_FROM_GALLERY)
}

복수 개의 이미지 선택을 위해, 갤러리 Intent에 "Intent.EXTRA_ALLOW_MULTIPLE" : true 데이터를 담아준다.

import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.example.showgalleryapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val PICK_IMAGE_FROM_GALLERY = 1000
    private val PICK_IMAGE_FROM_GALLERY_PERMISSION = 1010
    private val mAdapter = MyListAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 이미지를 보여줄 리사이클러 뷰 Init
        binding.recyclerView.adapter = mAdapter

        // 사진첨부 버튼 클릭 이벤트 구현
        binding.btnShowGallery.setOnClickListener {
            when {
                // 갤러리 접근 권한이 있는 겨우
                ContextCompat.checkSelfPermission(this,
                    android.Manifest.permission.READ_EXTERNAL_STORAGE
                ) == PackageManager.PERMISSION_GRANTED -> showGallery(this@MainActivity,
                    Intent.EXTRA_ALLOW_MULTIPLE)

                // 갤러리 접근 권한이 없는 경우 && 교육용 팝업을 보여줘야 하는 경우
                shouldShowRequestPermissionRationale(android.Manifest.permission.READ_EXTERNAL_STORAGE)
                -> showPermissionContextPopup()

                // 권한 요청 하기
                else -> requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),
                    PICK_IMAGE_FROM_GALLERY_PERMISSION)
            }
        }
    }

    private fun showGallery(activity: Activity, extra: String) {
        val intent = Intent(Intent.ACTION_PICK)
        intent.type = "image/*"
        intent.putExtra(extra, true)
        activity.startActivityForResult(intent, PICK_IMAGE_FROM_GALLERY)
    }

    private fun showPermissionContextPopup() {
        AlertDialog.Builder(this)
            .setTitle("권한이 필요합니다.")
            .setMessage("갤러리 접근 권한이 필요합니다.")
            .setPositiveButton("동의하기") { _, _ ->
                requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),
                    PICK_IMAGE_FROM_GALLERY_PERMISSION)
            }
            .setNegativeButton("취소하기") { _, _ -> }
            .create()
            .show()
    }

    // 사진 선택(갤러리에서 나온) 이후 실행되는 함수
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == PICK_IMAGE_FROM_GALLERY && resultCode == Activity.RESULT_OK) {
            val list = ArrayList<Uri>()

            data?.let { it ->
                if (it.clipData != null) {   // 사진을 여러개 선택한 경우
                    val count = it.clipData!!.itemCount
                    if (count > 4) {
                        Toast.makeText(this@MainActivity, "사진은 4장까지 선택 가능합니다.", Toast.LENGTH_SHORT)
                            .show()
                        return
                    }

                    for (i in 0 until count) {
                        val imageUri = it.clipData!!.getItemAt(i).uri
                        list.add(imageUri)
                    }
                } else {      // 1장 선택한 경우
                    val imageUri = it.data!!
                    list.add(imageUri)
                }
            }
            // adapter 에 Image 저장
            mAdapter.submitList(list)
        }
    }

    // 권한 요청 승인 이후 실행되는 함수
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray,
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {
            PICK_IMAGE_FROM_GALLERY_PERMISSION -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)
                    showGallery(this@MainActivity, Intent.EXTRA_ALLOW_MULTIPLE)
                else
                    Toast.makeText(this, "권한을 거부하셨습니다.", Toast.LENGTH_SHORT).show()

            }
        }
    }
}

개념 학습 - "접근 권한"

권한 요청 워크플로우

ContextCompat.checkSelfPermission(): 앱이 이미 권한이 부여되었는지 확인

사용자가 이미 앱에 특정 권한을 부여했는지 확인하려면 checkSelfPermission(Context context, String permission) 메서드에 권한을 전달한다. 이 메서드는 앱에 권한이 있는지에 따라서 PERMISSION_GRANTED 또는 PERMISSION_DENIED를 반환한다.

ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)

shouldShowRequestPermissionRationale(): 권한 요청 전에 권한이 필요한 이유를 사용자에게 설명해야 하는 지에 대한 여부 반환

requestPermissions()를 호출하기 전에 앱에서 권한을 요청하는 이유를 사용자에게 설명하는 것이 좋다. 연구에 따르면 사용자가 앱에서 권한이 필요한 이유를 알고 있을 때 권한 요청을 훨씬 더 편안하게 느낀다고 한다.

ContextCompap.checkSelfPermission() 메서드가 PERMISSION_DENIED를 반환하면 shouldShowRequestPermissionRationale()을 호출해라. 이 메서드가 true를 반환하면 교육용 UI를 사용자에게 표시한다. 이 UI에서 사용자가 사용 설정하려는 기능에 특정 권한이 필요한 이유를 설명해야 한다.

requestPermissions(@NonNull String[] permissions, int requestCode): 권한 요청

사용자에게 교육용 UI 가 표시되었거나, shouldShowRequestPermissionRationale()의 반환 값에서 이번에는 교육용 UI를 표시하지 않아도 된다고(false) 나타내면 권한을 요청한다.

permissions에 해당하는 권한들을 요청한다. 단, permissions에 있는 권한들은 manifest에 꼭 추가 되어있어야 한다.

권한 요청이 완료되면 onRequestPermissionResult() 함수를 호출한다.

반응형
Comments