凌峰创科服务平台

Android拍照上传服务器,如何实现?

整体流程概览

整个过程可以分为以下几个步骤:

Android拍照上传服务器,如何实现?-图1
(图片来源网络,侵删)
  1. 请求相机权限:在 Android 6.0 (API 23) 及以上,运行时权限是必须的。
  2. 启动相机应用:不推荐在 Activity 中直接嵌入相机预览,而是通过 Intent 调用系统自带的相机应用,这样更简单、兼容性更好。
  3. 接收拍照结果:相机应用拍照后,会返回一个包含图片文件路径的 Intent
  4. 图片压缩与优化:直接上传原图可能会导致文件过大,上传缓慢且消耗流量,通常需要对图片进行压缩。
  5. 构建网络请求:使用现代网络库(如 Retrofit + OkHttp)将图片文件作为 multipart/form-data 格式上传到服务器。
  6. 处理上传结果:在服务器响应后,根据结果提示用户成功或失败。

第一步:添加权限和依赖

AndroidManifest.xml 中添加权限

<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 读写外部存储权限 (用于保存和读取图片) -->
<!-- 注意:对于 Android 10 (API 29) 及以上,建议使用 MANAGE_EXTERNAL_STORAGE 或 scoped storage,但为了兼容旧版本,这里仍需添加 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 声明使用相机,避免在不支持相机的设备上安装 -->
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

build.gradle (Module: app) 中添加网络库依赖

我们使用 Retrofit + OkHttp + Gson 作为网络请求的黄金组合。

dependencies {
    // Retrofit: 类型安全的 HTTP 客户端
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    // Retrofit 的 Gson 转换器
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    // OkHttp: 一个高效的 HTTP 客户端
    implementation 'com.squareup.okhttp3:okhttp:4.9.3'
    // 图片压缩库 (推荐使用 Glide 或 Picasso 内置的,或专门的压缩库)
    // 这里我们使用 Glide 来加载和压缩
    implementation 'com.github.bumptech.glide:glide:4.12.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
}

第二步:请求相机权限

在启动相机之前,必须检查并请求 CAMERAWRITE_EXTERNAL_STORAGE 权限。

// 在 Activity 或 Fragment 中
private val PERMISSIONS_REQUEST_CODE = 1001
private val REQUIRED_PERMISSIONS = arrayOf(
    Manifest.permission.CAMERA,
    Manifest.permission.WRITE_EXTERNAL_STORAGE
)
// 检查权限
private fun hasPermissions(): Boolean {
    return REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
    }
}
// 请求权限
private fun requestPermissions() {
    ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, PERMISSIONS_REQUEST_CODE)
}
// 处理权限请求结果
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == PERMISSIONS_REQUEST_CODE) {
        if (hasPermissions()) {
            // 权限已授予,可以启动相机
            startCameraIntent()
        } else {
            // 权限被拒绝,可以提示用户
            Toast.makeText(this, "权限被拒绝,无法使用相机", Toast.LENGTH_SHORT).show()
        }
    }
}
// 在需要拍照的地方调用
fun onTakePhotoClick() {
    if (hasPermissions()) {
        startCameraIntent()
    } else {
        requestPermissions()
    }
}

第三步:启动相机并接收结果

创建一个 File 对象来存储图片

为了避免文件名冲突,最好使用时间戳来命名。

private fun createImageFile(): File {
    // 创建一个图片文件名,格式为 "JPEG_YYYYMMDD_HHMMSS_"
    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
    val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
    return File.createTempFile(
        "JPEG_${timeStamp}_", // 前缀
        ".jpg",               // 后缀
        storageDir            // 存储目录
    )
}

注意: getExternalFilesDir() 返回的目录是应用的专属空间,系统卸载应用时会自动删除,无需 WRITE_EXTERNAL_STORAGE 权限,但如果要调用系统相机,它通常需要将图片保存到公共目录(如 DCIM),WRITE_EXTERNAL_STORAGE 权限依然是必要的。

Android拍照上传服务器,如何实现?-图2
(图片来源网络,侵删)

启动相机 Intent

private var photoFile: File? = null // 用来保存创建的图片文件
private fun startCameraIntent() {
    photoFile = createImageFile()
    val photoURI = FileProvider.getUriForFile(
        this,
        "${applicationContext.packageName}.fileprovider", // 在 manifest.xml 中定义的 authorities
        photoFile!!
    )
    val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
    // 确保有相机应用可以处理这个 Intent
    if (takePictureIntent.resolveActivity(packageManager) != null) {
        startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PICTURE)
    }
}
// 在 Activity 中定义请求码
private val REQUEST_CODE_TAKE_PICTURE = 1002

AndroidManifest.xml 中配置 FileProvider

这是非常重要的一步,用于安全地共享文件。

<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>

然后在 res/xml/ 目录下创建一个 file_paths.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!-- <external-path name="my_images" path="Android/data/你的包名/files/Pictures/" /> -->
    <!-- 推荐使用 files-path,因为它更安全且不需要存储权限 -->
    <files-path name="my_images" path="Pictures/" />
</paths>

处理 onActivityResult

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_CODE_TAKE_PICTURE && resultCode == RESULT_OK) {
        photoFile?.let { file ->
            // 图片已成功保存到 file.path
            // 显示预览(可选)
            val bitmap = BitmapFactory.decodeFile(file.path)
            imageView.setImageBitmap(bitmap)
            // 上传图片
            uploadImage(file)
        }
    }
}

第四步:图片压缩 (可选但推荐)

uploadImage 方法中,我们首先对图片进行压缩,可以使用 GliderequestListener 来实现,也可以自己写压缩逻辑。

// 使用 Glide 进行压缩和获取 Bitmap
fun compressAndUploadImage(file: File) {
    Glide.with(this)
        .asBitmap()
        .load(file)
        .override(1024, 1024) // 设置目标尺寸
        .skipMemoryCache(true) // 跳过内存缓存
        .diskCacheStrategy(DiskCacheStrategy.NONE) // 跳过磁盘缓存
        .listener(object : RequestListener<Bitmap> {
            override fun onLoadFailed(
                e: GlideException?,
                model: Any?,
                target: Target<Bitmap>?,
                isFirstResource: Boolean
            ): Boolean {
                // 加载失败,可以在这里上传原图或提示错误
                uploadOriginalImage(file)
                return false
            }
            override fun onResourceReady(
                resource: Bitmap,
                model: Any?,
                target: Target<Bitmap>?,
                dataSource: DataSource?,
                isFirstResource: Boolean
            ): Boolean {
                // 压缩后的 Bitmap 已准备好
                val outputStream = ByteArrayOutputStream()
                resource.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) // 80% 质量
                val compressedBytes = outputStream.toByteArray()
                // 上传压缩后的字节数组
                uploadImageBytes(compressedBytes)
                return true
            }
        })
        .submit()
}
// 如果压缩失败,直接上传原图
fun uploadOriginalImage(file: File) {
    val requestBody = file.asRequestBody("image/jpeg".toMediaType())
    val multipartBody = MultipartBody.Part.createFormData(
        "image", // 服务器端接收图片的 key
        file.name,
        requestBody
    )
    uploadApiCall(multipartBody)
}
// 上传压缩后的字节数组
fun uploadImageBytes(bytes: ByteArray) {
    val requestFile = bytes.toRequestBody("image/jpeg".toMediaType())
    val multipartBody = MultipartBody.Part.createFormData(
        "image",
        "compressed_${System.currentTimeMillis()}.jpg",
        requestFile
    )
    uploadApiCall(multipartBody)
}

第五步:使用 Retrofit 上传图片

定义 Retrofit 接口

interface UploadApiService {
    @Multipart
    @POST("/api/upload") // 你的服务器上传接口
    suspend fun uploadImage(
        @Part image: MultipartBody.Part
    ): Response<UploadResponse> // 使用 Response 来获取原始响应信息
}
// 服务器返回的响应数据类 (示例)
data class UploadResponse(
    val success: Boolean,
    val message: String,
    val data: String? // 图片的 URL
)

创建 Retrofit 实例

object RetrofitClient {
    private const val BASE_URL = "http://你的服务器地址/"
    val instance: UploadApiService by lazy {
        val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        retrofit.create(UploadApiService::class.java)
    }
}

执行上传

// 在前面调用的 uploadImage 方法中
fun uploadApiCall(imagePart: MultipartBody.Part) {
    lifecycleScope.launch {
        try {
            val response = RetrofitClient.instance.uploadImage(imagePart)
            if (response.isSuccessful) {
                val uploadResponse = response.body()
                if (uploadResponse?.success == true) {
                    Toast.makeText(this@MainActivity, "上传成功: ${uploadResponse.data}", Toast.LENGTH_SHORT).show()
                } else {
                    Toast.makeText(this@MainActivity, "上传失败: ${uploadResponse?.message}", Toast.LENGTH_SHORT).show()
                }
            } else {
                Toast.makeText(this@MainActivity, "上传失败: ${response.code()}", Toast.LENGTH_SHORT).show()
            }
        } catch (e: Exception) {
            e.printStackTrace()
            Toast.makeText(this@MainActivity, "网络错误: ${e.message}", Toast.LENGTH_SHORT).show()
        }
    }
}

总结与最佳实践

  1. 使用 Intent 调用相机:简单、稳定,符合用户习惯。
  2. 权限管理:务必处理运行时权限,特别是 Android 10+ 的 scoped storage。
  3. FileProvider:安全地分享文件给其他应用(如相机)的必备组件。
  4. 图片压缩:强烈建议在上传前压缩图片,以提升用户体验和节省服务器资源。Glide 是一个很好的选择。
  5. 使用现代网络库:Retrofit + Coroutines/Kotlinx Serialization 是处理网络请求的现代化、简洁且高效的方式。
  6. 错误处理:对网络请求的失败、权限的拒绝、相机启动的失败等情况都要有友好的用户提示。
  7. UI 线程:网络请求和图片处理都是耗时操作,必须在协程或后台线程中执行,避免阻塞 UI 线程。

这个指南涵盖了从零开始实现拍照上传的全过程,你可以根据自己项目的具体需求进行调整和扩展。

Android拍照上传服务器,如何实现?-图3
(图片来源网络,侵删)
分享:
扫描分享到社交APP
上一篇
下一篇