1. 程式人生 > >自定義Camera系列之:TextureView + Camera

自定義Camera系列之:TextureView + Camera

一、前言

上一篇介紹了 自定義Camera系列之:SurfaceView + Camera,接著我們介紹使用 TextureView + Camera 的組合。

為什麼選擇 TextureView

由於 SurfaceView 是擁有一個獨立的 Surface,不在 View hierachy 體系中,因此不支援動畫和截圖,而 TextureView 則沒有該限制。

TextureView 是 Android 4.0 之後引入的,它將內容流直接投影到 View 中,資料流可以來自 App 程序或遠端程序。

缺點是必須在硬體加速的視窗中使用,且記憶體和耗電比 SurfaceView

更高,繪製也可能存在 1~3 幀的延遲。

下面是應用的簡要截圖:
在這裡插入圖片描述

二、相機開發步驟

我們選擇將 Camera 和 View 分開,Camera 的相關操作由 CameraProxy 類完成,而 View 持有一個 CameraProxy 物件。這樣 CameraProxy 也是可以重複利用的(該環節和上一篇部落格完全一致,也說明了其重用性)。

1. 開啟相機

開啟相機需要傳入一個 cameraId,舊的 API 中只有 CAMERA_FACING_BACKCAMERA_FACING_FRONT 兩個值可以選擇,返回一個 Camera 物件。

    public
void openCamera() { mCamera = Camera.open(mCameraId); // 開啟相機 Camera.getCameraInfo(mCameraId, mCameraInfo); // 獲取相機資訊 initConfig(); // 初始化相機配置 setDisplayOrientation(); // 設定相機顯示方向 }

2. 初始化相機配置

在舊 Camera API 中,相機的配置都是通過 Parameters 類完成。

我們可以設定 閃光模式聚焦模式曝光強度預覽圖片格式和大小

拍照圖片格式和大小 等等資訊。

    private void initConfig() {
        try {
            mParameters = mCamera.getParameters();
            // 如果攝像頭不支援這些引數都會出錯的,所以設定的時候一定要判斷是否支援
            List<String> supportedFlashModes = mParameters.getSupportedFlashModes();
            if (supportedFlashModes != null && supportedFlashModes.contains(Parameters.FLASH_MODE_OFF)) {
                mParameters.setFlashMode(Parameters.FLASH_MODE_OFF); // 設定閃光模式(關閉)
            }
            List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
            if (supportedFocusModes != null && supportedFocusModes.contains(Parameters.FOCUS_MODE_AUTO)) {
                mParameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); // 設定聚焦模式(自動)
            }
            mParameters.setPreviewFormat(ImageFormat.NV21); // 設定預覽圖片格式
            mParameters.setPictureFormat(ImageFormat.JPEG); // 設定拍照圖片格式
            mParameters.setExposureCompensation(0); // 設定曝光強度
            Size previewSize = getSuitableSize(mParameters.getSupportedPreviewSizes());
            mPreviewWidth = previewSize.width;
            mPreviewHeight = previewSize.height;
            mParameters.setPreviewSize(mPreviewWidth, mPreviewHeight); // 設定預覽圖片大小
            Log.d(TAG, "previewWidth: " + mPreviewWidth + ", previewHeight: " + mPreviewHeight);
            Size pictureSize = getSuitableSize(mParameters.getSupportedPictureSizes());
            mParameters.setPictureSize(pictureSize.width, pictureSize.height);
            Log.d(TAG, "pictureWidth: " + pictureSize.width + ", pictureHeight: " + pictureSize.height);
            mCamera.setParameters(mParameters); // 將設定好的parameters新增到相機裡
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

這裡用到的一個 getSuitableSize 方法獲取合數的 預覽/拍照 尺寸。(後面貼完整程式碼)

3. 設定相機預覽時的顯示方向

這個方法很重要,關乎著你預覽畫面是否正常。其實相機底層的預覽畫面全都是寬度大於高度的,但是豎屏時畫面要顯示正常,都是通過這個方法設定了一定的顯示方向。

    private void setDisplayOrientation() {
        int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }
        int result;
        if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (mCameraInfo.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror
        } else {  // back-facing
            result = (mCameraInfo.orientation - degrees + 360) % 360;
        }
        mCamera.setDisplayOrientation(result);
    }

4. 開始預覽、停止預覽

可以通過 TextureViewSurfaceTexture 設定給 Camera ,即可幫你完成一系列複雜的繫結。

    public void startPreview(SurfaceTexture surface) {
        if (mCamera != null) {
            try {
                mCamera.setPreviewTexture(surface); // 先繫結顯示的畫面
            } catch (IOException e) {
                e.printStackTrace();
            }
            mCamera.startPreview(); // 這裡才是開始預覽
        }
    }

    public void stopPreview() {
        if (mCamera != null) {
            mCamera.stopPreview(); // 停止預覽
        }
    }

5. 釋放相機

相機是很耗費系統資源的東西,用完一定要釋放。對應於 openCamera

    public void releaseCamera() {
        if (mCamera != null) {
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
    }

6. 點選聚焦

簡單的說,就是根據使用者在 view 上的觸控點,使相機對該點進行一次對焦操作。

詳細看我的這篇部落格介紹的把:Android自定義相機定點聚焦

完整程式碼後面會貼的。

7. 雙指放大縮小

我們也只實現放大縮小的邏輯,至於 View 的觸控交給 View 類去完成。

    public void handleZoom(boolean isZoomIn) {
        if (mParameters.isZoomSupported()) { // 首先還是要判斷是否支援
            int maxZoom = mParameters.getMaxZoom();
            int zoom = mParameters.getZoom();
            if (isZoomIn && zoom < maxZoom) {
                zoom++;
            } else if (zoom > 0) {
                zoom--;
            }
            mParameters.setZoom(zoom); // 通過這個方法設定放大縮小
            mCamera.setParameters(mParameters);
        } else {
            Log.w(TAG, "zoom not supported");
        }
    }

8. 拍照

拍照的邏輯我交給上層去完成了,後面再詳細介紹把。這裡我們只是簡單的封裝了一下元介面,一般常用的是 Camera.PictureCallback,會返回可用的 jpeg 給我們。

    public void takePicture(Camera.PictureCallback pictureCallback) {
        mCamera.takePicture(null, null, pictureCallback);
    }

9. 其它

諸如設定預覽回撥、切換前後攝像頭的操作等直接看下面的實現把。另外對於 聚焦模式閃光燈模式 等沒有詳細去介紹了,感興趣的可以另外搜尋相關模組。畢竟相機要介紹完全的話還是一塊很大的東西。

10. CameraProxy 類

下面程式碼還用到了 OrientationEventListener,這裡之前沒介紹,是通過感測器來獲取當前手機的方向的,用於 拍照 的時候設定圖片的選擇使用,後面會介紹。

package com.afei.camerademo.camera;

import android.app.Activity;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.util.Log;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.SurfaceHolder;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@SuppressWarnings("deprecation")
public class CameraProxy implements Camera.AutoFocusCallback {

    private static final String TAG = "CameraProxy";

    private Activity mActivity;
    private Camera mCamera;
    private Parameters mParameters;
    private CameraInfo mCameraInfo = new CameraInfo();
    private int mCameraId = CameraInfo.CAMERA_FACING_BACK;
    private int mPreviewWidth = 1440; // default 1440
    private int mPreviewHeight = 1080; // default 1080
    private float mPreviewScale = mPreviewHeight * 1f / mPreviewWidth;
    private PreviewCallback mPreviewCallback; // 相機預覽的資料回撥
    private OrientationEventListener mOrientationEventListener;
    private int mLatestRotation = 0;

    public byte[] mPreviewBuffer;

    public CameraProxy(Activity activity) {
        mActivity = activity;
        mOrientationEventListener = new OrientationEventListener(mActivity) {
            @Override
            public void onOrientationChanged(int orientation) {
                Log.d(TAG, "onOrientationChanged: orientation: " + orientation);
                setPictureRotate(orientation);
            }
        };
    }

    public void openCamera() {
        Log.d(TAG, "openCamera cameraId: " + mCameraId);
        mCamera = Camera.open(mCameraId);
        Camera.getCameraInfo(mCameraId, mCameraInfo);
        initConfig();
        setDisplayOrientation();
        Log.d(TAG, "openCamera enable mOrientationEventListener");
        mOrientationEventListener.enable();
    }

    public void releaseCamera() {
        if (mCamera != null) {
            Log.v(TAG, "releaseCamera");
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
        mOrientationEventListener.disable();
    }

    public void startPreview(SurfaceHolder holder) {
        if (mCamera != null) {
            Log.v(TAG, "startPreview");
            try {
                mCamera.setPreviewDisplay(holder);
            } catch (IOException e) {
                e.printStackTrace();
            }
            mCamera.startPreview();
        }
    }

    public void startPreview(SurfaceTexture surface) {
        if (mCamera != null) {
            Log.v(TAG, "startPreview");
            try {
                mCamera.setPreviewTexture(surface);
            } catch (IOException e) {
                e.printStackTrace();
            }
            mCamera.startPreview();
        }
    }

    public void stopPreview() {
        if (mCamera != null) {
            Log.v(TAG, "stopPreview");
            mCamera.stopPreview();
        }
    }

    public boolean isFrontCamera() {
        return mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT;
    }

    private void initConfig() {
        Log.v(TAG, "initConfig");
        try {
            mParameters = mCamera.getParameters();
            // 如果攝像頭不支援這些引數都會出錯的,所以設定的時候一定要判斷是否支援
            List<String> supportedFlashModes = mParameters.getSupportedFlashModes();
            if (supportedFlashModes != null && supportedFlashModes.contains(Parameters.FLASH_MODE_OFF)) {
                mParameters.setFlashMode(Parameters.FLASH_MODE_OFF); // 設定閃光模式
            }
            List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
            if (supportedFocusModes != null && supportedFocusModes.contains(Parameters.FOCUS_MODE_AUTO)) {
                mParameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); // 設定聚焦模式
            }
            mParameters.setPreviewFormat(ImageFormat.NV21); // 設定預覽圖片格式
            mParameters.setPictureFormat(ImageFormat.JPEG); // 設定拍照圖片格式
            mParameters.setExposureCompensation(0); // 設定曝光強度
            Size previewSize = getSuitableSize(mParameters.getSupportedPreviewSizes());
            mPreviewWidth = previewSize.width;
            mPreviewHeight = previewSize.height;
            mParameters.setPreviewSize(mPreviewWidth, mPreviewHeight); // 設定預覽圖片大小
            Log.d(TAG, "previewWidth: " + mPreviewWidth + ", previewHeight: " + mPreviewHeight);
            Size pictureSize = getSuitableSize(mParameters.getSupportedPictureSizes());
            mParameters.setPictureSize(pictureSize.width, pictureSize.height);
            Log.d(TAG, "pictureWidth: " + pictureSize.width + ", pictureHeight: " + pictureSize.height);
            mCamera.setParameters(mParameters); // 將設定好的parameters新增到相機裡
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Size getSuitableSize(List<Size> sizes) {
        int minDelta = Integer.MAX_VALUE; // 最小的差值,初始值應該設定大點保證之後的計算中會被重置
        int index = 0; // 最小的差值對應的索引座標
        for (int i = 0; i < sizes.size(); i++) {
            Size previewSize = sizes.get(i);
            Log.v(TAG, "SupportedPreviewSize, width: " + previewSize.width + ", height: " + previewSize.height);
            // 找到一個與設定的解析度差值最小的相機支援的解析度大小
            if (previewSize.width * mPreviewScale == previewSize.height) {
                int delta = Math.abs(mPreviewWidth - previewSize.width);
                if (delta == 0) {
                    return previewSize;
                }
                if (minDelta > delta) {
                    minDelta = delta;
                    index = i;
                }
            }
        }
        return sizes.get(index); // 預設返回與設定的解析度最接近的預覽尺寸
    }

    /**
     * 設定相機顯示的方向,必須設定,否則顯示的影象方向會錯誤
     */
    private void setDisplayOrientation() {
        int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }
        int result;
        if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (mCameraInfo.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror
        } else {  // back-facing
            result = (mCameraInfo.orientation - degrees + 360) % 360;
        }
        mCamera.setDisplayOrientation(result);
    }

    private void setPictureRotate(int orientation) {
        if (orientation == Orientati