본 포스팅은 Firebase Storage를 사용하여 다중 이미지를 업로드 하는 예제를 구현하고자 한다. Storage 사용이 처음이거나 단일 이미지 업로드를 먼저 구현하고 싶을 경우 단일 이미지 업로드 이전 포스팅을 먼저 확인해보자
- 화면 좌측의 카메라 아이콘의 이미지뷰를 클릭하면 앨범을 호출한다.
- 앨범에서 선택한 이미지 개수가 10개를 초과할 경우
- 이미지는 최대 10장까지 첨부할 수 있습니다. 토스트 메시지를 띄워준다.
- 앨범에서 선택한 이미지 개수가 선택 가능한 개수일 경우
- 선택한 이미지의 uri를 전달 받아 이미지를 RecyclerView에 표시한다.
- 앨범에서 선택한 이미지 개수가 선택 가능한 개수를 초과할 경우
- 선택 가능한 개수를 토스트 메시지로 띄워준다.
- 선택 된 이미지의 개수가 10개일 경우
- 앨범 진입이 불가능하며 이미지는 최대 10장까지 첨부할 수 있습니다. 토스트 메시지를 띄워준다.
- 업로드 하기 버튼을 클릭하면 Firebase Storage에 이미지를 업로드 한다.
단일 이미지 업로드 이전 포스팅 참고
단일 이미지 업로드 이전 포스팅 참고
<?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>
<?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>
<?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>
이미지를 다운로드 받아서 이미지뷰에 표시하기 위해 앱 수준 Gradle 파일에'com.github.bumptech.glide:glide:4.12.0'를 추가해준다.
dependencies {
implementation 'com.github.bumptech.glide:glide:4.12.0'
}
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)
}
}
}
📌 앨범에서 이미지 다중 선택
인텐트에 Intent.EXTRA_ALLOW_MULTIPLE 값을 true로 넣어줘야 멀티 선택이 가능하다.
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
registerForActivityResult.launch(intent)
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();
}
}
}