使用Camera拍照

使用 Camera 拍照

在 Android 中,使用 Camera API(旧版)或 Camera2 API(API 21+)可以实现拍照功能。Camera API 适用于较老设备(API 1-20),但已被标记为弃用;Camera2 API 提供更强大的功能(如手动控制、RAW 格式),是现代应用的首选。本文将重点介绍使用 Camera2 API 拍照,结合 Android 动画合集的背景,展示如何在自定义 View 中集成拍照功能,并添加动画效果(如预览缩放)。同时提供 Camera API 的简要说明,以便兼容旧设备。内容包括初始化、预览、拍照、保存照片及权限处理。

Camera2 API 的作用与原理

  • 作用Camera2 API 提供对相机硬件的精细控制,支持预览、拍照、录像及高级功能(如手动对焦、曝光)。它通过 CameraManager 访问设备相机,结合 SurfaceViewTextureView 显示预览。
  • 原理Camera2 使用会话(CaptureSession)管理相机操作,CaptureRequest 定义每次捕捉的参数(如对焦、闪光灯)。拍照结果通过 ImageReader 或文件输出。
  • 应用场景
  • 自定义相机应用(如美颜相机)。
  • 动画效果(如拍照时的缩放反馈)。
  • 实时预览(如二维码扫描)。
  • 结合 Canvas/Paint 绘制相机预览上的覆盖层。

权限要求

拍照需要以下权限,在 AndroidManifest.xml 中声明:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- API 28 及以下 -->
<uses-feature android:name="android.hardware.camera" android:required="true" />
  • 动态权限(API 23+):
  • Manifest.permission.CAMERA:访问相机。
  • Manifest.permission.WRITE_EXTERNAL_STORAGE:保存照片(API 28 及以下)。
  • 使用 ActivityCompat.requestPermissions 请求权限。

Camera2 API 拍照步骤

  1. 初始化 CameraManager:获取设备相机列表,选择后置相机。
  2. 显示预览:使用 TextureViewSurfaceView 显示相机预览。
  3. 创建 CaptureSession:配置预览和拍照的输出目标。
  4. 拍照:发送 CaptureRequest 捕获照片,保存到文件或 Bitmap。
  5. 释放资源:关闭相机设备和会话。

完整示例(Camera2 拍照 + 动画)

以下是一个完整的 Camera2 拍照示例,结合属性动画在拍照时显示缩放效果。

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.*;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Size;
import android.view.Surface;
import android.view.TextureView;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.animation.ObjectAnimator;

import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;

public class Camera2Activity extends AppCompatActivity {
    private static final int REQUEST_CAMERA_PERMISSION = 100;
    private TextureView textureView;
    private CameraDevice cameraDevice;
    private CameraCaptureSession captureSession;
    private CaptureRequest.Builder previewRequestBuilder;
    private Handler backgroundHandler;
    private HandlerThread backgroundThread;
    private String cameraId;
    private Size previewSize;
    private ImageReader imageReader;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera2);

        textureView = findViewById(R.id.texture_view);
        Button captureButton = findViewById(R.id.capture_button);

        // 请求相机权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
        } else {
            setupCamera();
        }

        // 拍照按钮
        captureButton.setOnClickListener(v -> takePicture());

        // TextureView 监听
        textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
                openCamera();
            }
            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}
            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
                return true;
            }
            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
        });
    }

    private void setupCamera() {
        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        try {
            // 选择后置相机
            for (String id : manager.getCameraIdList()) {
                CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
                Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) {
                    cameraId = id;
                    // 获取预览尺寸
                    StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                    if (map != null) {
                        previewSize = map.getOutputSizes(SurfaceTexture.class)[0];
                    }
                    break;
                }
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private void openCamera() {
        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        try {
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
                // 启动后台线程
                backgroundThread = new HandlerThread("CameraBackground");
                backgroundThread.start();
                backgroundHandler = new Handler(backgroundThread.getLooper());

                // 设置 ImageReader 用于拍照
                imageReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), ImageFormat.JPEG, 2);
                imageReader.setOnImageAvailableListener(reader -> {
                    // 保存照片
                    try (Image image = reader.acquireNextImage()) {
                        ByteBuffer buffer = image.getPlanes()[0].getBuffer();
                        byte[] bytes = new byte[buffer.remaining()];
                        buffer.get(bytes);
                        saveImage(bytes);
                    }
                }, backgroundHandler);

                // 打开相机
                manager.openCamera(cameraId, new CameraDevice.StateCallback() {
                    @Override
                    public void onOpened(@NonNull CameraDevice camera) {
                        cameraDevice = camera;
                        startPreview();
                    }
                    @Override
                    public void onDisconnected(@NonNull CameraDevice camera) {
                        camera.close();
                        cameraDevice = null;
                    }
                    @Override
                    public void onError(@NonNull CameraDevice camera, int error) {
                        camera.close();
                        cameraDevice = null;
                    }
                }, backgroundHandler);
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private void startPreview() {
        try {
            SurfaceTexture texture = textureView.getSurfaceTexture();
            texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
            Surface surface = new Surface(texture);

            previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            previewRequestBuilder.addTarget(surface);

            cameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession session) {
                    captureSession = session;
                    try {
                        previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                        captureSession.setRepeatingRequest(previewRequestBuilder.build(), null, backgroundHandler);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                    }
                }
                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession session) {}
            }, backgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private void takePicture() {
        if (cameraDevice == null) return;

        // 拍照动画
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(textureView, "scaleX", 1f, 0.9f, 1f);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(textureView, "scaleY", 1f, 0.9f, 1f);
        scaleX.setDuration(200);
        scaleY.setDuration(200);
        scaleX.start();
        scaleY.start();

        try {
            CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            captureBuilder.addTarget(imageReader.getSurface());
            captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

            captureSession.capture(captureBuilder.build(), null, backgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private void saveImage(byte[] bytes) {
        File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
            "photo_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()) + ".jpg");
        try (FileOutputStream output = new FileOutputStream(file)) {
            output.write(bytes);
            runOnUiThread(() -> Toast.makeText(this, "Photo saved: " + file.getAbsolutePath(), Toast.LENGTH_SHORT).show());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CAMERA_PERMISSION && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            setupCamera();
        } else {
            Toast.makeText(this, "Camera permission denied", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (captureSession != null) {
            captureSession.close();
            captureSession = null;
        }
        if (cameraDevice != null) {
            cameraDevice.close();
            cameraDevice = null;
        }
        if (backgroundThread != null) {
            backgroundThread.quitSafely();
            backgroundThread = null;
            backgroundHandler = null;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (imageReader != null) {
            imageReader.close();
            imageReader = null;
        }
    }
}
  • 布局 XML(res/layout/activity_camera2.xml):
  <?xml version="1.0" encoding="utf-8"?>
  <LinearLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical">
      <TextureView
          android:id="@+id/texture_view"
          android:layout_width="match_parent"
          android:layout_height="0dp"
          android:layout_weight="1" />
      <Button
          android:id="@+id/capture_button"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_gravity="center"
          android:text="Capture" />
  </LinearLayout>
  • 说明
  • 权限:动态请求相机权限,保存照片需存储权限(API 28 及以下)。
  • 预览TextureView 显示相机预览,SurfaceTexture 设置分辨率。
  • 拍照:点击按钮触发拍照,ImageReader 获取 JPEG 数据,保存到 Pictures 目录。
  • 动画:拍照时添加缩放动画(scaleXscaleY),增强交互反馈。
  • 生命周期:在 onPause 关闭会话和相机,onDestroy 释放 ImageReader

旧版 Camera API(简要说明)

Camera API(API 1-20,弃用)使用更简单,但功能有限。以下是基本步骤:

  1. 初始化Camera camera = Camera.open();
  2. 预览:绑定到 SurfaceView,设置 camera.setPreviewDisplay(holder)
  3. 拍照camera.takePicture(null, null, (data, camera) -> saveImage(data));
  4. 释放camera.release();
  • 代码片段
  Camera camera = Camera.open();
  SurfaceView surfaceView = findViewById(R.id.surface_view);
  SurfaceHolder holder = surfaceView.getHolder();
  holder.addCallback(new SurfaceHolder.Callback() {
      @Override
      public void surfaceCreated(SurfaceHolder holder) {
          try {
              camera.setPreviewDisplay(holder);
              camera.startPreview();
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
      // ...
  });
  findViewById(R.id.capture_button).setOnClickListener(v -> camera.takePicture(null, null, (data, cam) -> saveImage(data)));
  • 注意Camera API 已弃用,建议使用 Camera2CameraX(更现代的封装库)。

优缺点

  • 优点
  • Camera2 提供精细控制(对焦、曝光、闪光灯等)。
  • 支持多种分辨率和格式(JPEG、RAW)。
  • 适合自定义相机应用。
  • 缺点
  • Camera2 复杂,状态管理繁琐。
  • 旧版 Camera API 已弃用,兼容性问题多。
  • 权限处理和设备差异需特别注意。
  • 替代方案
  • CameraX:Jetpack 库,简化相机开发,推荐现代应用。
  • Third-party Libraries:如 Fotoapparat。
  • OpenGL/Canvas:结合自定义渲染效果。

注意事项

  • 权限:API 23+ 需动态请求权限,处理拒绝情况。
  • 设备兼容性:检查设备相机特性(如 CameraCharacteristics),处理无相机设备的情况。
  • 性能:异步操作(prepareAsynccapture)避免阻塞主线程;预览分辨率影响性能。
  • 生命周期:在 onPauseonDestroy 释放资源,避免泄漏。
  • 存储:API 29+ 使用 MediaStore 替代直接文件保存。
  • 调试:通过 Log 检查相机状态,验证预览和拍照结果。

扩展:Jetpack Compose + CameraX

CameraX 是推荐的现代相机 API,结合 Compose 实现更简洁。以下是示例:

import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.PreviewView
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executor

@Composable
fun CameraXScreen() {
    val context = LocalContext.current
    val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
    var imageCapture by remember { mutableStateOf<ImageCapture?>(null) }

    Column {
        AndroidView(
            factory = { ctx ->
                PreviewView(ctx).apply {
                    cameraProviderFuture.addListener({
                        val cameraProvider = cameraProviderFuture.get()
                        val preview = Preview.Builder().build().also {
                            it.setSurfaceProvider(this.surfaceProvider)
                        }
                        imageCapture = ImageCapture.Builder().build()
                        cameraProvider.unbindAll()
                        cameraProvider.bindToLifecycle(
                            ctx as ComponentActivity,
                            CameraSelector.DEFAULT_BACK_CAMERA,
                            preview,
                            imageCapture
                        )
                    }, ContextCompat.getMainExecutor(ctx))
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .height(400.dp)
        )
        Button(
            onClick = {
                val file = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES),
                    "IMG_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())}.jpg")
                val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build()
                imageCapture?.takePicture(
                    outputOptions,
                    ContextCompat.getMainExecutor(context),
                    object : ImageCapture.OnImageSavedCallback {
                        override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                            // 动画效果
                        }
                        override fun onError(exc: ImageCaptureException) {
                            exc.printStackTrace()
                        }
                    }
                )
            },
            modifier = Modifier.padding(16.dp)
        ) {
            Text("Capture")
        }
    }
}
  • 说明
  • 使用 CameraX 简化相机逻辑,PreviewView 显示预览。
  • ImageCapture 处理拍照,保存到文件。
  • 可结合 animate*AsState 添加动画(如缩放)。

示例:拍照动画

为拍照添加闪光动画:

captureButton.setOnClickListener(v -> {
    ObjectAnimator flash = ObjectAnimator.ofFloat(textureView, "alpha", 1f, 0.5f, 1f);
    flash.setDuration(100);
    flash.start();
    takePicture();
});

如果需要更复杂功能(如对焦控制、视频录制、CameraX 高级用法)、与 Canvas/Paint 的结合、或旧版 Camera API 详细实现,请告诉我!后续可探讨 Android 动画合集的其他部分(如过渡动画、Lottie)。

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注