基于CameraX与Jetpack Compose实现拍照后指定区域裁剪功能
实现带引导框的相机预览与拍摄后裁剪功能
核心思路
- 预览界面布局:以相机预览控件(如
TextureView)为底层,上层叠加半透明遮罩,中间留出矩形引导框,引导用户对齐拍摄对象。 - 相机适配:确保预览画面比例与相机输出图片比例一致,避免后续裁剪偏差;处理屏幕旋转导致的预览方向问题。
- 坐标转换与裁剪:拍摄完成后,将界面引导框的坐标转换为原始图片中的对应区域,再执行裁剪操作。
安卓平台实现示例
1. 布局文件(activity_camera.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"> <!-- 相机预览控件 --> <TextureView android:id="@+id/camera_preview" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- 半透明遮罩 + 中间镂空引导框 --> <View android:id="@+id/mask_view" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/camera_mask" /> <!-- 拍摄按钮 --> <Button android:id="@+id/capture_btn" android:layout_width="80dp" android:layout_height="80dp" android:layout_gravity="bottom|center_horizontal" android:layout_marginBottom="30dp" android:background="@drawable/round_button" /> </FrameLayout>
2. 遮罩层Drawable(camera_mask.xml)
用LayerList实现半透明背景+中间镂空矩形:
<?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 半透明背景 --> <item> <shape android:shape="rectangle"> <solid android:color="#AA000000" /> </shape> </item> <!-- 中间镂空区域(带白色边框) --> <item> <shape android:shape="rectangle"> <solid android:color="@android:color/transparent" /> <stroke android:width="2dp" android:color="#FFFFFF" /> <!-- 通过padding控制引导框的大小和位置 --> <padding android:left="100dp" android:right="100dp" android:top="200dp" android:bottom="200dp" /> </shape> </item> </layer-list>
3. 相机逻辑实现(CameraActivity.kt)
import android.Manifest import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import android.hardware.Camera import android.os.Bundle import android.view.TextureView import android.view.View import android.widget.Button import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat class CameraActivity : AppCompatActivity(), TextureView.SurfaceTextureListener { private var camera: Camera? = null private lateinit var previewView: TextureView private lateinit var captureBtn: Button private lateinit var maskView: View private var previewWidth = 0 private var previewHeight = 0 // 引导框在界面中的坐标与尺寸 private var guideRectLeft = 0 private var guideRectTop = 0 private var guideRectWidth = 0 private var guideRectHeight = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_camera) previewView = findViewById(R.id.camera_preview) captureBtn = findViewById(R.id.capture_btn) maskView = findViewById(R.id.mask_view) // 检查相机权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ) { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 100) } else { previewView.surfaceTextureListener = this } captureBtn.setOnClickListener { camera?.takePicture(null, null, Camera.PictureCallback { data, _ -> processCapturedImage(data) }) } } override fun onSurfaceTextureAvailable(surface: android.graphics.SurfaceTexture, width: Int, height: Int) { camera = Camera.open() camera?.apply { val parameters = parameters // 选择适配屏幕比例的预览尺寸 val previewSize = getOptimalPreviewSize(parameters.supportedPreviewSizes, width, height) previewSize?.let { parameters.setPreviewSize(it.width, it.height) previewWidth = it.width previewHeight = it.height } parameters.pictureFormat = Camera.Parameters.JPEG this.parameters = parameters calculateGuideRect() try { setPreviewTexture(surface) startPreview() } catch (e: Exception) { e.printStackTrace() } } } // 计算引导框的实际坐标 private fun calculateGuideRect() { val paddingLeft = maskView.paddingLeft val paddingTop = maskView.paddingTop val paddingRight = maskView.paddingRight val paddingBottom = maskView.paddingBottom guideRectLeft = paddingLeft guideRectTop = paddingTop guideRectWidth = maskView.width - paddingLeft - paddingRight guideRectHeight = maskView.height - paddingTop - paddingBottom } // 处理拍摄后的图片裁剪 private fun processCapturedImage(data: ByteArray) { // 1. 解码图片并处理竖屏旋转 val originalBitmap = BitmapFactory.decodeByteArray(data, 0, data.size) val matrix = Matrix() matrix.postRotate(90f) // 竖屏拍摄时图片默认旋转90度,需修正 val rotatedBitmap = Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.width, originalBitmap.height, matrix, true) originalBitmap.recycle() // 2. 计算预览与实际图片的缩放比例 val scaleX = rotatedBitmap.width.toFloat() / previewView.width.toFloat() val scaleY = rotatedBitmap.height.toFloat() / previewView.height.toFloat() // 3. 转换引导框坐标到图片坐标系 val imgLeft = (guideRectLeft * scaleX).toInt() val imgTop = (guideRectTop * scaleY).toInt() val imgWidth = (guideRectWidth * scaleX).toInt() val imgHeight = (guideRectHeight * scaleY).toInt() // 4. 执行裁剪 val croppedBitmap = Bitmap.createBitmap(rotatedBitmap, imgLeft, imgTop, imgWidth, imgHeight) rotatedBitmap.recycle() // 此处可将croppedBitmap保存或用于后续业务逻辑 // ... // 重启预览 camera?.startPreview() } // 选择最适配屏幕的预览尺寸 private fun getOptimalPreviewSize(sizes: List<Camera.Size>, w: Int, h: Int): Camera.Size? { val targetRatio = h.toDouble() / w var optimalSize: Camera.Size? = null var minDiff = Double.MAX_VALUE for (size in sizes) { val ratio = size.width.toDouble() / size.height if (Math.abs(ratio - targetRatio) < minDiff) { minDiff = Math.abs(ratio - targetRatio) optimalSize = size } } return optimalSize } override fun onSurfaceTextureSizeChanged(surface: android.graphics.SurfaceTexture, width: Int, height: Int) { calculateGuideRect() } override fun onSurfaceTextureDestroyed(surface: android.graphics.SurfaceTexture): Boolean { camera?.stopPreview() camera?.release() camera = null return true } override fun onSurfaceTextureUpdated(surface: android.graphics.SurfaceTexture) {} override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == 100 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { previewView.surfaceTextureListener = this } } }
关键注意点
- 权限处理:Android 6.0以上需动态申请
CAMERA权限,否则无法启动相机。 - 比例匹配:预览尺寸必须与相机输出图片比例一致,否则裁剪区域会出现偏移。
- 图片旋转:不同设备的相机输出方向可能不同,需根据实际设备方向调整旋转角度。
- 坐标转换:界面坐标需乘以预览与图片的缩放比例,才能得到准确的裁剪区域。
内容的提问来源于stack exchange,提问作者Reza Faraji
相关产品推荐
相关产品推荐

