Android Camera SurfaceView 預覽拍照

2021-02-24 音視頻開發進階

本文已獲得作者授權,本文作者:cain_huang
原文連結:https://www.jianshu.com/p/9e0f3fc5a3b4

Android使用 Camera API + SurfaceView 方式進行預覽拍照。

SurfaceView 的創建以及回調

創建一個SurfaceView,並實現SurfaceHolder的回調。

由於Camera在SurfaceView中是通過SurfaceHolder 使得Surfaceview能夠預覽Camera返回的數據,因此我們需要實現SurfaceHolder 的回調,實現代碼如下:

public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

    private static final String TAG = CameraSurfaceView.class.getSimpleName();

    private SurfaceHolder mSurfaceHolder;

    public CameraSurfaceView(Context context) {
        super(context);
        init();
    }

    public CameraSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mSurfaceHolder = getHolder();
        mSurfaceHolder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        CameraUtils.openFrontalCamera(CameraUtils.DESIRED_PREVIEW_FPS);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        CameraUtils.startPreviewDisplay(holder);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        CameraUtils.releaseCamera();
    }
}

Camera 操作的輔助類

CameraUtils 輔助類主要是Camera API 的一些操作,比如打開相機、開始預覽、停止預覽、切換相機、設置預覽參數等操作,具體實現如下:

public class CameraUtils {

    
    public static final int DEFAULT_WIDTH = 1280;
    public static final int DEFAULT_HEIGHT = 720;
    public static final int DESIRED_PREVIEW_FPS = 30;

    private static int mCameraID = Camera.CameraInfo.CAMERA_FACING_FRONT;
    private static Camera mCamera;
    private static int mCameraPreviewFps;
    private static int mOrientation = 0;

    
    public static void openFrontalCamera(int expectFps) {
        if (mCamera != null) {
            throw new RuntimeException("camera already initialized!");
        }
        Camera.CameraInfo info = new Camera.CameraInfo();
        int numCameras = Camera.getNumberOfCameras();
        for (int i = 0; i < numCameras; i++) {
            Camera.getCameraInfo(i, info);
            if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                mCamera = Camera.open(i);
                mCameraID = info.facing;
                break;
            }
        }
        
        if (mCamera == null) {
            mCamera = Camera.open();
            mCameraID = Camera.CameraInfo.CAMERA_FACING_BACK;
        }
        
        if (mCamera == null) {
            throw new RuntimeException("Unable to open camera");
        }

        Camera.Parameters parameters = mCamera.getParameters();
        mCameraPreviewFps = CameraUtils.chooseFixedPreviewFps(parameters, expectFps * 1000);
        parameters.setRecordingHint(true);
        mCamera.setParameters(parameters);
        setPreviewSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);
        setPictureSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);
        mCamera.setDisplayOrientation(mOrientation);
    }

    
    public static void openCamera(int cameraID, int expectFps) {
        if (mCamera != null) {
            throw new RuntimeException("camera already initialized!");
        }
        mCamera = Camera.open(cameraID);
        if (mCamera == null) {
            throw new RuntimeException("Unable to open camera");
        }
        mCameraID = cameraID;
        Camera.Parameters parameters = mCamera.getParameters();
        mCameraPreviewFps = CameraUtils.chooseFixedPreviewFps(parameters, expectFps * 1000);
        parameters.setRecordingHint(true);
        mCamera.setParameters(parameters);
        setPreviewSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);
        setPictureSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);
        mCamera.setDisplayOrientation(mOrientation);
    }

    
    public static void startPreviewDisplay(SurfaceHolder holder) {
        if (mCamera == null) {
            throw new IllegalStateException("Camera must be set when start preview");
        }
        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    
    public static void switchCamera(int cameraID, SurfaceHolder holder) {
        if (mCameraID == cameraID) {
            return;
        }
        mCameraID = cameraID;
        
        releaseCamera();
        
        openCamera(cameraID, CameraUtils.DESIRED_PREVIEW_FPS);
        
        startPreviewDisplay(holder);
    }

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

    
    public static void startPreview() {
        if (mCamera != null) {
            mCamera.startPreview();
        }
    }

    
    public static void stopPreview() {
        if (mCamera != null) {
            mCamera.stopPreview();
        }
    }

    
    public static void takePicture(Camera.ShutterCallback shutterCallback,
                                   Camera.PictureCallback rawCallback,
                                   Camera.PictureCallback pictureCallback) {
        if (mCamera != null) {
            mCamera.takePicture(shutterCallback, rawCallback, pictureCallback);
        }
    }

    
    public static void setPreviewSize(Camera camera, int expectWidth, int expectHeight) {
        Camera.Parameters parameters = camera.getParameters();
        Camera.Size size = calculatePerfectSize(parameters.getSupportedPreviewSizes(),
                expectWidth, expectHeight);
        parameters.setPreviewSize(size.width, size.height);
        camera.setParameters(parameters);
    }

    
    public static Camera.Size getPreviewSize() {
        if (mCamera != null) {
            return mCamera.getParameters().getPreviewSize();
        }
        return null;
    }

    
    public static void setPictureSize(Camera camera, int expectWidth, int expectHeight) {
        Camera.Parameters parameters = camera.getParameters();
        Camera.Size size = calculatePerfectSize(parameters.getSupportedPictureSizes(),
                expectWidth, expectHeight);
        parameters.setPictureSize(size.width, size.height);
        camera.setParameters(parameters);
    }

    
    public static Camera.Size getPictureSize() {
        if (mCamera != null) {
            return mCamera.getParameters().getPictureSize();
        }
        return null;
    }

    
    public static Camera.Size calculatePerfectSize(List<Camera.Size> sizes, int expectWidth,
                                                   int expectHeight) {
        sortList(sizes); 
        Camera.Size result = sizes.get(0);
        boolean widthOrHeight = false; 
        
        for (Camera.Size size: sizes) {
            
            if (size.width == expectWidth && size.height == expectHeight) {
                result = size;
                break;
            }
            
            if (size.width == expectWidth) {
                widthOrHeight = true;
                if (Math.abs(result.height - expectHeight)
                        > Math.abs(size.height - expectHeight)) {
                    result = size;
                }
            }
            
            else if (size.height == expectHeight) {
                widthOrHeight = true;
                if (Math.abs(result.width - expectWidth)
                        > Math.abs(size.width - expectWidth)) {
                    result = size;
                }
            }
            
            else if (!widthOrHeight) {
                if (Math.abs(result.width - expectWidth)
                        > Math.abs(size.width - expectWidth)
                        && Math.abs(result.height - expectHeight)
                        > Math.abs(size.height - expectHeight)) {
                    result = size;
                }
            }
        }
        return result;
    }

    
    private static void sortList(List<Camera.Size> list) {
        Collections.sort(list, new Comparator<Camera.Size>() {
            @Override
            public int compare(Camera.Size pre, Camera.Size after) {
                if (pre.width > after.width) {
                    return 1;
                } else if (pre.width < after.width) {
                    return -1;
                }
                return 0;
            }
        });
    }

    
    public static int chooseFixedPreviewFps(Camera.Parameters parameters, int expectedThoudandFps) {
        List<int[]> supportedFps = parameters.getSupportedPreviewFpsRange();
        for (int[] entry : supportedFps) {
            if (entry[0] == entry[1] && entry[0] == expectedThoudandFps) {
                parameters.setPreviewFpsRange(entry[0], entry[1]);
                return entry[0];
            }
        }
        int[] temp = new int[2];
        int guess;
        parameters.getPreviewFpsRange(temp);
        if (temp[0] == temp[1]) {
            guess = temp[0];
        } else {
            guess = temp[1] / 2;
        }
        return guess;
    }

    
    public static int calculateCameraPreviewOrientation(Activity activity) {
        Camera.CameraInfo info = new Camera.CameraInfo();
        Camera.getCameraInfo(mCameraID, info);
        int rotation = activity.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 (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360;
        } else {
            result = (info.orientation - degrees + 360) % 360;
        }
        mOrientation = result;
        return result;
    }


    
    public static int getCameraID() {
        return mCameraID;
    }

    
    public static int getPreviewOrientation() {
        return mOrientation;
    }

    
    public static int getCameraPreviewThousandFps() {
        return mCameraPreviewFps;
    }
}

權限申請

在Activity中使用CameraSurfaceview,有Android6.0動態權限申請問題,需要我們判斷相機和存儲權限是否申請了:

public class CameraSurfaceViewActivity extends AppCompatActivity implements View.OnClickListener {

    private static final int REQUEST_CAMERA = 0x01;

    private CameraSurfaceView mCameraSurfaceView;
    private Button mBtnTake;
    private Button mBtnSwitch;

    private int mOrientation;

    
    private FrameLayout mAspectLayout;
    private boolean mCameraRequested;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setContentView(R.layout.activity_camera_surface);
        
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            initView();
        } else {
            ActivityCompat.requestPermissions(this,
                    new String[]{
                            Manifest.permission.CAMERA,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE
                    }, REQUEST_CAMERA);
        }
    }

    
    private void initView() {
        mAspectLayout = (FrameLayout) findViewById(R.id.layout_aspect);;
        mCameraSurfaceView = new CameraSurfaceView(this);
        mAspectLayout.addView(mCameraSurfaceView);
        mOrientation = CameraUtils.calculateCameraPreviewOrientation(CameraSurfaceViewActivity.this);
        mBtnTake = (Button) findViewById(R.id.btn_take);
        mBtnTake.setOnClickListener(this);
        mBtnSwitch = (Button) findViewById(R.id.btn_switch);
        mBtnSwitch.setOnClickListener(this);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            
            case REQUEST_CAMERA:
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    mCameraRequested = true;
                    initView();
                }
                break;
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (mCameraRequested) {
            CameraUtils.startPreview();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        CameraUtils.stopPreview();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_take:
                takePicture();
                break;

            case R.id.btn_switch:
                switchCamera();
                break;
        }
    }

    
    private void takePicture() {
        CameraUtils.takePicture(new Camera.ShutterCallback() {
            @Override
            public void onShutter() {

            }
        }, null, new Camera.PictureCallback() {
            @Override
            public void onPictureTaken(byte[] data, Camera camera) {
                CameraUtils.startPreview();
                Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
                if (bitmap != null) {
                    bitmap = ImageUtils.getRotatedBitmap(bitmap, mOrientation);
                    String path = Environment.getExternalStorageDirectory() + "/DCIM/Camera/"
                            + System.currentTimeMillis() + ".jpg";
                    try {
                        FileOutputStream fout = new FileOutputStream(path);
                        BufferedOutputStream bos = new BufferedOutputStream(fout);
                        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
                        bos.flush();
                        bos.close();
                        fout.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                CameraUtils.startPreview();
            }
        });
    }


    
    private void switchCamera() {
        if (mCameraSurfaceView != null) {
            CameraUtils.switchCamera(1 - CameraUtils.getCameraID(), mCameraSurfaceView.getHolder());
            
            mOrientation = CameraUtils.calculateCameraPreviewOrientation(CameraSurfaceViewActivity.this);
        }
    }

}

由於用到了相機和存儲權限,我們需要在manifest中註冊相機和存儲權限,這裡要說明的是,manifest用use-permission只是聲明了需要使用哪些權限,而我們實際項目中在使用到這兩項權限時,需要你檢查權限是否已經被授權,如果沒授權,則需要請求授權:

<!-- 存儲權限 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!-- 相機權限 -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.FLASHLIGHT" />

圖像處理的輔助類

另外,ImageUtils類的實現如下:

public class ImageUtils {

    
    public static Bitmap getRotatedBitmap(Bitmap bitmap, int rotation) {
        Matrix matrix = new Matrix();
        matrix.postRotate(rotation);
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                bitmap.getHeight(), matrix, false);
    }

    
    public static Bitmap getFlipBitmap(Bitmap bitmap) {
        Matrix matrix = new Matrix();
        matrix.setScale(-1, 1);
        matrix.postTranslate(bitmap.getWidth(), 0);
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                bitmap.getHeight(), matrix, false);
    }
}

layout如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.cgfay.camerasample.CameraSurfaceViewActivity">

    <FrameLayout
        android:id="@+id/layout_aspect"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    </FrameLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_gravity="bottom"
        android:gravity="center">
        <Button
            android:id="@+id/btn_take"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="拍照" />

        <Button
            android:id="@+id/btn_switch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="切換相機" />
    </LinearLayout>

</FrameLayout>

至此,通過SurfaceView + Camera API 預覽拍照功能已經實現。

備註

Camera API 在打開相機是在哪個線程,那麼onPreviewFrame回調執行就在哪個線程。

因此,如果要通過onPreviewFrame回調使用預覽數據,則可以通過HandlerThread 異步調用Camera進行操作。
另外一個問題,onPreviewFrame方法中不要執行過於複雜的邏輯操作,這樣會阻塞Camera,無法獲取新的Frame,導致幀率下降。

關注微信公眾號【紙上淺談】,閱讀更多音視頻、Camera、OpenGL、NDK 開發相關文章~~~

相關焦點

  • Android使用SurfaceView實現仿微信視頻通話大小視圖切換功能
    ><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent
  • 【Android】CameraX-CameraView說明
    官方為了讓我們更容易使用Camera,出了個一個官方的庫cameraview。不過這個庫已經Deprecated,官方建議使用Jetpack CameraX 替代。CameraX基本上分為三部分,一部分控制拍照,一部分控制錄製視頻,還有一部分時進行視頻和照片的預覽。
  • 【camera hal 系列之一】android camera HAL v3.0概述
    Android的camera硬體抽象層(HAL)將更高層次的android.hardware.Camera中的camera框架層API與底層的camera驅動和camera硬體模塊連接了起來。android最新版本介紹了camera棧的一個新的、底層的實現。
  • Android Camera2 使用總結
    CameraCaptureSession:這是一個非常重要的API,當程序需要預覽、拍照時,都需要先通過該類的實例創建Session。而且不管預覽還是拍照,也都是由該對象的方法進行控制的,其中控制預覽的方法為setRepeatingRequest();控制拍照的方法為capture()。
  • Android Camera2 實現高幀率預覽錄製(附源碼)
    Android的相機 Camera2 在 6.0M 的時候,出了一個支持高幀率預覽和錄像的功能。就是創建一個新的 session,叫做 mCameraDevice.createConstrainedHighSpeedCaptureSession,通過這個,可以實現相機的高幀率(>120fps)的預覽和錄像(需要相機本身支持)。
  • 【技術乾貨】Android直播相機開發詳解
    通過surfaceHolder可以將Camera和surface連接起來,當camera和surface連接後,camera獲得的預覽幀數據就可以通過surface顯示在屏幕上了。·      setPrameters設置相機參數:包括前後攝像頭,閃光燈模式、聚焦模式、預覽和拍照尺寸等。
  • [Android進階]使用Camera2 API實現一個相機預覽頁面
    Camera2 API(位於android.hardware.camera2包下)是Android L後推出的一套訪問攝像頭設備的接口,用來替換原有的Camera。Camera2 API採用管道式的設計,使數據流從攝像頭流向Surface,使用Camera2 API實現拍照錄製視頻功能時,主要涉及到以下幾個類:CameraManager:Camera設備的管理類,通過該類對象可以查詢設備的Camera設備信息,得到CameraDevice對象;CameraDevice:CameraDevice提供了Camera設備相關的一系列固定參數
  • Android Camera/Camera2開發指南
    創建預覽界面,創建繼承自SurfaceView並實現SurfaceHolder接口的拍攝預覽類。有了拍攝預覽類,即可創建一個布局文件,將預覽畫面與設計好的用戶界面控制項融合在一起,實時顯示相機的預覽圖像。設置拍照監聽器,給用戶界面控制項綁定監聽器,使其能響應用戶操作, 開始拍照過程。
  • Android平臺Camera開發指南
    創建預覽界面,創建繼承自SurfaceView並實現SurfaceHolder接口的拍攝預覽類。有了拍攝預覽類,即可創建一個布局文件,將預覽畫面與設計好的用戶界面控制項融合在一起,實時顯示相機的預覽圖像。設置拍照監聽器,給用戶界面控制項綁定監聽器,使其能響應用戶操作, 開始拍照過程。
  • Camera2/HAL3 框架分析 程式設計師Android
    進程中分離出來,作為一個獨立進程 android.hardware.camera.provider@2.4-service 用來控制 camera HAL,cameraserver通過 HIDL 機制於camera provider進行通信。
  • Android Jetpack CameraX 庫 Beta 版正式發布!
    forum/camerax-developers新建一個 issuehttp://issuetracker.google.com/官方文檔https://developer.android.google.cn/training/camerax官方示例代碼https://github.com/android/camera-samples
  • Android CameraView 實用經驗分享
    二、使用非規整的 CameraView 尺寸 當我們需要自定義 CameraView 的尺寸時,比如不是規整的 4:3、16:9 等尺寸時,我們會發現,相機的預覽圖和我們拍攝的圖會不一致,拍攝完的圖會比預覽圖向右或者向下拓展一些內容,類似於下圖,這樣的話本來我們拍攝的在中央的主體就不在中央了,這顯然是不可以接受的,那我們應該如何將成像和預覽修正成中心對齊的呢(我認為這樣的效果可以滿足大多數需求
  • 獲取Android設備上的詳細的攝像頭信息
    ,一般自然些有0度、90度、180度和270度,這樣可以獲取我們正確的手握設備是橫著還是豎著,有關拍照時的方向設置,可以參考下面的代碼設置  public static void setCameraDisplayOrientation(Activity activity,         int cameraId, android.hardware.Camera camera)
  • Flutter 中文文檔:使用 Camera 插件實現拍照功能
    因此,Flutter 提供了 camera 插件。camera 插件提供了一系列可用的相機,並使用特定的相機展示相機預覽、拍照、錄視頻。這個章節將會講解如何使用 camera 插件去展示相機預覽、拍照並顯示。步驟3. 創建並初始化 CameraController4.
  • 安卓相機實現拍照和錄像
    ;import android.app.Activity;import android.graphics.ImageFormat;import android.graphics.SurfaceTexture;import android.hardware.Camera;import android.hardware.Camera.Parameters
  • Google Camera 架構了解下
    原圖:https://source.android.google.cn/devices/camera1.1 Android Camera 基本分層Google Android 框架圖從上圖得知,Android手機中Camera軟體主要有大體上有4層:1.應用層:應用開發者調用AOSP提供的接口即可,AOSP的接口即Android
  • Camera2 / HAL3 架構了解下
    進程中分離出來,作為一個獨立進程 android.hardware.camera.provider@2.4-service 用來控制 camera HAL,cameraserver通過 HIDL 機制於camera provider進行通信。
  • 使用 CameraX Extensions API 將特效應用到照片上
    Android CameraXhttps://android-developers.googleblog.com/2019/05/whats-new-with-android-jetpack.htmlExtensionsManagerhttps://developer.android.google.cn/reference/androidx/camera/extensions/ExtensionsManagerAndroid
  • 歡迎體驗 | Android 12 開發者預覽版 3
    今天的開發者預覽版中值得關注的新特性包括: 全新的應用啟動體驗;新的視頻和攝像頭功能,幫助您更好地釋放底層硬體能力;新的精確鬧鐘權限,以幫助用戶節省電量。請繼續閱讀本文了解更多發布亮點。如需了解本次預覽版的詳情,並獲取有關下載並安裝到 Pixel 設備的信息,請訪問 Android 12 開發者網站。如果您已經安裝了開發者預覽版 2,我們還將提供無線 (OTA) 更新。
  • Android-二維碼掃描與生成二維碼詳解
    添加權限<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature