[Android /Kotlin] Firebase Storage 다중 이미지 업로드 (1)(+RecyclerView)

Subeen·2023년 3월 13일
0

Android

목록 보기
11/71
post-thumbnail

본 포스팅은 Firebase Storage를 사용하여 다중 이미지를 업로드 하는 예제를 구현하고자 한다. Storage 사용이 처음이거나 단일 이미지 업로드를 먼저 구현하고 싶을 경우 단일 이미지 업로드 이전 포스팅을 먼저 확인해보자

📍 결과 동영상

  • 화면 좌측의 카메라 아이콘의 이미지뷰를 클릭하면 앨범을 호출한다.
  • 앨범에서 선택한 이미지 개수가 10개를 초과할 경우
    • 이미지는 최대 10장까지 첨부할 수 있습니다. 토스트 메시지를 띄워준다.
  • 앨범에서 선택한 이미지 개수가 선택 가능한 개수일 경우
    • 선택한 이미지의 uri를 전달 받아 이미지를 RecyclerView에 표시한다.
  • 앨범에서 선택한 이미지 개수가 선택 가능한 개수를 초과할 경우
    • 선택 가능한 개수를 토스트 메시지로 띄워준다.
  • 선택 된 이미지의 개수가 10개일 경우
    • 앨범 진입이 불가능하며 이미지는 최대 10장까지 첨부할 수 있습니다. 토스트 메시지를 띄워준다.
  • 업로드 하기 버튼을 클릭하면 Firebase Storage에 이미지를 업로드 한다.

📍 Firebase Storage 만들기

단일 이미지 업로드 이전 포스팅 참고

📍 앱에 Firebase 인증 추가

단일 이미지 업로드 이전 포스팅 참고

📍 Code

👩🏻‍💻 RecyclerView 생성하기

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

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".fragment.MultiImageFragment">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"

                android:gravity="center"
                android:text="다중 이미지 업로드"
                android:textColor="@color/black"
                android:textSize="16sp"
                android:textStyle="bold" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:layout_margin="10dp"
                android:orientation="horizontal">

                <LinearLayout
                    android:layout_width="100dp"
                    android:layout_height="100dp"
                    android:background="@drawable/background_radius_stroke"
                    android:gravity="center"
                    android:orientation="vertical">

                    <ImageView
                        android:id="@+id/imageArea"
                        android:layout_width="50dp"
                        android:layout_height="50dp"
                        android:src="@drawable/camera" />

                    <TextView
                        android:id="@+id/countArea"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="0/0"
                        android:textColor="#999999"
                        android:textSize="14sp" />
                </LinearLayout>

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/recyclerview"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="5dp" />
            </LinearLayout>

            <Button
                android:id="@+id/btnRegister"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="10dp"
                android:background="@color/black"
                android:text="업로드 하기"
                android:textColor="@color/white"
                android:textSize="16sp" />
        </LinearLayout>

    </FrameLayout>
</layout>
  • background_radius_stroke.xml
    ImageView에 테두리를 설정하기 위한 xml 파일 생성하기
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <stroke
        android:width="1dp"
        android:color="#999999" />
    <solid android:color="#00ff0000" />
</shape>

👩🏻‍💻 RecyclerView 아이템 생성하기

  • item_image.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"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/imageArea"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginStart="3dp"
        android:layout_marginEnd="3dp"
        android:scaleType="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

👩🏻‍💻 RecyclerView Adapter 생성하기

이미지를 다운로드 받아서 이미지뷰에 표시하기 위해 앱 수준 Gradle 파일에'com.github.bumptech.glide:glide:4.12.0'를 추가해준다.

dependencies {
    implementation 'com.github.bumptech.glide:glide:4.12.0'
}
  • ImageAdapter.kt
class ImageAdapter(val context: Context, val items: ArrayList<Uri>) :
    RecyclerView.Adapter<ImageAdapter.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageAdapter.ViewHolder {
        val v =
            LayoutInflater.from(parent.context).inflate(R.layout.item_image, parent, false)
        return ViewHolder(v)
    }

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

    override fun onBindViewHolder(holder: ImageAdapter.ViewHolder, position: Int) {
        holder.bindItems(items[position])
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bindItems(item: Uri) {
            val imageArea = itemView.findViewById<ImageView>(R.id.imageArea)
            Glide.with(context).load(item).into(imageArea)
        }
    }
}

👩🏻‍💻 이미지 업로드 화면 Activity 구현하기

📌 앨범에서 이미지 다중 선택
인텐트에 Intent.EXTRA_ALLOW_MULTIPLE 값을 true로 넣어줘야 멀티 선택이 가능하다.
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
registerForActivityResult.launch(intent)

  • MultiImageFragment.kt
class MultiImageFragment : Fragment() {
    private lateinit var binding: FragmentMultiImageBinding
    private lateinit var uri: Uri
    lateinit var mainActivity: MainActivity
    private var uriList = ArrayList<Uri>()
    private val maxNumber = 10
    lateinit var adapter: ImageAdapter

    override fun onAttach(context: Context) {
        super.onAttach(context)

        mainActivity = context as MainActivity
    }

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

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_multi_image, container, false)
        printCount()
        // RecyclerView에 Adapter 연결하기
        adapter = ImageAdapter(mainActivity, uriList)
        binding.recyclerview.adapter = adapter
        // LinearLayoutManager을 사용하여 수평으로 아이템을 배치한다.
        binding.recyclerview.layoutManager =
            LinearLayoutManager(mainActivity, LinearLayoutManager.HORIZONTAL, false)

		// ImageView를 클릭할 경우 
        // 선택 가능한 이미지의 최대 개수를 초과하지 않았을 경우에만 앨범을 호출한다.
        binding.imageArea.setOnClickListener {
            if (uriList.count() == maxNumber) {
                Toast.makeText(
                    getActivity(),
                    "이미지는 최대 ${maxNumber}장까지 첨부할 수 있습니다.",
                    Toast.LENGTH_SHORT
                ).show();
                return@setOnClickListener
            }
            val intent = Intent(Intent.ACTION_PICK)
            intent.type = "image/*"
            intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
            registerForActivityResult.launch(intent)
        }

		// 업로드 하기 버튼을 클릭할 경우
        // for문을 통해 uriList.count()만큼 imageUpload 함수를 호출한다.
        binding.btnRegister.setOnClickListener {
            for (i in 0 until uriList.count()) {
                imageUpload(uriList.get(i), i)
                try {
                    Thread.sleep(500)
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
            }

        }

        return binding.root
    }

    private val registerForActivityResult =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            when (result.resultCode) {
                AppCompatActivity.RESULT_OK -> {
                    val clipData = result.data?.clipData
                    if (clipData != null) { // 이미지를 여러 개 선택할 경우
                        val clipDataSize = clipData.itemCount
                        val selectableCount = maxNumber - uriList.count()
                        if (clipDataSize > selectableCount) { // 최대 선택 가능한 개수를 초과해서 선택한 경우
                            Toast.makeText(
                                getActivity(),
                                "이미지는 최대 ${selectableCount}장까지 첨부할 수 있습니다.",
                                Toast.LENGTH_SHORT
                            ).show();
                        } else {
                            // 선택 가능한 경우 ArrayList에 가져온 uri를 넣어준다.
                            for (i in 0 until clipDataSize) {
                                uriList.add(clipData.getItemAt(i).uri)
                            }
                        }
                    } else { // 이미지를 한 개만 선택할 경우 null이 올 수 있다.
                        val uri = result?.data?.data
                        if (uri != null) {
                            uriList.add(uri)
                        }
                    }
                    // notifyDataSetChanged()를 호출하여 adapter에게 값이 변경 되었음을 알려준다.
                    adapter.notifyDataSetChanged()
                    printCount()
                }
            }
        }

    private fun printCount() {
        val text = "${uriList.count()}/${maxNumber}"
        binding.countArea.text = text
    }

	// 파일 업로드 
	// 파일을 가리키는 참조를 생성한 후 putFile에 이미지 파일 uri를 넣어 파일을 업로드한다.
    private fun imageUpload(uri: Uri, count: Int) {
        // storage 인스턴스 생성
        val storage = Firebase.storage
        // storage 참조
        val storageRef = storage.getReference("image")
        // storage에 저장할 파일명 선언
        val fileName = SimpleDateFormat("yyyyMMddHHmmss_${count}").format(Date())

        val mountainsRef = storageRef.child("${fileName}.png")
        val uploadTask = mountainsRef.putFile(uri)

        uploadTask.addOnSuccessListener { taskSnapshot ->
            // 파일 업로드 성공
            Toast.makeText(getActivity(), "사진 업로드 성공", Toast.LENGTH_SHORT).show();
        }.addOnFailureListener {
            // 파일 업로드 실패
            Toast.makeText(getActivity(), "사진 업로드 실패", Toast.LENGTH_SHORT).show();
        }
    }
}

👀 Storage 데이터 확인하기

profile
개발 공부 기록 🌱

0개의 댓글