iT邦幫忙

0

【Android/Kotlin】拍照/相簿照片上傳到Server

Tom 2021-06-16 17:09:534157 瀏覽
  • 分享至 

  • twitterImage
  •  

前言:

本篇文章內容注重在把照相照片/相簿照片轉成上傳至Server的 Multipart.Part格式,
之後有空會再補上去 Retrofit的分享文/images/emoticon/emoticon12.gif
大致上要做的事情如以下

相簿:

▶從相簿那邊拿回來的照片Uri會是content://Uri
▶把content://Uri改成真實路徑
▶再把它改成要上傳的格式 MultipartBody.Part格式

照相:

▶先創造一個臨時file
▶建立fileProvider,並把拍完照的照片儲存至指定位置
▶透過Uri拿到真實路徑
▶改成要上傳的MultipartBody.Part格式

相簿

一、新增權限

在Manifest新增以下權限

		//相機
		<uses-permission android:name="android.permission.CAMERA"/>
    
		//寫入外部資料權限
		<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    
		//讀外部資料權限
		<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

在Android6之後,需要向使用者詢問權限

fun permissionPhoto() {
ActivityCompat.requestPermissions(
requireActivity(),arrayOf(
android.Manifest.permission.CAMERA,
            android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
            android.Manifest.permission.READ_EXTERNAL_STORAGE
    ), 0
	)
}

二、接下來設定開啟相簿

binding.btnPickImage.setOnClickListener {
  val gallery =   
  Intent(Intent.ACTION_PICK,MediaStore.Images.Media.INTERNAL_CONTENT_URI)
  startActivityForResult(gallery, PICTUREFROMGALLERY)
        }

Intent裡面第一個參數填寫為ACTION_PICK
▶ACTION_PICK:獲取相簿照片,回傳值為 Content://Uri
▶ACTION_GET_CONTENT:獲取所有本地端圖片,Android 4.4以下跟ACTION_PICK回傳一樣,4.4則返回多種格式

並且設定requestCode

companion object {
        const val PICTUREFROMGALLERY = 1001
        const val PICTUREFROMCAMERA = 1002
    }

在startActivityForResult第二個參數填寫RequestCode,才可以跟onActivityResult對起來

三、拿返回值

因為我們是用startActivityForResult,所以我們的回傳資料從onActivityResult取得

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK && requestCode == PICTUREFROMGALLERY) {
            if (data != null && data?.data != null) {
                imageUri = data?.data
                imageView.setImageURI(imageUri)
                imagePath = imageUri?.let { getPathFromUri(it) }
				val file: File = File(imagePath)         
                val requestBody: RequestBody = file.asRequestBody("multipart/form-data".toMediaTypeOrNull())               
                val multipart: MultipartBody.Part = MultipartBody.Part.createFormData("image",file.name,requestBody)              
                viewModel.uploadImage(multipart)
            }
}

關於onActivityResult 我們可以透過

if (resultCode == Activity.RESULT_OK && requestCode == PICTUREFROMGALLERY)

▶reqeustCode來判別是從哪個來源的
▶resultCode來判別回傳結果

相簿我們可以透過 data.data去拿到 Content://Uri(該uri不能直接上傳)
所以我們需要把它轉成可以上傳的格式
再過來我們要創一個getPathFromUri的funtion

private fun getPathFromUri(uri: Uri): String{
        val projection = arrayOf(MediaStore.MediaColumns.DATA)
        val cursor = requireActivity().contentResolver.query(uri,projection,null,null,null)
        val column_index: Int? = cursor?.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) ?: null
        cursor?.moveToFirst()
        val result: String = column_index?.let { cursor?.getString(it) } ?: ""
        cursor?.close()
        return result
    }

▶val projection = arrayOf(MediaStore.MediaColumns.DATA): 需要擷取的資料欄位,以下顯示只有DATA的欄位,所以不能印出其他資訊(已被棄用,之後再回來改)
▶cursor:逐一看每一列的欲查詢查詢欄位內容(參考如下)

https://ithelp.ithome.com.tw/upload/images/20210616/20138017z1yGnyTbn5.png
(每一欄位代表項目,每一列代表實體,圖片取自Android官方文件)

▶column_index:取得indext
▶cursor?.moveToFirst():因為cursor的初始位置是 -1,所以要透過此行讓他移動到0否則會報以下的錯

android.database.CursorIndexOutOfBoundsException: Index -1 requested, with a size of 1

▶result:拿到剛剛搜尋到的結果 path

繼續回到onActivitiyResult

			imagePath = imageUri?.let { getPathFromUri(it) }
			val file: File = File(imagePath)         
            val requestBody: RequestBody = file.asRequestBody("multipart/form-data".toMediaTypeOrNull())               
            val multipart: MultipartBody.Part = MultipartBody.Part.createFormData("image",file.name,requestBody)              
                viewModel.uploadImage(multipart)

▶imagePath:拿到剛剛getPathFromUri的返回值
▶file:透過path創建file
▶requestBody:建立requestBody,因為這次上傳照片的 content-Type是multipart/form-data,並用toMediaTypeOrNull()轉變成asRequestBody所需要的MediaType
▶multipart:透過MultipartBody.Part.創建FormData,第一個參數是要寫Server要的Body的Key名稱,不然會上傳不上去

大功告成啦!!
/images/emoticon/emoticon01.gif

照相

一、創立Intent

一樣,首先我們先做出Intent
照相的話有分成兩種

在Intent沒有給予儲存Uri的話,那就會返回縮圖,可以透過以下的code拿到bitmap顯示

binding.btnTakePicture.setOnClickListener{
          val camera = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
          startActivityForResult(camera, PICTURE_FROM_CAMERA)
}

並在onActivityResult拿到bitmap,並顯示

                 val extras = data?.extras
                 val imageBitmap = extras?.get("data") as Bitmap
                 imageView.setImageBitmap(imageBitmap)

但是我們要上傳,所以我們就要在Intent的地方先建立一個臨時的tmpFile,並用他拿到realPath,那就需要有個fileProvider

我也是很好奇為什麼相簿不用碰到fileProvider,而拍照就要呢?

/後來爬文後發現,onActivityResult方法中可以獲取授予臨時許可權的Content URI,所以就可以直接用它去轉換並上傳
但是照相功能則不是返回contentUri,所以我們要給予它一個臨時的file,並再獲取Content URI/

二、創立fileProvider

打開Manifest

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.example.bookreports.provider"
    android:exported="false"
    android:grantUriPermissions="true">
<meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths">

</meta-data>


</provider>

▶authorities: 一個手機裡面只能有一個唯一識別
▶exported: 是否給其他應用使用,這邊要false 拒絕外部直接訪問
▶grantUriPermissions:授予臨時Uri權限

再過來去建立 xml檔案

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

<!--    外部儲存空間的根目錄,等同 Environment.getExternalStorageDirectory()獲取的路徑-->
        <external-path
        name="external_files"
        path="."/>

</paths>

再把它放到Manifest的 fileProvider內的 android:resource

三、回到Intent

既然我們已經創建好fileProvider後,我們就回來看Intent

binding.btnTakePicture.setOnClickListener{

     val camera = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
     //先新增一張照片
     val tmpFile = File(context?.getExternalFilesDir(null),"image.jpg")
     
	//建立uri,這邊拿到的格式就會是 content://了
     val outputFileUri = FileProvider.getUriForFile(requireActivity(),"com.example.bookreports.provider",tmpFile)

			imageUri = outputFileUri
            val path: String = tmpFile.absolutePath
			imagePath = path
  
			//指定為輸出檔案的位置
            camera.putExtra(MediaStore.EXTRA_OUTPUT,outputFileUri)
startActivityForResult(camera, PICTURE_FROM_CAMERA)
}

▶tmpFile:這裡根據parent抽象路徑名和child路徑名字串建立一個新File
▶outputFileUri:getUriFromFile,第二個參數是在Manifest裡面填的provider的authority,必須一致
▶path:透過剛剛建立的file,來拿到path

好的,既然我們都拿到path了,我們就來到 onActivityResult

四、轉換成MultipartBody.Part

if(imageUri != null){
imageView.setImageURI(imageUri)
val file: File = File(imagePath)
val requestBody:RequestBody = file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
val multipart: MultipartBody.Part = MultipartBody.Part.createFormData("image",file.name,requestBody)
viewModel.uploadImage(multipart)
}

後續就跟之前大同小異
▶imagePath:拿到剛剛從Intent的值
▶file:透過path創建file
▶requestBody:建立requestBody,因為這次上傳照片的 content-Type是multipart/form-data,並用toMediaTypeOrNull()轉變成asRequestBody所需要的MediaType
▶multipart:透過MultipartBody.Part.創建FormData,第一個參數是要寫Server要的Body的Key名稱

大功告成!!大功告成!!

/images/emoticon/emoticon74.gif


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
rabbit
iT邦新手 4 級 ‧ 2021-12-14 16:15:05

請問gradle添加的依賴是哪一種呢?

Tom iT邦新手 4 級 ‧ 2021-12-20 13:19:51 檢舉

單就上傳照片的部分,都是用原生,沒有用第三方套件喔~
後續上傳至 Server 則使用到 Retrofit,是有遇到什麼嗎? 或是你可以研究一下使用 imagePicker 這個第三方套件

我要留言

立即登入留言