You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于CameraX与Jetpack Compose实现拍照后指定区域裁剪功能

实现带引导框的相机预览与拍摄后裁剪功能

核心思路

  1. 预览界面布局:以相机预览控件(如TextureView)为底层,上层叠加半透明遮罩,中间留出矩形引导框,引导用户对齐拍摄对象。
  2. 相机适配:确保预览画面比例与相机输出图片比例一致,避免后续裁剪偏差;处理屏幕旋转导致的预览方向问题。
  3. 坐标转换与裁剪:拍摄完成后,将界面引导框的坐标转换为原始图片中的对应区域,再执行裁剪操作。

安卓平台实现示例

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 23:26:05