Android自定义相机开发(类似OCR扫描相机):拍照与相册选择全解析
本文将详细介绍如何实现一个功能完整的Android自定义相机,包含拍照、相册选择、图片裁剪、压缩和上传等核心功能
一、背景与需求
在电商类App中,经常需要用户拍摄订单信息。系统相机体验不统一且无法定制,因此我们需要开发一个自定义相机,满足以下需求:
- ✅ 自定义相机界面与取景框
- ✅ 支持拍照和相册选择
- ✅ 支持图片裁剪和压缩功能
- ✅ 权限动态申请
- ✅ 多语言适配
- ✅ 图片上传服务
二、实现效果预览
拍照界面 | 相册选择 | 预览确认 |
---|---|---|
![]() |
![]() |
![]() |
三、核心实现代码
1. 相机初始化与预览
kotlin
private fun startCameraPreview() {
try {
releaseCamera()
camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK).apply {
val parameters = parameters.apply {
// 设置连续对焦模式
focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
}
setParameters(parameters)
// 根据设备旋转设置预览方向
setDisplayOrientation(90)
setPreviewDisplay(surfaceView?.holder)
startPreview()
}
} catch (e: IOException) {
Log.e(TAG, "Camera preview failed", e)
showToast("无法启动相机预览")
} catch (e: RuntimeException) {
Log.e(TAG, "Camera unavailable", e)
showToast("相机不可用或已被占用")
}
}
2. 权限动态申请
kotlin
private fun checkPermissions() {
val permissionsNeeded = mutableListOf<String>()
if (!checkCameraPermission()) {
permissionsNeeded.add(Manifest.permission.CAMERA)
}
if (!checkStoragePermission()) {
permissionsNeeded.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
if (permissionsNeeded.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsNeeded.toTypedArray(),
CAMERA_PERMISSION_REQUEST
)
} else {
initCamera()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
when (requestCode) {
CAMERA_PERMISSION_REQUEST -> {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initCamera()
} else {
showToast("需要相机权限才能使用此功能")
finish()
}
}
// 处理其他权限...
}
}
3. 图片压缩算法(智能压缩)
kotlin
private fun compressImageFile(originalFile: File): File {
// 1. 小于1MB不压缩
if (originalFile.length() < 1024 * 1024) return originalFile
// 2. 获取图片尺寸
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(originalFile.absolutePath, options)
// 3. 计算缩放比例 (目标分辨率1280px)
val (width, height) = options.run { outWidth to outHeight }
val scale = calculateScaleFactor(width, height, 1280f)
// 4. 解码时进行尺寸压缩
val decodeOptions = BitmapFactory.Options().apply {
inSampleSize = scale
inPreferredConfig = Bitmap.Config.RGB_565
}
// 5. 质量压缩 (80% → 50% 阶梯式)
return BitmapFactory.decodeFile(originalFile.absolutePath, decodeOptions)?.run {
val compressedFile = File.createTempFile("compressed_", ".jpg", cacheDir)
var quality = 80
do {
ByteArrayOutputStream().use { baos ->
compress(Bitmap.CompressFormat.JPEG, quality, baos)
if (baos.size() < 1024 * 1024) {
compressedFile.outputStream().use { fos ->
fos.write(baos.toByteArray())
}
return compressedFile
}
}
quality -= 10
} while (quality >= 50)
recycle()
originalFile
} ?: originalFile
}
private fun calculateScaleFactor(width: Int, height: Int, maxSize: Float): Int {
val scale = when {
width > height -> width / maxSize
else -> height / maxSize
}
return when {
scale <= 1 -> 1
scale <= 2 -> 2
scale <= 4 -> 4
scale <= 8 -> 8
else -> 8
}
}
4. 图片裁剪实现
kotlin
private fun getCroppedBitmap(imageView: TouchImageView, container: View): Bitmap {
// 1. 获取裁剪框位置
val location = IntArray(2)
container.getLocationOnScreen(location)
val (left, top) = location
val right = left + container.width
val bottom = top + container.height
// 2. 创建裁剪Bitmap
val bitmap = Bitmap.createBitmap(
container.width,
container.height,
Bitmap.Config.ARGB_8888
)
// 3. 绘制裁剪区域
val canvas = Canvas(bitmap)
canvas.translate(
-imageView.scrollX.toFloat() - left,
-imageView.scrollY.toFloat() - top
)
imageView.draw(canvas)
return bitmap
}
5. 多语言适配实现
kotlin
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(LocalManageUtil.setLocal(newBase))
}
override fun getResources(): Resources {
return LocalManageUtil.setLocal(baseContext).resources
}
// 在布局中使用资源ID
<TextView
android:id="@+id/btn_cancel"
android:text="@string/cancle"
... />
四、布局文件核心设计
xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<!-- 相机预览区域 -->
<RelativeLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_marginHorizontal="30dp">
<SurfaceView
android:id="@+id/camera_preview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- 双预览方案:拍照使用ImageView,相册使用TouchImageView -->
<ImageView
android:id="@+id/preview_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<com.riven.ui.TouchImageView
android:id="@+id/preview_touch_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
<!-- 取景框装饰元素 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView android:src="@mipmap/icon_left_top" ... />
<ImageView android:src="@mipmap/icon_right_top" ... />
<!-- 其他装饰元素... -->
</RelativeLayout>
</RelativeLayout>
<!-- 操作按钮区域 -->
<RelativeLayout
android:id="@+id/rl_bottom_take"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom">
<TextView
android:id="@+id/btn_cancel"
android:text="@string/cancle" />
<Button
android:id="@+id/btn_capture"
android:background="@mipmap/icon_camera_take" />
<TextView
android:id="@+id/btn_album"
android:text="Gallery" />
</RelativeLayout>
</FrameLayout>
五、性能优化要点
-
内存管理
- 及时回收Bitmap:
bitmap.recycle()
- 使用RGB_565配置减少内存占用
- 在onDestroy中释放资源
- 及时回收Bitmap:
-
相机资源释放
kotlinoverride fun onPause() { super.onPause() releaseCamera() } private fun releaseCamera() { camera?.apply { stopPreview() release() } camera = null }
-
异步处理
kotlinObservable.fromCallable { // 耗时操作:图片压缩 compressImageFile(file) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ compressedFile -> // 更新UI }, { error -> // 错误处理 })
-
大图处理策略
- 使用
inSampleSize
进行采样压缩 - 分步加载:先加载低分辨率预览图
- 使用ViewStub延迟加载
- 使用
六、总结与踩坑经验
开发中常见问题
-
相机方向问题:
- 解决方案:根据设备旋转动态设置
setDisplayOrientation()
- 注意前置/后置摄像头方向差异
- 解决方案:根据设备旋转动态设置
-
内存溢出(OOM):
- 使用BitmapFactory.Options进行采样压缩
- 及时回收不再使用的Bitmap
- 使用WeakReference引用大图对象
-
权限兼容性:
- Android 6.0+需要动态申请权限
- 处理用户拒绝权限的场景
- 适配Android 10的存储权限变更
-
图片旋转问题:
- 从相册选择的图片需要读取EXIF信息
- 使用Matrix进行旋转校正
后续扩展功能
-
增加高级功能:
- 手势缩放对焦
- 人脸识别框
- HDR模式支持
- 美颜滤镜
-
性能优化方向:
- 使用Camera2 API替代已废弃的Camera API
- 实现图片缓存池
- 使用Glide/Picasso等专业库管理图片
-
用户体验优化:
- 添加拍照动画
- 实现连拍功能
- 添加网格线辅助构图
完整项目源码已开源:GitHub链接
欢迎Star & Fork!如有任何问题,欢迎在评论区交流讨论~