【Android】Kotlin实现选取相片&拍照&截图&图片上传(兼容android 9-14)(超详细完整版)

13 篇文章 0 订阅
5 篇文章 0 订阅

需求

技术栈 : android 原生 & kotlin 1.6.20
需求 : ① 选取相片&拍照 ② 截图(CropImage) ③ 上传图片(含网络接口请求封装)

代码

一. build.gradle (Module: app) 配置依赖

		// 网络请求
        implementation 'com.squareup.okhttp3:okhttp:4.9.0'
        implementation "com.squareup.retrofit2:retrofit:2.9.0"
        implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
        implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
        // 图片截图
        implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0"
        // 
        implementation 'com.github.bumptech.glide:glide:4.12.0'
        annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'

二. AndroidManifest.xml

1. 打开读写及摄像权限

	<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>

2. 定义 FileProvider
在你的应用的 AndroidManifest.xml 文件中定义一个 元素来注册 FileProvider。你需要指定一个唯一的 authority(通常是你的应用包名加上一个唯一标识符),以及一个或多个 元素来指定文件目录。
坑1 : 这里必须是"@xml/file_paths", 不能是"@layout/file_paths"" 其他目录, 否则file_paths 里的 等标签无法识别

  <application>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>

3. 声明 CropImageActivity

		<activity
            android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
            android:screenOrientation="portrait"
            android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
 		 </activity>

4. 配置 queries 元素指定应用需要查询相机应用的权限
原文 : https://blog.csdn.net/T_Y_F_/article/details/135709241

原因 : Android 11及更高版本中,由于引入了更严格的权限控制和隐私策略,应用需要通过 元素在清单文件中声明对其他应用组件的查询权限。

问题 : 否则 takePictureIntent.resolveActivity(packageManager) != null, android 12手机打不开摄像头页

<queries>
    <!-- Camera -->
    <intent>
        <action android:name="android.media.action.IMAGE_CAPTURE" />
    </intent>
</queries>

三. 在 res/xml 目录下创建 file_paths.xml 文件,并配置正确的路径:

1. xml/file_paths

<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--    <root-path name="root" path="" /> //代表设备的根目录new File("/");-->
<!--    <files-path name="files" path="files/" /> // 代表context.getFilesDir()-->
<!--    <files-path name="images" path="images/" /> // 代表context.getFilesDir()-->
<!--    <cache-path name="cache" path="cache/" /> // 代表context.getCacheDir()-->
<!--    <external-path name="external" path="external/" /> // 代表Environment.getExternalStorageDirectory()-->
<!--    <external-files-path name="external_images" path="external/images/" /> // 代表context.getExternalFilesDirs()-->
<!--    <external-cache-path name="external_cache" path="external/cache/" /> // 代表getExternalCacheDirs()-->

<!--    // 代表context.getExternalFilesDirs()-->
    <files-path
        name="my_files"
        path="Pictures" />
    <external-files-path
        name="external_files"
        path="." />
    <external-files-path name="external_files" path="external/files/" />
</paths>

四. Activity.kt (逻辑代码)

import android.Manifest
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.os.Looper
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide
import com.lzx.musiclib.R
import com.lzx.musiclib.TestApplication
import com.lzx.musiclib.http.NetworkService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.IOException
import android.icu.text.SimpleDateFormat
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import com.bumptech.glide.request.RequestOptions
import com.lzx.musiclib.http.RetrofitClient
import com.theartofdev.edmodo.cropper.CropImage
import com.theartofdev.edmodo.cropper.CropImageView
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.util.Date
import retrofit2.Response

// 定义上传图片后接口返回对象
data class UploadResponse(
    val retCode: String,
    val retMsg: String,
    val profile: String
)

class PersonnalInfoActivity : AppCompatActivity() {
	// 权限申请
    private var REQUEST_PERMISSION = 1

    private lateinit var pickImageLauncher: ActivityResultLauncher<Intent>
    private lateinit var takePictureLauncher: ActivityResultLauncher<Intent>
    private lateinit var cropImageLauncher: ActivityResultLauncher<Intent>

    @RequiresApi(Build.VERSION_CODES.N)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_personal_info)

		selectPhoto.setOnClickListener {
             pickImage()
        }

        takePhoto.setOnClickListener {
            takePicture()
        }
		
		// 打开读写及摄像权限(android 14似乎有动态权限申请, 这里我没有做细分, 但是会有权限询问框)
        ActivityCompat.requestPermissions(
            this,
            arrayOf(
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.CAMERA
            ),
            REQUEST_PERMISSION
        )

        // 设置 ActivityResultLauncher
        pickImageLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
                if (result.resultCode == RESULT_OK) {
                    val data = result.data
                    startCropImageActivity(data?.data!!)
                }
            }
		
		// 创建一个ActivityResultLauncher 用于打开拍照后的相片
		// 拍照没有和选取相片共用startCropImageActivity, 直接写在takePictureLauncher 里面, 共用的话拍照打不开截图页面
        takePictureLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
                val data = result.data
                if (result.resultCode == RESULT_OK) {
                    val imageBitmap = data?.extras?.get("data") as Bitmap
                    val file = File(getExternalFilesDir(null), "temp_image.jpg")
                    // 压缩图片 展现在截图页面
                    file.outputStream().use { out ->
                        imageBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
                    }
					// getExternalFilesDir 获取目录文件方式和file-path.xml定义的文件位置对应(这里没验证过)
                    val imageUri = Uri.fromFile(File(getExternalFilesDir(null), "temp_image.jpg"))
                    // 截图页面
                    val cropImageIntent = CropImage.activity(imageUri)
                        .setGuidelines(CropImageView.Guidelines.ON)
                        .setAspectRatio(1, 1)
                        .getIntent(this)
                    cropImageLauncher.launch(cropImageIntent)
                }
            }
            
        // 创建一个ActivityResultLauncher 用于处理裁剪结果
        cropImageLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
                if (result.resultCode != null) {
                    val resultUri = result.data?.let { CropImage.getActivityResult(it).uri }
                    if (resultUri != null) {
                        val token = SharedPrefs.getToken(TestApplication.context)
						// 上传图片
                        CoroutineScope(Dispatchers.IO).launch {
                            if (token != null) {
                                performUpload(File(resultUri.path), token)
                            }
                        }
                    }
                } else {
                    Log.d("MainActivity", "Crop image failed.")
                }
            }
    }

    private fun pickImage() {
        val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        intent.type = "image/*"  // 通过 setType 方法限制类型为图像, 否则有些android版本会同时显示视频
        pickImageLauncher.launch(intent)
    }

    @RequiresApi(Build.VERSION_CODES.N)
    private fun takePicture() {
        val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        if (takePictureIntent.resolveActivity(packageManager) != null) {
            takePictureLauncher.launch(takePictureIntent)
        }
    }

    @RequiresApi(Build.VERSION_CODES.N)
    private fun createImageFile(): File {
        val imageFileName = "temp"
        return File.createTempFile(
            imageFileName,  /* prefix */
            ".jpg",         /* suffix */
            getExternalFilesDir(Environment.DIRECTORY_PICTURES), /* directory */
        )
    }

    @RequiresApi(Build.VERSION_CODES.N)
    private fun getImageUri(): Uri {
        val imageFile = createImageFile()
        return FileProvider.getUriForFile(this, "${applicationId}.fileprovider", imageFile)
    }
	
    private fun startCropImageActivity(uri: Uri) {
        val cropImageIntent = CropImage.activity(uri)
            .setGuidelines(CropImageView.Guidelines.ON)
            .setAspectRatio(1, 1)
            .getIntent(this)
        cropImageLauncher.launch(cropImageIntent)
    }
	
	// 上传图片
    private suspend fun uploadImage(file: File, token: String): Response<UploadResponse> {
        val requestBody: RequestBody = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
        val body = MultipartBody.Part.createFormData("file", file.name, requestBody)
        val idOfUser = "001"

        val apiService = RetrofitClient.provideApiService()
        return withContext(Dispatchers.IO) {
            apiService.uploadImage(body, token, idOfUser).execute()
        }
    }

    suspend fun performUpload(file: File, token: String) {
        val response = withContext(Dispatchers.IO) {
            uploadImage(file, token)
        }

        if (response.isSuccessful) {
            val uploadResponse = response.body()
            if (uploadResponse != null) {
                if (uploadResponse.retCode == "0") {
                // 处理上传图片成功后的操作
                    val profile = uploadResponse.profile
                    runOnUiThread {
                        Glide.with(this)
                            .load(profile)
                            .apply(RequestOptions().timeout(5000)) // 为避免超时, 设置超时时间为 5 秒
                            .into(avatarImg)
                        Toast.makeText(
                            TestApplication.context,
                            "Upload successfully",
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                } else {
                    Looper.prepare();
                    Toast.makeText(
                        TestApplication.context,
                        "Upload failed",
                        Toast.LENGTH_SHORT
                    ).show()
                    Looper.loop();
                }
            } else {
                Looper.prepare();
                Toast.makeText(
                    TestApplication.context,
                    "Upload failed",
                    Toast.LENGTH_SHORT
                ).show()
                Looper.loop();
                Log.e("MainActivity", "Error uploading image: ${response.code()}")
            }
        } else {
            Looper.prepare();
            Toast.makeText(
                TestApplication.context,
                "Upload failed",
                Toast.LENGTH_SHORT
            ).show()
            Looper.loop();
            Log.e("MainActivity", "Error uploading image: ${response.code()}")
        }
    }
}

五. 网络请求封装

http/Api.kt
import com.lzx.musiclib.act.UploadResponse
import okhttp3.MultipartBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.Call

	// 这里有三个参数
	// file, token, userId
    interface ApiService {
        @Multipart
        @POST("/uploadFile")
        fun uploadImage(@Part file: MultipartBody.Part, @Header("Authorization") token: String, @Query("id_of_user") idOfUser: Int?,): Call<UploadResponse>  // 这里的UploadResponse就是和Activity.kt 对应的UploadResponse对象
    }
http/NetworkService.kt
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitClient {
    private const val BASE_URL = "your api url"
    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(OkHttpClientProvider.provideClient())
            .build()
    }
    fun provideApiService(): Api.ApiService = retrofit.create(Api.ApiService::class.java)
}

object OkHttpClientProvider {
    private val httpClient: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            })
            .build()
    }

    fun provideClient(): OkHttpClient = httpClient
}

完成 !
其中有很多的坑, 很多情况下ide不会报错的, 比如开通了权限, 却打不开摄像头, 或者打开摄像头拍摄完成后直接退出返回主页,而不进入裁剪页面…

这里面的功能其实每个拆分出来都可以作为一篇博客叙述, 偷个懒, 大杂烩放这啦!
再梳理下里面具体的功能!
① 打开摄像头/访问相册权限 (兼容android 9-14)
② 选取相片/拍摄, 存放临时路径
③ 引入CropImage依赖, 配置, 打开cropImageActivity, 进行截图操作
④ 封装图片上传网络请求, 上传图片

关于和uniapp开发的感受对比 :

虽然使用了CropImage第三方库, 但是相比较uniapp插件, 原生 android需要自己写的还是很多, 比如选取图片, 打开摄像头, 图片上传, uniapp都有自己封装的一套api, 直接调用即可, 而android把这些交回给了开发者, 第一次写android 原生代码, 也是学到了好多~ 后续会继续出android 原生开发的博客, 日历底部弹窗组件~ 内置缓存使用~ RecyclerView遍历较复杂的列表元素~ 等等~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值