連載 | Android之Camera1實現相機開發

1、前言

如今不少app都會有拍照功能,通常調用系統進行拍照裁剪就能知足平時的需求,但有些場景或者特殊狀況下如:持續不間斷拍多張照片或者是進行人臉識別的時候,這時候之間調用系統原生相機拍照時不能知足本身的開發需求,就須要使用原生Camera來進行自定義開發,本文會採用android.hardware.CameraAPI來進行開發。在Android生態中,Camera是碎片化較爲嚴重的一塊,由於如今Android自己有三套API:php

  • Camera:Android 5.0如下
  • Camera2:Android 5.0以上
  • CameraX:基於Camera2API實現,極大簡化在minsdk21及以上版本的實現過程

下面打算分別採用camera1,camera2,cameraX來實現相機開發。java

另外各家廠商(華爲,OPPO,VIVO,小米)都對Camera2支持程度各不相同,從而致使須要花很大功夫來作適配工做,不少時候直接採用camera1進行開發。 作過相機的同窗都知道,Camera1相機開發通常分爲五個步驟:android

  • 檢測相機資源,若是存在相機資源,就請求訪問相機資源,不然就結束
  • 建立預覽界面,通常是繼承SurfaceView而且實現SurfaceHolder接口的拍攝預覽類,而且建立佈局文件,將預覽界面和用戶界面綁定,進行實時顯示相機預覽圖像
  • 建立拍照監聽器來響應用戶的不一樣操做,如開始拍照,中止拍照等
  • 拍照成功後保存文件,將拍攝得到的圖像文件轉成位圖文件,而且保存輸出須要的格式圖片
  • 釋放相機資源,當相機再也不使用時,進行釋放

瞭解完開發步驟後,由於本文是針對Camera1來進行開發,那下面先了解一些具體的類和方法。git

2、Surface、SurfaceView、SurfaceHolder

1.Surface

Surface根據英文直譯是表面的意思,在源碼中有這樣描述的:github

/** * Handle onto a raw buffer that is being managed by the screen compositor. * * <p>A Surface is generally created by or from a consumer of image buffers (such as a * {@link android.graphics.SurfaceTexture}, {@link android.media.MediaRecorder}, or * {@link android.renderscript.Allocation}), and is handed to some kind of producer (such as * {@link android.opengl.EGL14#eglCreateWindowSurface(android.opengl.EGLDisplay,android.opengl.EGLConfig,java.lang.Object,int[],int) OpenGL}, * {@link android.media.MediaPlayer#setSurface MediaPlayer}, or * {@link android.hardware.camera2.CameraDevice#createCaptureSession CameraDevice}) to draw * into.</p> */
複製代碼

上面的意思:Surface是用來處理屏幕顯示內容合成器所管理的原始緩存區工具,它一般由圖像緩衝區的消費者來建立如(SurfaceTexture,MediaRecorder),而後被移交給生產者(如:MediaPlayer)或者顯示到其上(如:CameraDevice),從上面能夠得知:面試

  • Surface一般由SurfaceTexture或者MediaRecorder來建立
  • Surface最後顯示在MediaPlayer或者CameraDevice上
  • 經過Surface就能夠得到管理原始緩存區的數據
  • 原始緩衝區(rawbuffer)是用來保存當前窗口的像素數據

Surface內有一個Canvas成員:canvas

private final Canvas mCanvas = new CompatibleCanvas();
複製代碼

咱們知道,畫圖都是在Canvas對象上來畫的,由於Suface持有Canvas,那麼咱們能夠這樣認爲,Surface是一個句柄,而Canvas是開發者畫圖的場所,就像黑板,而原生緩衝器(rawbuffer)是用來保存數據的地方,全部獲得Surface就能獲得其中的Canvas和原生緩衝器等其餘內容。數組

2.SurfaceView

SurfaceView簡單理解就是Surface的View。緩存

/** * Provides a dedicated drawing surface embedded inside of a view hierarchy. * You can control the format of this surface and, if you like, its size; the * SurfaceView takes care of placing the surface at the correct location on the * screen */
複製代碼

意思就是SurfaceView提供了嵌入視圖層級中的專用surface,你能夠控制surface的格式或者大小(經過SurfaceView就能夠看到Surface部分或者所有內容),SurfaceView負責把surface顯示在屏幕的正確位置。網絡

public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback {
....
final Surface mSurface = new Surface();       // Current surface in use
....
private final SurfaceHolder mSurfaceHolder = new SurfaceHolder(){
    .....
 }
}
複製代碼

SurfaceView繼承自View,而且其中有兩個成員變量,一個是Surface對象,一個是SurfaceHolder對象,SurfaceViewSurface顯示在屏幕上,SurfaceView經過SurfaceHolder得知Surface的狀態(建立、變化、銷燬),能夠經過getHolder()方法得到當前SurfaceViewSurfaceHolder對象,而後就能夠對SurfaceHolder對象添加回調來監聽Surface的狀態。

Surface是從Object派生而來,實現了Parcelable接口,看到Parcelable很容易讓人想到數據,而SurfaceView就是用來展現Surface數據的,二者的關係能夠用下面一張圖來描述:

SurfaceView和Suface
Surface是經過SurfaceView才能展現其中內容。

到這裏也許你們會有一個疑問,SurfaceView和普通的View有什麼區別?相機開發就必定要用SurfaceView嗎?

首先普通的View和其派生類都是共享同一個Surface,全部的繪製必須在主線程(UI線程)進行,經過Surface得到對應的Canvas,完成繪製View的工做。

SurfaceView是特殊的View,它不與其餘普通的view共享Surface,在本身內部持有Surface能夠在獨立的線程中進行繪製,在自定義相機預覽圖像這塊,更新速度比較快和幀率要求比較高,若是用普通的View去更新,極大可能會阻塞UI線程,SurfaceView是在一個新起的線程去更新畫面並不會阻塞UI線程。還有SurfaceView底層實現了雙緩衝機制,雙緩衝技術主要是爲了解決反覆局部刷新帶來的閃爍問題,對於像遊戲,視頻這些畫面變化特別頻繁,若是前面沒有顯示完,程序又從新繪製,這樣會致使屏幕不停得閃爍,而雙緩衝及時會把要處理的圖片在內存中處理後,把要畫的東西先畫到一個內存區域裏,而後總體一次行畫處理,顯示在屏幕上。舉例說明: 在Android中,若是自定義View大多數都會重寫onDraw方法,onDraw方法並非繪製一點顯示一點,而是繪製完成後一次性顯示到屏幕上。由於CPU訪問內存的速度遠遠大於訪問屏幕的速度,若是須要繪製大量複雜的圖形時,每次都一個個從內存讀取圖形而後繪製到屏幕就會形成屢次訪問屏幕,這些效率會很低。爲了解決這個問題,咱們能夠建立一個臨時的Canvas對象,將圖像都繪製到這個臨時的Canvas對象中,繪製完成後經過drawBitmap方法繪製到onDraw方法中的Canvas對象中,這樣就相對於Bitmap的拷貝過程,比直接繪製效率要高。

因此相機開發中最適合用SurfaceView來繪製。

3.SurfaceHolder

/** * Abstract interface to someone holding a display surface. Allows you to * control the surface size and format, edit the pixels in the surface, and * monitor changes to the surface. This interface is typically available * through the {@link SurfaceView} class. * * <p>When using this interface from a thread other than the one running * its {@link SurfaceView}, you will want to carefully read the * methods * {@link #lockCanvas} and {@link Callback#surfaceCreated Callback.surfaceCreated()}. */
 public interface SurfaceHolder {
    ....
     public interface Callback {

        public void surfaceCreated(SurfaceHolder holder);

        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height);

        public void surfaceDestroyed(SurfaceHolder holder);
        ...
    }
 }
複製代碼

這是一個抽象的接口給持有surface對象使用,容許你控制surface的大小和格式,編輯surface中的像素和監聽surface的變化,這個接口一般經過SurfaceView這個類來得到。

另外SurfaceHolder中有一個Callback接口,這個接口有三個方法:

  • public void surfaceCreated(SurfaceHolder holder)

    surface第一次建立回調

  • public void surfaceChanged(SurfaceHolder,int format,int width,int height)

    surface變化的時候會回調

  • public void surfaceDestroyed(SurfaceHolder holder)

    surface銷燬的時候回調

除了上面Callback接口比較重要外,另外還有如下幾個方法也比較重要:

  • public void addCallback(Callback callback)

    爲SurfaceHolder添加回調接口

  • public void removeCallback(Callback callback)

    對SurfaceHolder移除回調接口

  • public Canvas lockCanvas()

    獲取Canvas對象而且對它上鎖

  • public Canvas lockCanvas(Rect dirty)

    獲取一個Canvas對象,而且對它上鎖,可是所動的內容是dirty所指定的矩形區域

  • public void unlockCanvasAndPost(Canvas canvas)

    當修改Surface中的數據完成後,釋放同步鎖,而且提交改變,將新的數據進行展現,同時Surface中的數據會丟失,加鎖的目的就是爲了在繪製的過程當中,Surface數據不會被改變。

  • public void setType(int type)

    設置Surface的類型,類型有如下幾種:

    SURFACE_TYPE_NORMAL:用RAM緩存原生數據的普通Surface

    SURFACE_TYPE_HARDWARE:適用於DMA(Direct memory access)引擎和硬件加速的Surface

    SURFACE_TYPE_GPU:適用於GPU加速的Surface

    SURFACE_TYPE_PUSH_BUFFERS:代表該Surface不包含原生數據,Surface用到的數據由其餘對象提供,在Camera圖像預覽中就使用該類型的Surface,有Camera負責提供給預覽Surface數據,這樣圖像預覽會比較流暢,若是設置這種類型就不能調用lockCanvas來獲取Canvas對象。

    到這裏,會發現SurfaceSurfaceViewSurfaceHolder就是典型的MVC模型。

    • Surface:原始數據緩衝區,MVC中的M
    • SurfaceView:用來繪製Surface的數據,MVC中的V
    • SurfaceHolder:控制Surface尺寸格式,而且監聽Surface的更改,MVC中的C

上面三者的關係能夠用下面一張圖來表示:

三者關係圖

3、Camera

查看源碼時,發現android.hardware.cameragoogle不推薦使用了:

camera
下面講講 Camera最主要的成員和一些接口:

Camera核心類

1.CameraInfo

Camera類裏,CameraInfo是靜態內部類:

/** * Information about a camera * 用來描述相機信息 * @deprecated We recommend using the new {@link android.hardware.camera2} API for new * applications. * 推薦在新的應用使用{android.hardware.camera2}API */
    @Deprecated
    public static class CameraInfo {
        /** * The facing of the camera is opposite to that of the screen. * 相機正面和屏幕正面相反,意思是後置攝像頭 */
        public static final int CAMERA_FACING_BACK = 0;

        /** * The facing of the camera is the same as that of the screen. * 相機正面和屏幕正面一致,意思是前置攝像頭 */
        public static final int CAMERA_FACING_FRONT = 1;

        /** * The direction that the camera faces. It should be * CAMERA_FACING_BACK or CAMERA_FACING_FRONT. * 攝像機面對的方向,它只能是CAMERA_FACING_BACK或者CAMERA_FACING_FRONT * */
        public int facing;

        /** * <p>The orientation of the camera image. The value is the angle that the * camera image needs to be rotated clockwise so it shows correctly on * the display in its natural orientation. It should be 0, 90, 180, or 270.</p> * orientation是相機收集圖片的角度,這個值是相機採集的圖片須要順時針旋轉才能正確顯示自 * 然方向的圖像,它必須是0,90,180,270中 * * * <p>For example, suppose a device has a naturally tall screen. The * back-facing camera sensor is mounted in landscape. You are looking at * the screen. If the top side of the camera sensor is aligned with the * right edge of the screen in natural orientation, the value should be * 90. If the top side of a front-facing camera sensor is aligned with * the right of the screen, the value should be 270.</p> * 舉個例子:假設如今豎着拿着手機,後面攝像頭傳感器是橫向(水平方向)的,你如今正在看屏幕 * 若是攝像機傳感器的頂部在天然方向上右邊,那麼這個值是90度(手機是豎屏,傳感器是橫屏的)* * 若是前置攝像頭的傳感器頂部在手機屏幕的右邊,那麼這個值就是270度,也就是說這個值是相機圖像順時針 * 旋轉到設備天然方向一致時的角度。 * */
        public int orientation;

        /** * <p>Whether the shutter sound can be disabled.</p> * 是否禁用開門聲音 */
        public boolean canDisableShutterSound;
    };
複製代碼

1.1.orientation

可能不少人對上面orientation解釋有點懵,這裏重點講一下orientation,首先先知道四個方向:屏幕座標方向天然方向圖像傳感器方向相機預覽方向

1.1.1.屏幕座標方向

屏幕方向
在Android系統中,以屏幕左上角爲座標系統的原點(0,0)座標,向右延伸是X軸的正方向,向下延伸是y軸的正方向,如上圖所示。

1.1.2.天然方向

每一個設備都有一個天然方向,手機和平板天然方向不同,在Android應用程序中,android:screenOrientation來控制activity啓動時的方向,默認值unspecified即爲天然方向,固然能夠取值爲:

  • unspecified,默認值,天然方向
  • landscape,強制橫屏顯示,正常拿設備的時候,寬比高長,這是平板的天然方向
  • portrait,正常拿着設備的時候,寬比高短,這是手機的天然方向
  • behind:和前一個Activity方向相同
  • sensor:根據物理傳感器方向轉動,用戶90度,180度,270度旋轉手機方向
  • sensorLandScape:橫屏選擇,通常橫屏遊戲會這樣設置
  • sensorPortait:豎屏旋轉
  • nosensor:旋轉設備的時候,界面不會跟着旋轉,初始化界面方向由系統控制
  • user:用戶當前設置的方向

默認的話:平板的天然方向是橫屏,而手機的天然方向是豎屏方向。

1.1.3.圖像傳感器方向

手機相機的圖像數據都是來自於攝像頭硬件的圖像傳感器,這個傳感器在被固定到手機上後有一個默認的取景方向,方向通常是和手機橫屏方向一致,以下圖:

傳感器方向
和豎屏應用方向呈90度。

1.1.4.相機預覽方向

將圖像傳感器捕獲的圖像,顯示在屏幕上的方向。在默認狀況下,和圖像傳感器方向一致,在相機API中能夠經過setDisplayOrientation(int degrees)設置預覽方向(順時針設置,不是逆時針)。默認狀況下,這個值是0,在註釋文檔中:

/** * Set the clockwise rotation of preview display in degrees. This affects * the preview frames and the picture displayed after snapshot. This method * is useful for portrait mode applications. Note that preview display of * front-facing cameras is flipped horizontally before the rotation, that * is, the image is reflected along the central vertical axis of the camera * sensor. So the users can see themselves as looking into a mirror. * * <p>This does not affect the order of byte array passed in {@link * PreviewCallback#onPreviewFrame}, JPEG pictures, or recorded videos. This * method is not allowed to be called during preview. * * 設置預覽顯示的順時針旋轉角度,會影響預覽幀和拍拍照後顯示的圖片,這個方法對豎屏模式的應用 * 頗有用,前置攝像頭進行角度旋轉以前,圖像會進行一個水平的鏡像翻轉,用戶在看預覽圖像的時候* 就像鏡子同樣了,這個不影響PreviewCallback的回調,生成JPEG圖片和錄像文件的方向。 * */
複製代碼
1.1.4.1.後置

注意,對於手機來講:

  • 橫屏下:由於屏幕方向和相機預覽方向一致,因此預覽圖像和看到的實物方向一致
  • 豎屏下:屏幕方向和預覽方向垂直,會形成旋轉90度現象,不管怎麼旋轉手機,UI預覽界面和實物始終是90度,爲了獲得一致的預覽界面須要將相機預覽方向旋轉90度(setDisplayOrientation(90)),這樣預覽界面和實物方向一致。

下面舉個簡單例子:

相機預覽界面座標解析

這裏重點講解一下豎屏下:

相機和圖像傳感器方向

後置相機預覽圖像

須要結合上下兩張圖來看:

  • 當圖像傳感器得到圖像後,就會知道這幅圖像每一個座標的像素值,可是要顯示到屏幕上就要根據屏幕天然方向的座標來顯示(豎屏下屏幕天然方向座標系和後置相機圖像傳感器方向呈90度),因此圖像會逆時針旋轉旋轉90度,顯示到屏幕座標系上。
  • 那麼收集的圖像時逆時針旋轉了90度,那麼這時候須要順時針旋轉90度才能和收集的天然方向保持一致,也就是和實物圖方向同樣。
1.1.4.2.前置

Android中,對於前置攝像頭,有如下規定:

  • 在預覽圖像是真實物體的鏡像
  • 拍出的照片和真實場景同樣

前置相機預覽界面座標解析

同理這裏重點講一下,前置豎屏

前置相機和圖像傳感器方向

前置相機收集圖像方向

前置相機預覽界面座標解析

前置相機預覽圖像方向

在前置相機中,預覽圖像相機收集圖像是鏡像關係,上面圖中Android圖標中前置收集圖像預覽圖像時相反的,前置相機圖像傳感器方向和前置相機預覽圖像方向是左右相反的,上圖也有體現。

  • 前置攝像頭收集到圖像後(沒有通過鏡像處理),可是要顯示到屏幕上,就要按照屏幕天然方向的座標系來進行顯示,須要順時針旋轉270度(API沒有提供逆時針90度的方法),才能和手機天然方向一致。
  • 在預覽的時候,作了鏡像處理,因此只須要順時針旋轉90度,就能和天然方向一致,由於攝像圖像沒有作水平翻轉,因此前置攝像頭拍出來的圖片,你會發現跟預覽的時候是左右翻轉的,本身能夠根據需求作處理。 上面把角度知識梳理了,後面會經過代碼一步一步驗證,下面按照最開始的思惟導圖繼續看Camera內的方法:

1.2.facing

facing表明相機方向,可取值有二:

  • CAMREA_FACING_BACK,值爲0,表示是後置攝像頭
  • CAMERA_FACING_FRONT,值爲1,表示是前置攝像頭

1.3.canDisableShutterSound

是否禁用快門聲音

2.PreviewCallback

2.1.void onPreviewFrame(byte[] data, Camera camera)

PreviewCallback是一個接口,能夠給Camera設置Camrea.PreviewCallback,而且實現這個onPreviewFrame(byte[] data, Camera camera)這個方法,就能夠去Camera預覽圖片時的數據,若是設置Camera.setPreviewCallback(callback)onPreviewFrame這個方法會被一直調用,能夠在攝像頭對焦成功後設置camera.setOneShotPreviewCallback(previewCallback),這樣設置onPreviewFrame這個方法就會被調用異常,處理data數據,data是相機預覽到的原始數據,能夠保存下來當作一張照片。

3.AutoFocusCallback

3.1.onAutoFocus(boolean success,Camera camera)

AutoFocusCallback是一個接口,用於在相機自動對焦完成後時通知回調,第一個參數是相機是否自動對焦成功,第二個參數是相機對象。

4.Face

做爲靜態內部類,用來描述經過相機人臉檢測識別的人臉信息。

4.1.rect

Rect對象,表示檢測到人臉的區域,這個Rect對象中的座標並非安卓屏幕中的座標,須要進行轉換才能使用。

4.2.score

人臉檢測的置信度,範圍是1到100。100是最高的信度

4.3.leftEye

是一個Point對象,表示的是檢測到左眼的位置座標

4.4.rightEye

是一個Point對象,表示的是檢測到右眼的位置座標

4.5.mouth

同時一個Point對象,表示的是檢測到嘴的位置座標 leftEyerightEyemouth有可能得到不到,並非全部相機支持,不支持狀況下,獲取爲空。

5.Size

表明拍照圖片的大小。

5.1.width

拍照圖片的寬

5.2.height

拍照圖片的高

6.FaceDetectionListener

這是一個接口,當開始預覽(人臉識別)的時候開始回調

6.1.onFaceDetection(Face[] faces,Camera camera)

通知監聽器預覽幀檢測到的人臉,Face[]是一個數組,用來存放檢測的人臉(存放多張人臉),第二個參數是識別人臉的相機。

7.Parameters

Camera做爲內部類存在,是相機配置設置類,不一樣設備可能具備不一樣的照相機功能,如圖片大小或者閃光模式。

7.1.setPreviewSize(int width,int height)

設置預覽相機圖片的大小,width是圖片的寬,height是圖片的高

7.2.setPreviewFormat(int pixel_format)

設置預覽圖片的格式,有如下格式:

  • ImageFormat.NV16
  • ImageFormat.NV21
  • ImageFormat.YUY2
  • ImageFormat.YV12
  • ImgaeFormat.RGB_565
  • ImageFormat.JPEG 若是不設置返回的數據,會默認返回NV21編碼數據。

7.3.setPictureSize(int width,int height)

設置保存圖片的大小,width圖片的寬度,以像素爲單位,height是圖片的高度,以像素爲單位。

7.4.setPictureFormat(int pixel_format)

設置保存圖片的格式,取值和setPreviewFormat格式同樣。

7.5.setRotation(int degree)

上面已經講過,設置相機採集照片的角度,這個值是相機所採集的圖片須要順時針選擇到天然方向的角度值,它必須是0,90,180或者270中的一個。

7.6.setFocusMode(String value)

設置相機對焦模式,對焦模式有如下:

  • AUTO
  • INFINITY
  • MACRO
  • FIXED
  • EDOF
  • CONTINUOUS_VIDEO

7.7.setZoom(int value)

設置縮放係數,也就是日常所說的變焦。

7.8.getSupportedPreviewSizes()

返回相機支持的預覽圖片大小,返回值是一個List<Size>數組,至少有一個元素。

7.9.getSupportedVideoSizes()

返回獲取相機支持的視頻幀大小,能夠經過MediaRecorder來使用。

7.10.getSupportedPreviewFormats()

返回相機支持的圖片預覽格式,全部相機都支持ImageFormat.NV21,返回是集合類型而且返回至少包含一個元素。

7.11.getSupportedPictureSize()

以集合的形式返回相機支持採集的圖片大小,至少返回一個元素。

7.12.getSupportedPictureFormats()

以集合的形式返回相機支持的圖片(拍照後)格式,至少返回一個元素。

7.13.getSupportedFocusModes()

以集合的形式返回相機支持的對焦模式,至少返回一個元素。

7.14.getMaxNumDetectedFaces()

返回相機所支持的最多人臉檢測數,若是返回0,則說明制定類型的不支持人臉識別。若是手機攝像頭支持最多的人臉檢測個數是5個,當畫面超出5我的臉數,仍是檢測到5我的臉數。

7.15.getZoom()

返回當前縮放值,這個值的範圍在0到getMaxZoom()之間。

8.getNumberOfCameras()

返回當前設備可用的攝像頭個數。

9.getCameraInfo(int cameraId,CameraInfo cameraInfo)

返回指定id所表示的攝像頭信息,若是getNumberOfCameras()返回N,那麼有效的id值爲0~(N-1),通常手機至少有先後兩個攝像頭。

10.open(int cameraId)

使用傳入的id所表示的攝像頭來建立Camera對象,若是這個id所表示的攝像頭被其餘應用程序打開調用此方法會跑出異常,當使用完相機後,必須調用release()來釋放資源,不然它會保持鎖定狀態,不可用其餘應用程序。

11.setPreviewDisplay(SurfaceHolder holder)

根據所傳入的SurfaceHolder對象來設置實時預覽。

12.setPreviewCallback(PreviewCallback cb)

根據傳入的PreviewCallback對象來監聽相機預覽數據的回調,PreviewCallback再上面已經講過。

13.setParameters(Parameters params)

根據傳入的Parameters對象來設置當前相機的參數信息。

14.getParameters()

根據傳入的Parameters對象來返回當前相機的參數信息

15.startPreview()

開始預覽,在屏幕上繪製預覽幀,若是沒有調用setPreviewDisplay(SurfaceHolder)或者setPreviewTexture(SurfaceTexture)直接調用這個方法是沒有任何效果的,若是啓動預覽失敗,則會引起RuntimeException。

16.stopPreview()

中止預覽,中止繪製預覽幀到屏幕,若是中止失敗,會引起RuntimeException。

17.startFaceDetection()

開始人臉識別,這個要調用startPreview以後調用,也就是在預覽以後才能進行人臉識別,若是不支持人臉識別,調用此方法會拋出IllegalArgumentException。

18.stopFaceDetection()

中止人臉識別。

19.setFaceDetectionListener()

給人臉檢測設置監聽,以便提供預覽幀。

20.release()

斷開而且釋放相機對象資源。

21.setDisplayOrientation(int degress)

設置相機預覽畫面旋轉的角度,在剛開始講述orientation的時候講述角度問題,查看源碼時,有如下注釋:

public static void setCameraDisplayOrientation(Activity activity, int cameraId, android.hardware.Camera camera) {
     android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
     android.hardware.Camera.getCameraInfo(cameraId, info);
     //獲取window(Activity)旋轉方向
     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;  // compensate the mirror
     } else {  // back-facing
         result = (info.orientation - degrees + 360) % 360;
     }
     //調整圖像旋轉角度
     camera.setDisplayOrientation(result);
 }
複製代碼

上面已經描述過在豎屏下,對於後置相機來說:

只須要旋轉後置相機的orientation也就是90度便可和屏幕方向保持一致;

對於前置相機預覽方向,相機預覽的圖像是相機採集到的圖像鏡像,因此旋轉orientation 270-180=90度才和屏幕方向一致。 CameraInfo是實例化的相機類,info.orientation是相機對於屏幕天然方向(左上角座標系)的旋轉角度數。 那下面跟着官方適配方法走:

  • int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); rotation是預覽Window的旋轉方向,對於手機而言,當在清單文件設置Activity的screenOrientation="portait"時,rotation=0,這時候沒有旋轉,當screenOrientation="landScape"時,rotation=1。

  • 對於後置攝像頭,手機豎屏顯示時,預覽圖像旋轉的角度:result=(90-0+360)%360=90;手機橫屏顯示時,預覽圖像旋轉:result = (90-0+360)%360 = 0;

  • camera.setDisplayOrientation(int param)這個方法是圖片輸出後所旋轉的角度數,旋轉值能夠是0,90,180,270。

注意: camera.setDisplayOrientation(int param)這個方法僅僅是修改相機的預覽方向,不會影響到PreviewCallback回調、生成的JPEG圖片和錄像視頻的方向,這些數據的方向會和圖像Sensor方向一致。

4、具體實踐

1.權限處理

須要申請拍照權限和外部存儲權限:

<!--權限申請 相機-->
    <uses-permission android:name="android.permission.CAMERA"/>
    <!--使用uses-feature指定須要相機資源-->
    <uses-feature android:name="android.hardware.Camera"/>
    <!--須要自動聚焦 -->
    <uses-feature android:name="android.hardware.camera.autofocus"/>
    <!--存儲圖片或者視頻-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
複製代碼

onCreate檢查權限:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initBind();
        initListener();
        checkNeedPermissions();
    }
複製代碼
/** * 檢測須要申請的權限 * */
    private void checkNeedPermissions(){
        //6.0以上須要動態申請權限 動態權限校驗 Android 6.0 的 oppo & vivo 手機時,始終返回 權限已被容許 可是當真正用到該權限時,卻又彈出權限申請框。
        if (Build.VERSION.SDK_INT >= 23) {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                    != PackageManager.PERMISSION_GRANTED
                    || ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED) {
                //多個權限一塊兒申請
                ActivityCompat.requestPermissions(this, new String[]{
                        Manifest.permission.CAMERA,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE
                }, 1);

            } else {
                //已經所有申請 初始化相機資源
                initCamera();
            }

        }else{
            //6.0如下不用動態申請
            initCamera();
        }
    }
複製代碼

onRequestPermissionsResult處理回調:

/** * 動態處理申請權限的結果 * 用戶點擊贊成或者拒絕後觸發 * * @param requestCode 請求碼 * @param permissions 權限 * @param grantResults 結果碼 */
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case 1:
                //獲取權限一一驗證
                if (grantResults.length > 1) {
                    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        if (grantResults[1] == PackageManager.PERMISSION_GRANTED) {
                            initCamera();
                        } else {
                            //拒絕就要強行跳轉設置界面
                            Permissions.showPermissionsSettingDialog(this, permissions[1]);
                        }
                    } else {
                        //拒絕就要強行跳轉設置界面
                        Permissions.showPermissionsSettingDialog(this, permissions[0]);
                    }
                } else {
                    ToastUtil.showShortToast(this, "請從新嘗試~");
                }
                break;
        }
    }
複製代碼

2.調用系統相機

/** * 調用系統相機 * */
    private void goSystemCamera(){
        //在根目錄建立jpg文件
        cameraSavePath = new File(Environment.getExternalStorageDirectory().getPath() + "/" + System.currentTimeMillis() +".jpg");
        //指定跳到系統拍照
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        //適配Android 7.0以上版本應用私有目錄限制被訪問
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
           uri = FileProvider.getUriForFile(this, SystemUtil.getPackageName(getApplicationContext()) + ".fileprovider",cameraSavePath);
           intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        }else{
            //7.0如下
            uri = Uri.fromFile(cameraSavePath);
        }
        //指定ACTION爲MediaStore.EXTRA_OUTPUT
        intent.putExtra(MediaStore.EXTRA_OUTPUT,uri);
        //請求碼賦值爲1
        startActivityForResult(intent,1);
    }
複製代碼

OnActivityResult(int requestCode,int resultCode,Intent data)方法作處理:

@Override
    protected void onActivityResult(int requestCode,int resultCode,Intent data){
        String photoPath;
        //處理拍照後返回的圖片路徑
        if(requestCode == 1 && resultCode == RESULT_OK){
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
               photoPath = String.valueOf(cameraSavePath);
            }else{
               photoPath = uri.getEncodedPath();
            }
            Log.d("拍照返回圖片的路徑:",photoPath);
            Glide.with(this).load(photoPath).apply(RequestOptions.noTransformation()
            .override(iv_photo.getWidth(),iv_photo.getHeight())
            .error(R.drawable.default_person_icon))
            .into(iv_photo);
        }else if(requestCode == 2 && resultCode == RESULT_OK){
            //處理調用相冊返回的路徑
            photoPath = PhotoAlbumUtil.getRealPathFromUri(this,data.getData());
            Glide.with(this).load(photoPath).apply(RequestOptions.noTransformation()
                    .override(iv_photo.getWidth(),iv_photo.getHeight())
                    .error(R.drawable.default_person_icon))
                    .into(iv_photo);

        }
        super.onActivityResult(requestCode, resultCode, data);

    }
複製代碼

2.1.實際效果

調用系統相機效果
上面是調用系統相機拍照後的效果,另外照片存儲到了外部存儲的根目錄位置:

系統相機存儲路徑

3.自定義相機

下面按照如下步驟來實現自定義相機開發:

  • 在佈局xml文件中定義SurfaceView用於預覽,經過SurfaceView.getHolder獲取SurfaceHolder對象
  • 給SurfaceHolder對象設置監聽回調,實現三個方法surfaceCreated(SurfaceHolder holder)、surfaceChanged(SurfaceHolder holder, int format, int width, int height)、surfaceDestroyed(SurfaceHolder holder)
  • 在surfaceCreated(SurfaceHolder holder)方法裏經過傳入的相機id來Camera.open(int cameraId)打開相機
  • 給相機設置具體參數,如:預覽格式,對焦模式
  • 經過Camera.setPreviewDisplay(SurfaceHolder holder)設置實時預覽
  • 根據官方方法來設置正確的照片預覽方向
  • 調用Camera.startPreview()開始預覽
  • 同時能夠調用Camera.startFaceDetection來人臉檢測,並設置回調,重寫onFaceDetection(Camera.Face[] faces, Camera camera)獲得檢測人臉數量
  • 調用Camera.takePicture來進行拍照
  • 處理保存的照片,旋轉或者壓縮
  • 當相機再也不調用時,釋放相機資源

3.1.佈局文件

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent">
    <SurfaceView android:id="@+id/sf_camera" android:layout_width="match_parent" android:layout_height="match_parent"/>
    <android.support.constraint.ConstraintLayout android:id="@+id/cl_bottom" android:layout_width="match_parent" android:layout_height="80dp" app:layout_constraintBottom_toBottomOf="parent" >
        <!-- 拍照後顯示的圖片-->
        <ImageView android:id="@+id/iv_photo" android:layout_width="40dp" android:layout_height="40dp" android:layout_marginLeft="20dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" />
        <!-- 拍照按鈕-->
        <TextView android:id="@+id/tv_takephoto" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/icon_take_photo_selector" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/>
    </android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>
複製代碼

佈局文件主要有拍照預覽控件SurfaceView、拍照後顯示的圖片Imageview、拍照按鈕Textview組成。

3.2.初始化SurfaceHolder

新增相機業務邏輯類CameraPresenter,目的是將業務和界面顯示分開,Activity負責UI的顯示,業務邏輯在CameraPresenter,新增構造函數,構造函數有兩個參數,分別是持有手機界面的ActivitySurfaceView對象,並根據傳入的SurfaceView對象經過SurfaceView.getHolder方法獲取SurfaceHolder對象:

public CameraPresenter(AppCompatActivity mAppCompatActivity, SurfaceView mSurfaceView) {
        this.mAppCompatActivity = mAppCompatActivity;
        this.mSurfaceView = mSurfaceView;
        mSurfaceHolder = mSurfaceView.getHolder();
    }
複製代碼

SurfaceHolder對象設置監聽回調:

public CameraPresenter(AppCompatActivity mAppCompatActivity, SurfaceView mSurfaceView) {
        this.mAppCompatActivity = mAppCompatActivity;
        this.mSurfaceView = mSurfaceView;
        mSurfaceHolder = mSurfaceView.getHolder();
        init();
    }
    /** * 初始化增長回調 */
    private void init() {
        mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                //surface建立時執行
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
                //surface繪製時執行
            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                //surface銷燬時執行
            }
        });
    }
複製代碼

3.3.打開相機

surfaceCreated(SurfaceHolder holder)方法裏調用打開相機:

//攝像頭Id 默認後置 0,前置的值是1
    private int mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
     @Override
     public void surfaceCreated(SurfaceHolder holder) {
         //surface建立時執行
         if (mCamera == null) {
             // mCameraId是後置仍是前置 0是後置 1是前置
             openCamera(mCameraId);
            }
        }
     /** * 打開相機 而且判斷是否支持該攝像頭 * * @param FaceOrBack 前置仍是後置 * @return */
    private boolean openCamera(int FaceOrBack) {
        //是否支持先後攝像頭
        boolean isSupportCamera = isSupport(FaceOrBack);
        //若是支持
        if (isSupportCamera) {
            try {
                mCamera = Camera.open(FaceOrBack);
            } catch (Exception e) {
                e.printStackTrace();
                ToastUtil.showShortToast(mAppCompatActivity, "打開相機失敗~");
                return false;
            }

        }

        return isSupportCamera;
    }
複製代碼

3.4.設置相機具體參數

調用Camera.open(int cameraId)後返回具體的Camera對象後,還須要設置相機一些參數,如預覽模式,對焦模式等:

/** * 打開相機 而且判斷是否支持該攝像頭 * * @param FaceOrBack 前置仍是後置 * @return */
private boolean openCamera(int FaceOrBack) {
    //是否支持先後攝像頭
    boolean isSupportCamera = isSupport(FaceOrBack);
    //若是支持
    if (isSupportCamera) {
        try {
            mCamera = Camera.open(FaceOrBack);
            initParameters(mCamera);
            //設置預覽回調
            if (mCamera != null) {
                mCamera.setPreviewCallback(this);
            }
        } catch (Exception e) {
            e.printStackTrace();
            ToastUtil.showShortToast(mAppCompatActivity, "打開相機失敗~");
            return false;
        }

    }

    return isSupportCamera;
}


/** * 設置相機參數 * * @param camera */
private void initParameters(Camera camera) {
    try {
        //獲取Parameters對象
        mParameters = camera.getParameters();
        //設置預覽格式
        mParameters.setPreviewFormat(ImageFormat.NV21);
        //判斷是否支持連續自動對焦圖像
        if (isSupportFocus(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
            mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
        //判斷是否支持單次自動對焦 
        } else if (isSupportFocus(Camera.Parameters.FOCUS_MODE_AUTO)) {
            //自動對焦(單次)
            mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
        }
        //給相機設置參數
        mCamera.setParameters(mParameters);
    } catch (Exception e) {
        e.printStackTrace();
        ToastUtil.showShortToast(mAppCompatActivity, "初始化相機失敗");
    }
複製代碼

3.5.開始預覽

設置完相機參數以後,就能夠須要相機調用Camera.setPreviewDisplay(SurfaceHolder holder)Camera.startPreview()開啓預覽:

/** * 開始預覽 */
private void startPreview() {
    try {
        //根據所傳入的SurfaceHolder對象來設置實時預覽
        mCamera.setPreviewDisplay(mSurfaceHolder);
        mCamera.startPreview();
        //這裏同時開啓人臉檢測
        startFaceDetect();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

/** * 人臉檢測 */
private void startFaceDetect() {
    //開始人臉識別,這個要調用startPreview以後調用
    mCamera.startFaceDetection();
    //添加回調
    mCamera.setFaceDetectionListener(new Camera.FaceDetectionListener() {
        @Override
        public void onFaceDetection(Camera.Face[] faces, Camera camera) {
            mCameraCallBack.onFaceDetect(transForm(faces), camera);
            Log.d("sssd", "檢測到" + faces.length + "人臉");
        }
    });
}
複製代碼

surfaceCreated(SurfaceHolder holder)回調方法調用:

...
@Override
public void surfaceCreated(SurfaceHolder holder) {
       //surface建立時執行
       if (mCamera == null) {
            //mCameraId是後置仍是前置 0是後置 1是前置
            openCamera(mCameraId);
        }
           //並設置預覽
           startPreview();
       }
...
複製代碼

3.6.釋放相機資源

當相機再也不調用的時候,須要調用Camera.release()來釋放相機資源

/** * 釋放相機資源 */
public void releaseCamera() {
    if (mCamera != null) {
        //中止預覽
        mCamera.stopPreview();
        mCamera.setPreviewCallback(null);
        //釋放相機資源
        mCamera.release();
        mCamera = null;
    }
}
複製代碼

surfaceDestroyed(SurfaceHolder holder)調用:

/** * 初始化增長回調 */
private void init() {
    mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            //surface建立時執行 mCameraId是後置仍是前置 0是後置 1是前置
            if (mCamera == null) {
                openCamera(mCameraId);
            }
            //並設置預覽
            startPreview();
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            //surface繪製時執行
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            //surface銷燬時執行
            releaseCamera();
        }
    });
}

/** * 設置前置仍是後置 * * @param mCameraId 前置仍是後置 */
public void setFrontOrBack(int mCameraId) {
    this.mCameraId = mCameraId;

}
複製代碼

在自定義相機的Activity界面進行調用:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_customcamera);
    //綁定View
    initBind();
    //添加點擊,觸摸事件等監聽
    initListener();
    //初始化CameraPresenter
    mCameraPresenter = new CameraPresenter(this,sf_camera);
    //設置後置攝像頭
    mCameraPresenter.setFrontOrBack(Camera.CameraInfo.CAMERA_FACING_BACK);
}

複製代碼

onDestroy()方法調用releaseCamera():

/** * Activity 銷燬回調方法 釋放各類資源 */
@Override
protected void onDestroy(){
    super.onDestroy();
    if(mCameraPresenter != null){
        mCameraPresenter.releaseCamera();
    }
}
複製代碼

如今先看看效果:

效果一

3.7.調整預覽圖像角度

發現預覽效果圖逆時針旋轉了90度,當你把手機橫屏擺放也是,上面已經說過,由於屏幕天然方向和圖像傳感器方向不一致形成的,須要從新設置預覽時的角度,採用官方的推薦方法:

/** * 保證預覽方向正確 * * @param appCompatActivity Activity * @param cameraId 相機Id * @param camera 相機 */
private void setCameraDisplayOrientation(AppCompatActivity appCompatActivity, int cameraId, Camera camera) {
    Camera.CameraInfo info =
            new Camera.CameraInfo();
    Camera.getCameraInfo(cameraId, info);
    //rotation是預覽Window的旋轉方向,對於手機而言,當在清單文件設置Activity的screenOrientation="portait"時,
    //rotation=0,這時候沒有旋轉,當screenOrientation="landScape"時,rotation=1。
    int rotation = appCompatActivity.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;  // compensate the mirror
    } else {  // back-facing
        result = (info.orientation - degrees + 360) % 360;
    }
    orientation = result;
    //調整預覽圖像旋轉角度
    camera.setDisplayOrientation(result);

}
複製代碼

並在startPreview()方法裏調用:

/** * 開始預覽 */
private void startPreview() {
    try {
        //根據所傳入的SurfaceHolder對象來設置實時預覽
        mCamera.setPreviewDisplay(mSurfaceHolder);
        //調整預覽角度
        setCameraDisplayOrientation(mAppCompatActivity,mCameraId,mCamera);
        mCamera.startPreview();
        startFaceDetect();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
複製代碼

再次看下運行效果:

沒有設置預覽界面尺寸的效果

3.8.調整預覽和保存圖像尺寸

上面調整了預覽角度的問題後,由於在市面上安卓機型五花八門,屏幕分辨率也不少,爲了不圖像變形,須要調整預覽圖像和保存的圖像尺寸:

//獲取屏幕寬和高
private int screenWidth, screenHeight;
public CameraPresenter(AppCompatActivity mAppCompatActivity, SurfaceView mSurfaceView) {
    this.mAppCompatActivity = mAppCompatActivity;
    this.mSurfaceView = mSurfaceView;
    mSurfaceHolder = mSurfaceView.getHolder();
    DisplayMetrics dm = new DisplayMetrics();
    mAppCompatActivity.getWindowManager().getDefaultDisplay().getMetrics(dm);
    //獲取寬高像素
    screenWidth = dm.widthPixels;
    screenHeight = dm.heightPixels;
    Log.d("sssd-手機寬高尺寸:",screenWidth +"*"+screenHeight);
    init();
}
/** * * 設置保存圖片的尺寸 */
private void setPictureSize() {
    List<Camera.Size> localSizes = mParameters.getSupportedPictureSizes();
    Camera.Size biggestSize = null;
    Camera.Size fitSize = null;// 優先選預覽界面的尺寸
    Camera.Size previewSize = mParameters.getPreviewSize();//獲取預覽界面尺寸
    float previewSizeScale = 0;
    if (previewSize != null) {
        previewSizeScale = previewSize.width / (float) previewSize.height;
    }

    if (localSizes != null) {
        int cameraSizeLength = localSizes.size();
        for (int n = 0; n < cameraSizeLength; n++) {
            Camera.Size size = localSizes.get(n);
            if (biggestSize == null) {
                biggestSize = size;
            } else if (size.width >= biggestSize.width && size.height >= biggestSize.height) {
                biggestSize = size;
            }

            // 選出與預覽界面等比的最高分辨率
            if (previewSizeScale > 0
                    && size.width >= previewSize.width && size.height >= previewSize.height) {
                float sizeScale = size.width / (float) size.height;
                if (sizeScale == previewSizeScale) {
                    if (fitSize == null) {
                        fitSize = size;
                    } else if (size.width >= fitSize.width && size.height >= fitSize.height) {
                        fitSize = size;
                    }
                }
            }
        }

        // 若是沒有選出fitSize, 那麼最大的Size就是FitSize
        if (fitSize == null) {
            fitSize = biggestSize;
        }
        mParameters.setPictureSize(fitSize.width, fitSize.height);
    }

}


/** * 設置預覽界面尺寸 */
private void setPreviewSize() {
    //獲取系統支持預覽大小
    List<Camera.Size> localSizes = mParameters.getSupportedPreviewSizes();
    Camera.Size biggestSize = null;//最大分辨率
    Camera.Size fitSize = null;// 優先選屏幕分辨率
    Camera.Size targetSize = null;// 沒有屏幕分辨率就取跟屏幕分辨率相近(大)的size
    Camera.Size targetSiz2 = null;// 沒有屏幕分辨率就取跟屏幕分辨率相近(小)的size
    if (localSizes != null) {
        int cameraSizeLength = localSizes.size();
        for (int n = 0; n < cameraSizeLength; n++) {
            Camera.Size size = localSizes.get(n);
            Log.d("sssd-系統支持的尺寸:",size.width + "*" +size.height);
            if (biggestSize == null ||
                    (size.width >= biggestSize.width && size.height >= biggestSize.height)) {
                biggestSize = size;
            }

            //若是支持的比例都等於所獲取到的寬高
            if (size.width == screenHeight
                    && size.height == screenWidth) {
                fitSize = size;
                //若是任一寬或者高等於所支持的尺寸
            } else if (size.width == screenHeight
                    || size.height == screenWidth) {
                if (targetSize == null) {
                    targetSize = size;
                //若是上面條件都不成立 若是任一寬高小於所支持的尺寸
                } else if (size.width < screenHeight
                        || size.height < screenWidth) {
                    targetSiz2 = size;
                }
            }
        }

        if (fitSize == null) {
            fitSize = targetSize;
        }

        if (fitSize == null) {
            fitSize = targetSiz2;
        }

        if (fitSize == null) {
            fitSize = biggestSize;
        }
        Log.d("sssd-最佳預覽尺寸:",fitSize.width + "*" + fitSize.height);
        mParameters.setPreviewSize(fitSize.width, fitSize.height);
    }
}
複製代碼

這裏額外要注意:對於相機來講,都是width是長邊,也就是width > height,在上面setPreviewSize()方法裏,獲取所支持的size.width要和screenHeight比較,size.height要和screenWidth,最後在設置相機裏調用便可:

/** * 設置相機參數 * * @param camera */
private void initParameters(Camera camera) {
    try {
        //獲取Parameters對象
        mParameters = camera.getParameters();
        //設置預覽格式
        mParameters.setPreviewFormat(ImageFormat.NV21);
        setPreviewSize();
        setPictureSize();
        //.....
        mCamera.setParameters(mParameters);
    } catch (Exception e) {
        e.printStackTrace();
        ToastUtil.showShortToast(mAppCompatActivity, "初始化相機失敗");
    }
}
複製代碼

下面看看在vivo x9所支持的尺寸:

所支持的預覽尺寸

3.9.拍照

下面進行拍照處理,拍照保存圖片有兩種方式:

  • 直接調用Camera.takePicture(ShutterCallback shutter,PictureCallback raw,PictureCallback jpeg)
/** * Equivalent to <pre>takePicture(Shutter, raw, null, jpeg)</pre>. * * @see #takePicture(ShutterCallback, PictureCallback, PictureCallback, PictureCallback) */
    public final void takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) {
        takePicture(shutter, raw, null, jpeg);
    }
    /** * @param shutter the callback for image capture moment, or null * @param raw the callback for raw (uncompressed) image data, or null * @param postview callback with postview image data, may be null * @param jpeg the callback for JPEG image data, or null * @throws RuntimeException if starting picture capture fails; usually this * would be because of a hardware or other low-level error, or because * release() has been called on this Camera instance. */
    public final void takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback postview, PictureCallback jpeg) {
            ...
     }
複製代碼

三個參數的takePicture實際調用四個參數的takePicture,只是帶有postview圖像數據的回調,設置爲空了。

  • 在相機預覽的回調中直接保存:
mCamera.setPreviewCallback(new Camera.PreviewCallback() {
            @Override
            public void onPreviewFrame(byte[] data, Camera camera) {

            }
        });
複製代碼

onPreviewFrame以字節數組形式返回具體照片數據,這個方法會不停的回調,這裏不演示這個方法,保存圖片的方法和第一個方法是同樣的。 首先先自定義回調:

//自定義回調
    private CameraCallBack mCameraCallBack;
    public interface CameraCallBack {
        //預覽幀回調
        void onPreviewFrame(byte[] data, Camera camera);

        //拍照回調
        void onTakePicture(byte[] data, Camera Camera);

        //人臉檢測回調
        void onFaceDetect(ArrayList<RectF> rectFArrayList, Camera camera);

        //拍照路徑返回
        void getPhotoFile(String imagePath);
    }
複製代碼

調用Camera.takePicture方法:

/** * 拍照 */
public void takePicture() {
    if (mCamera != null) {
        //拍照回調 點擊拍照時回調 寫一個空實現
        mCamera.takePicture(new Camera.ShutterCallback() {
            @Override
            public void onShutter() {

            }
        }, new Camera.PictureCallback() {
            //回調沒壓縮的原始數據
            @Override
            public void onPictureTaken(byte[] data, Camera camera) {

            }
        }, new Camera.PictureCallback() {
            //回調圖片數據 點擊拍照後相機返回的照片byte數組,照片數據
            @Override
            public void onPictureTaken(byte[] data, Camera camera) {
                //拍照後記得調用預覽方法,否則會停在拍照圖像的界面
                mCamera.startPreview();
                //回調
                mCameraCallBack.onTakePicture(data, camera);
                //保存圖片
                getPhotoPath(data);

            }
        });

    }
}
複製代碼

保存圖片目錄先放在app內:

public class Configuration {
    //這是app內部存儲 格式以下 /data/data/包名/xxx/
    public static String insidePath = "/data/data/com.knight.cameraone/pic/";
    //外部路徑
    public static String OUTPATH = Environment.getExternalStorageDirectory() + "/拍照-相冊/";
}
複製代碼

建立目錄具體方法:

/** * 建立拍照照片文件夾 */
private void setUpFile() {
    photosFile = new File(Configuration.insidePath);
    if (!photosFile.exists() || !photosFile.isDirectory()) {
        boolean isSuccess = false;
        try {
            isSuccess = photosFile.mkdirs();
        } catch (Exception e) {
            ToastUtil.showShortToast(mAppCompatActivity, "建立存放目錄失敗,請檢查磁盤空間~");
            mAppCompatActivity.finish();
        } finally {
            if (!isSuccess) {
                ToastUtil.showShortToast(mAppCompatActivity, "建立存放目錄失敗,請檢查磁盤空間~");
                mAppCompatActivity.finish();
            }
        }

    }
}
複製代碼

在初始化相機時先調用建立文件:

public CameraPresenter(AppCompatActivity mAppCompatActivity, SurfaceView mSurfaceView) {
    //...
    screenWidth = dm.widthPixels;
    screenHeight = dm.heightPixels;
    Log.d("sssd-手機寬高尺寸:",screenWidth +"*"+screenHeight);
    //建立文件夾目錄
    setUpFile();
    init();
}
複製代碼

拍照後保存圖片這種輸出耗時操做應該用線程來處理,新建線程池類:

public class ThreadPoolUtil {
    private static ExecutorService threadPool = Executors.newCachedThreadPool();

    /** * 在線程池執行一個任務 * @param runnable 任務 */
    public static void execute(Runnable runnable){
        threadPool.execute(runnable);
    }

}
複製代碼

getPhotoPath(byte[] data)方法:

/** * @return 返回路徑 */
private void getPhotoPath(final byte[] data) {
    ThreadPoolUtil.execute(new Runnable() {
        @Override
        public void run() {
            long timeMillis = System.currentTimeMillis();
            String time = SystemUtil.formatTime(timeMillis);
            //拍照數量+1
            photoNum++;
            //圖片名字
            String name = SystemUtil.formatTime(timeMillis, SystemUtil.formatTime(photoNum) + ".jpg");
            //建立具體文件
            File file = new File(photosFile, name);
            if (!file.exists()) {
                try {
                    file.createNewFile();
                } catch (Exception e) {
                    e.printStackTrace();
                    return;
                }
            }
            try {
                FileOutputStream fos = new FileOutputStream(file);
                try {
                    //將數據寫入文件
                    fos.write(data);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                //將圖片保存到手機相冊中
                SystemUtil.saveAlbum(Configuration.insidePath + file.getName(), file.getName(), mAppCompatActivity);
                //將圖片複製到外部
                SystemUtil.coptPicture(Configuration.insidePath + file.getName(),Configuration.OUTPATH,file.getName());
                //發消息給主線程
                Message message = new Message();
                message.what = 1;
                //文件路徑
                message.obj = Configuration.insidePath + file.getName();
                mHandler.sendMessage(message);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    });
}
複製代碼

上面代碼先把照片存到app包內,再將照片複製到app包外,當圖片保存處理完後,回調主線程進行顯示圖片:

@SuppressLint("HandlerLeak")
    Handler mHandler = new Handler(){
        @SuppressLint("NewApi")
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    mCameraCallBack.getPhotoFile(msg.obj.toString());
                    break;
                default:
                    break;
            }
        }
    };
複製代碼

Activity中設置回調:

//添加監聽
mCameraPresenter.setCameraCallBack(this);
複製代碼

拍照後保存圖片後顯示在界面上,Activity實現照片顯示:

/** * 返回拍照後的照片 * @param imagePath */
@Override
public void getPhotoFile(String imagePath) {
    //設置頭像
    Glide.with(this).load(imagePath)
            .apply(RequestOptions.bitmapTransform(new CircleCrop())
                    .override(iv_photo.getWidth(), iv_photo.getHeight())
                    .error(R.drawable.default_person_icon))
            .into(iv_photo);

}
複製代碼

佈局文件增長ImageView來顯示拍照存儲後的圖片:

<android.support.constraint.ConstraintLayout android:id="@+id/cl_bottom" android:layout_width="match_parent" android:layout_height="80dp" app:layout_constraintBottom_toBottomOf="parent" >

    <!-- 拍照後顯示的圖片-->
    <ImageView android:id="@+id/iv_photo" android:layout_width="40dp" android:layout_height="40dp" android:layout_marginLeft="20dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" />
    <!-- 拍照按鈕-->
    <TextView android:id="@+id/tv_takephoto" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/icon_take_photo_selector" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/>


</android.support.constraint.ConstraintLayout>
複製代碼

效果以下:

效果二
看看拍照後存儲的照片:

存儲的照片路徑

照片信息

存儲後的照片

發現拍照後存儲的照片通過逆時針90度旋轉,須要將順時針90度,緣由在上面分析orientation的時候講述過,雖然調整來預覽圖像角度,可是並不能調整圖片傳感器的圖片方向,因此只能保存圖片後再將圖片旋轉:

/** * 旋轉圖片 * @param cameraId 前置仍是後置 * @param orientation 拍照時傳感器方向 * @param path 圖片路徑 */
private void rotateImageView(int cameraId,int orientation,String path){
    Bitmap bitmap = BitmapFactory.decodeFile(path);
    Matrix matrix = new Matrix();
    //0是後置
    if(cameraId == 0){
        if(orientation == 90){
            matrix.postRotate(90);
        }
    }
    //1是前置
    if(cameraId == 1){
            //順時針旋轉270度 
            matrix.postRotate(270);
    }
    // 建立新的圖片
    Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
            bitmap.getWidth(), bitmap.getHeight(), matrix, true);
    File file = new File(path);
    //從新寫入文件
    try{
        // 寫入文件
        FileOutputStream fos;
        fos = new FileOutputStream(file);
        //默認jpg
        resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
        fos.flush();
        fos.close();
        resizedBitmap.recycle();
    }catch (Exception e){
        e.printStackTrace();
        return;
    }

}
複製代碼

在保存圖像後調用:

/** * * 返回圖片路徑 * @param data */
private void getPhotoPath(final byte[] data) {
    ...
    //將圖片旋轉
    rotateImageView(mCameraId,orientation,Configuration.insidePath + file.getName());
    //將圖片保存到手機相冊
    SystemUtil.saveAlbum(Configuration.insidePath + file.getName(), file.getName(), mAppCompatActivity);
    ...
}
複製代碼

3.10.變換攝像頭

在佈局文件添加TextView做爲先後攝像頭轉換:

<SurfaceView android:id="@+id/sf_camera" android:layout_width="match_parent" android:layout_height="match_parent"/>


    <TextView android:id="@+id/tv_change_camera" android:layout_width="40dp" android:layout_height="40dp" android:layout_marginRight="15dp" android:layout_marginTop="15dp" android:background="@drawable/icon_change_camera_default" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />
複製代碼

CameraPersenter中,添加改變攝像頭方法:

/** * 先後攝像切換 */
public void switchCamera() {
    //先釋放資源
    releaseCamera();
    //在Android P以前 Android設備仍然最多隻有先後兩個攝像頭,在Android p後支持多個攝像頭 用戶想打開哪一個就打開哪一個
    mCameraId = (mCameraId + 1) % Camera.getNumberOfCameras();
    //打開攝像頭
    openCamera(mCameraId);
    //切換攝像頭以後開啓預覽
    startPreview();
}
複製代碼

具體調用:

case R.id.tv_change_camera:
        mCameraPresenter.switchCamera();
        break;
複製代碼

效果以下圖:

先後攝像頭變換
在看看拍照後存儲的照片:

前置攝像頭拍照
這裏能夠發現,在預覽的時候只是順時針調用 setDisplayOrientation()設置預覽方向,並無作鏡面翻轉,爲何切換前置時,預覽效果跟實物同樣呢,原來是在調用 setDisplayOrientation()作了水平鏡面的翻轉,可是拍照後保存下來的照片是沒有水平翻轉的,因此同時要對拍照後的照片作水平方向鏡面翻轉,那就在旋轉圖片裏的方法加上翻轉處理:

/** * 旋轉圖片 * @param cameraId 前置仍是後置 * @param orientation 拍照時傳感器方向 * @param path 圖片路徑 */
private void rotateImageView(int cameraId,int orientation,String path){
    Bitmap bitmap = BitmapFactory.decodeFile(path);
    Matrix matrix = new Matrix();
    // 建立新的圖片
    Bitmap resizedBitmap;
    //0是後置
    if(cameraId == 0){
        if(orientation == 90){
            matrix.postRotate(90);
        }
    }
    //1是前置
    if(cameraId == 1){
        matrix.postRotate(270);
    }
    // 建立新的圖片
    resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
            bitmap.getWidth(), bitmap.getHeight(), matrix, true);

    //新增 若是是前置 須要鏡面翻轉處理
    if(cameraId == 1){
        Matrix matrix1 = new Matrix();
        matrix1.postScale(-1f,1f);
        resizedBitmap = Bitmap.createBitmap(resizedBitmap, 0, 0,
                resizedBitmap.getWidth(), resizedBitmap.getHeight(), matrix1, true);

    }


    File file = new File(path);
    //從新寫入文件
    try{
        // 寫入文件
        FileOutputStream fos;
        fos = new FileOutputStream(file);
        //默認jpg
        resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
        fos.flush();
        fos.close();
        resizedBitmap.recycle();
    }catch (Exception e){
        e.printStackTrace();
        return;
    }

}
複製代碼

這樣就能保證預覽和拍攝後保存的照片和實物同樣了。

3.11.改變焦距

拍照必不可少的一個功能:改變焦距。在Camera中的內部類Camera.ParametersParameters.setZoom(int value)來調整預覽圖像縮放係數,由於在佈局SurfaceView是全屏的,在OnTouch方法作處理,並點擊屏幕進行自動變焦處理:

//默認狀態
private static final int MODE_INIT = 0;
//兩個觸摸點觸摸屏幕狀態
private static final int MODE_ZOOM = 1;
//標識模式
private int mode = MODE_INIT;
...
/** * * 觸摸回調 * @param v 添加Touch事件具體的view * @param event 具體事件 * @return */
@Override
public boolean onTouch(View v, MotionEvent event) {
    //不管多少跟手指加進來,都是MotionEvent.ACTION_DWON MotionEvent.ACTION_POINTER_DOWN
    //MotionEvent.ACTION_MOVE:
    switch (event.getAction() & MotionEvent.ACTION_MASK){
        //手指按下屏幕
        case MotionEvent.ACTION_DOWN:
           mode = MODE_INIT;
           break;
        //當屏幕上已經有觸摸點按下的狀態的時候,再有新的觸摸點被按下時會觸發
        case MotionEvent.ACTION_POINTER_DOWN:
           mode = MODE_ZOOM;
           //計算兩個手指的距離 兩點的距離
           startDis = SystemUtil.twoPointDistance(event);
           break;
        //移動的時候回調
        case MotionEvent.ACTION_MOVE:
            isMove = true;
           //這裏主要判斷有兩個觸摸點的時候才觸發
           if(mode == MODE_ZOOM){
               //只有兩個點同時觸屏才執行
               if(event.getPointerCount() < 2){
                 return true;
               }
               //獲取結束的距離
               float endDis = SystemUtil.twoPointDistance(event);
               //每變化10f zoom變1
               int scale = (int) ((endDis - startDis) / 10f);
               if(scale >= 1 || scale <= -1){
                   int zoom = mCameraPresenter.getZoom() + scale;
                   //判斷zoom是否超出變焦距離
                   if(zoom > mCameraPresenter.getMaxZoom()){
                       zoom = mCameraPresenter.getMaxZoom();
                   }
                   //若是係數小於0
                   if(zoom < 0 ){
                       zoom = 0;
                   }
                   //設置焦距
                   mCameraPresenter.setZoom(zoom);
                   //將最後一次的距離設爲當前距離
                   startDis = endDis;
               }
           }
           break;
        case MotionEvent.ACTION_UP:
            //判斷是否點擊屏幕 若是是自動聚焦
            if(isMove == false){
                //自動聚焦
                mCameraPresenter.autoFoucus();
            }
            isMove = false;
            break;
    }
    return true;
}
複製代碼

CameraPresenter內調用:

/** * 變焦 * @param zoom 縮放係數 */
public void setZoom(int zoom){
   if(mCamera == null){
       return;
   }
   //獲取Paramters對象
   Camera.Parameters parameters;
   parameters = mCamera.getParameters();
   //若是不支持變焦
   if(!parameters.isZoomSupported()){
       return;
   }
   //
   parameters.setZoom(zoom);
   //Camera對象從新設置Paramters對象參數
   mCamera.setParameters(parameters);
   mZoom = zoom;

}

/** * 自動變焦 */
public void autoFoucus(){
    if(mCamera == null){
        mCamera.autoFocus(new Camera.AutoFocusCallback() {
            @Override
            public void onAutoFocus(boolean success, Camera camera) {

            }
        });
    }
}
複製代碼

最終效果以下圖:

加入自動變焦效果圖

3.12.閃光燈設置

經過Parameters.setFlashMode(String value)來控制閃光燈,參數類型有如下:

  • FLASH_MODE_OFF 關閉閃光燈
  • FLASH_MODE_AUTO 在預覽,自動對焦和快照過程當中須要時,閃光燈會自動開啓。
  • FLASH_MODE_ON 不管如何均使用閃光燈
  • FLASH_MODE_RED_EYE 仿紅眼模式,下降紅眼模式
  • FLASH_MODE_TORCH 系統會判斷須要補光而自動決定是否開啓閃光燈,手電筒模式,自動對焦

在平時中,用FLASH_MODE_OFFFLASH_MODE_TORCH就行

/** * * 閃光燈 * @param turnSwitch true 爲開啓 false 爲關閉 */
    public void turnLight(boolean turnSwitch){
        if(mCamera == null){
            return;
        }
        Camera.Parameters parameters = mCamera.getParameters();
        if(parameters == null){
            return;
        }

        parameters.setFlashMode(turnSwitch ? Camera.Parameters.FLASH_MODE_TORCH : Camera.Parameters.FLASH_MODE_OFF);
        mCamera.setParameters(parameters);
    }
複製代碼

具體調用:

@Override
public void onClick(View v) {
    switch (v.getId()){
        //拍照
        case R.id.iv_photo:
            cy_photo.setVisibility(cy_photo.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
            break;
        //改變攝像頭 
        case R.id.tv_change_camera:
            mCameraPresenter.switchCamera();
            break;
        //關閉仍是開啓閃光燈 
        case R.id.tv_flash:
            mCameraPresenter.turnLight(isTurn);
            tv_flash.setBackgroundResource(isTurn ? R.drawable.icon_turnon : R.drawable.icon_turnoff);
            isTurn = !isTurn;
        default:
            break;
    }
}
複製代碼

實際效果:

閃光燈效果

3.13.調整亮度

到這裏能夠發現,相比於調用系統拍照的清晰度,自定義拍照就遜色一籌,感受上面有一層蒙版罩着。調用系統拍照能夠發現,屏幕亮度故意調亮,那麼是否是把自定義拍照的界面亮度調大,效果清晰度會不會好一些呢,下面試試,在CustomCameraActivity加入:

/** * * 加入調整亮度 */
private void getScreenBrightness(){
    WindowManager.LayoutParams lp = getWindow().getAttributes();
    //screenBrightness的值是0.0-1.0 從0到1.0 亮度逐漸增大 若是是-1,那就是跟隨系統亮度 這裏調成 0.78左右
    lp.screenBrightness = Float.valueOf(200) * (1f / 255f);
    getWindow().setAttributes(lp);
}
複製代碼

onCreate調用便可,最後效果以下:

自定義相機效果以下:

調整亮度_自定義
調用系統相機效果以下:

調整亮度_調用系統相機
效果確實比以前好多了。

3.14.視頻錄製

下面簡單實現錄製視頻的功能,利用MediaRecorder來實現直接錄製視頻,這裏要注意:MediaRecorder是不能對每一幀數據作處理的,錄製視頻須要用到如下工具:

  • MediaRecorder:視頻編碼的封裝
  • camera:視頻畫面原屬數據採集
  • SurfaceView:提供預覽畫面
3.14.1.MediaRecorder基本介紹

MediaRecorder是Android中面向應用層的封裝,用於提供音視頻編碼的封裝操做的工具,下面直接上官方圖:

官方MediaRecorder生命週期圖
下面簡單介紹這幾個生命週期的狀態意思:

  • Initial:在MediaRecorder對象被建立時或者調用reset()方法後,會處於該狀態。
  • Initialized:當調用setAudioSource()或者setVideoSource()後就會處於該狀態,這兩個方法主要用於設置音視頻的播放源配置,在該狀態下能夠調用reset()回到Initial狀態。
  • DataSourceConfigured:當調用setOutputFormat方法後,就會處於該狀態,這個方法用來設置文件格式,如設置爲mp4或者mp3,在這個狀態同時能夠設置音視頻的封裝格式,採樣率,視頻碼率,幀率等,能夠經過調用reset()回到Initial狀態。
  • Prepared:當調用上面幾個方法後,就能夠調用prepare()進入這個狀態,只有處於這個狀態才能調用start()方法。
  • Recording:經過調用start()來進入該狀態,處於這個狀態就是真正錄製音視頻,經過調用reset()或者stop()來回到Initial狀態。
  • error:當錄製過程當中發生錯誤,就會進入該狀態,調用reset()回到Initial狀態。
  • release:釋放系統資源,只有在Initial狀態才能調用release()回到該狀態。
3.14.2.調整輸出視頻尺寸的寬高

注意:要添加錄音權限,這裏不在講述。

/** * 獲取輸出視頻的width和height * */
public void getVideoSize(){
    int biggest_width=0 ,biggest_height=0;//最大分辨率
    int fitSize_width=0,fitSize_height=0;
    int fitSize_widthBig=0,fitSize_heightBig=0;
    Camera.Parameters parameters = mCamera.getParameters();
    //獲得系統支持視頻格式
    List<Camera.Size> videoSize = parameters.getSupportedVideoSizes();
    for(int i = 0;i < videoSize.size();i++){
        int w = videoSize.get(i).width;
        int h = videoSize.get(i).height;
        if ((biggest_width == 0 && biggest_height == 0)||
                (w >= biggest_height && h >= biggest_width)) {
            biggest_width = w;
            biggest_height = h;
        }

        if(w == screenHeight && h == screenWidth){
            width = w;
            height = h;
        }else if(w == screenHeight || h == screenWidth){
            if(width == 0 || height == 0){
                fitSize_width = w;
                fitSize_height = h;

            }else if(w < screenHeight || h < screenWidth){
                fitSize_widthBig = w;
                fitSize_heightBig = h;

            }
        }
    }

    if(width == 0 && height == 0){
        width = fitSize_width;
        height = fitSize_height;
    }

    if(width == 0 && height == 0){
        width = fitSize_widthBig;
        height = fitSize_heightBig;
    }

    if(width == 0 && height == 0){
        width = biggest_width;
        height = biggest_height;

    }
}
複製代碼

在初始化相機方法調用,而且建立MediaRecorder對象:

@Override
        public void surfaceCreated(SurfaceHolder holder) {
            //surface建立時執行
            if (mCamera == null) {
                openCamera(mCameraId);
            }
            //並設置預覽
            startPreview();
            //新增獲取系統支持視頻
            getVideoSize();
            mediaRecorder = new MediaRecorder();
        }
複製代碼
3.14.3.設置MediaRecorder參數
//解鎖Camera硬件
    mCamera.unlock();
    mediaRecorder.setCamera(mCamera);
    //音頻源 麥克風
    mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
    //視頻源 camera
    mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
    //輸出格式
    mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
    //音頻編碼
    mediaRecorder.setAudioEncoder(MediaRecorder.VideoEncoder.DEFAULT);
    //視頻編碼
    mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
    //設置幀頻率
    mediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024 * 100);
    Log.d("sssd視頻寬高:","寬"+width+"高"+height+"");
    mediaRecorder.setVideoSize(width,height);
    //每秒的幀數
    mediaRecorder.setVideoFrameRate(24);
複製代碼
3.14.4.調整保存視頻角度

若是不設置調整保存視頻的角度,用後置錄製視頻會逆時針翻轉90度,因此須要設置輸出順時針旋轉90度:

//調整視頻旋轉角度 若是不設置 後置和前置都會被旋轉播放
    if(mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        if(orientation == 270 || orientation == 90 || orientation == 180){
            mediaRecorder.setOrientationHint(180);
        }else{
            mediaRecorder.setOrientationHint(0);
        }
    }else{
        if(orientation == 90){
            mediaRecorder.setOrientationHint(90);
        }
    }
複製代碼

整個錄製方法以下:

/** * * 錄製方法 */
public void startRecord(String path,String name){
    //解鎖Camera硬件
    mCamera.unlock();
    mediaRecorder.setCamera(mCamera);
    //音頻源 麥克風
    mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
    //視頻源 camera
    mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
    //輸出格式
    mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
    //音頻編碼
    mediaRecorder.setAudioEncoder(MediaRecorder.VideoEncoder.DEFAULT);
    //視頻編碼
    mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
    //設置幀頻率
    mediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024 * 100);
    Log.d("sssd視頻寬高:","寬"+width+"高"+height+"");
    mediaRecorder.setVideoSize(width,height);
    //每秒的幀數
    mediaRecorder.setVideoFrameRate(24);
    //調整視頻旋轉角度 若是不設置 後置和前置都會被旋轉播放
    if(mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        if(orientation == 270 || orientation == 90 || orientation == 180){
            mediaRecorder.setOrientationHint(180);
        }else{
            mediaRecorder.setOrientationHint(0);
        }
    }else{
        if(orientation == 90){
            mediaRecorder.setOrientationHint(90);
        }
    }

    File file = new File(path);
    if(!file.exists()){
        file.mkdirs();
    }
    //設置輸出文件名字
    mediaRecorder.setOutputFile(path + File.separator + name + "mp4");
    File file1 = new File(path + File.separator + name + "mp4");
    if(file1.exists()){
        file1.delete();
    }
    //設置預覽
    mediaRecorder.setPreviewDisplay(mSurfaceView.getHolder().getSurface());
    try {
        //準備錄製
        mediaRecorder.prepare();
        //開始錄製
        mediaRecorder.start();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
複製代碼
3.14.5.中止錄製

當中止錄製後須要把MediaRecorder釋放,而且從新調用預覽方法:

/** * * 中止錄製 */
public void stopRecord(){
    if(mediaRecorder != null){
        mediaRecorder.release();
        mediaRecorder = null;
    }

    if(mCamera != null){
        mCamera.release();
    }
    openCamera(mCameraId);
    //並設置預覽
    startPreview();
}
複製代碼
3.14.6.具體調用
mCameraPresenter.startRecord(Configuration.OUTPATH,"video");
複製代碼
3.14.7.視頻播放

當錄製完須要播放,用新的界面來,用SurfaceView+MediaPlayer來實現:

public class PlayAudioActivity extends AppCompatActivity implements MediaPlayer.OnCompletionListener,MediaPlayer.OnPreparedListener{
    private SurfaceView sf_play;
    private MediaPlayer player;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_playaudio);
        sf_play = findViewById(R.id.sf_play);
        //下面開始實例化MediaPlayer對象
        player = new MediaPlayer();
        player.setOnCompletionListener(this);
        player.setOnPreparedListener(this);
        //設置數據數據源,也就播放文件地址,能夠是網絡地址
        String dataPath = Configuration.OUTPATH + "/videomp4";
        try {
            player.setDataSource(dataPath);
        } catch (Exception e) {
            e.printStackTrace();
        }

        sf_play.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                //將播放器和SurfaceView關聯起來
                player.setDisplay(holder);
                //異步緩衝當前視頻文件,也有一個同步接口
                player.prepareAsync();
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }
            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });


    }
    /** * * 設置循環播放 * @param mp */
    @Override
    public void onCompletion(MediaPlayer mp) {
        player.start();
        player.setLooping(true);
    }

    /** * 這邊播放 * @param mp */
    @Override
    public void onPrepared(MediaPlayer mp) {
        player.start();
    }

    /** * 釋放資源 * */
    @Override
    protected void onDestroy(){
        super.onDestroy();
        if(player != null){
            player.reset();
            player.release();
            player = null;

        }
    }
}
複製代碼

實際效果:

最終效果
視頻存放路徑信息:

視頻存放信息

3.15.人臉檢測

下面實現人臉檢測,注意是人臉檢測不是人臉識別,步驟以下:

  • 在相機預覽後,調用startFaceDetection方法開啓人臉檢測
  • 調用setFaceDetectionListener(FaceDetectionListener listener)設置人臉檢測回調
  • 自定義View,用來繪製人臉大體區域
  • 在人臉回調中,所獲取的人臉信息傳遞給自定義View,自定義View根據人臉信息繪製大體區域
3.15.1.開啓人臉檢測

在相機調用開啓預覽後才能調用:

/** * 開始預覽 */
private void startPreview() {
    try {
        //根據所傳入的SurfaceHolder對象來設置實時預覽
        mCamera.setPreviewDisplay(mSurfaceHolder);
        //調整預覽角度
        setCameraDisplayOrientation(mAppCompatActivity,mCameraId,mCamera);
        mCamera.startPreview();
        //開啓人臉檢測
        startFaceDetect();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
複製代碼
3.15.2.設置人臉檢測回調
/** * 人臉檢測 */
private void startFaceDetect() {
    //開始人臉檢測,這個要調用startPreview以後調用
    mCamera.startFaceDetection();
    //添加回調
    mCamera.setFaceDetectionListener(new Camera.FaceDetectionListener() {
        @Override
        public void onFaceDetection(Camera.Face[] faces, Camera camera) {
      // mCameraCallBack.onFaceDetect(transForm(faces), camera);
            mFaceView.setFace(transForm(faces));
            Log.d("sssd", "檢測到" + faces.length + "人臉");
            for(int i = 0;i < faces.length;i++){
                Log.d("第"+(i+1)+"張人臉","分數"+faces[i].score+"左眼"+faces[i].leftEye+"右眼"+faces[i].rightEye+"嘴巴"+faces[i].mouth);
            }
        }
    });
}
複製代碼

Face源碼中,能夠看到這麼一段描述:

Bounds of the face. (-1000, -1000) represents the top-left of the
      camera field of view, and (1000, 1000) represents the bottom-right of
      the field of view. For example, suppose the size of the viewfinder UI
      is 800x480. The rect passed from the driver is (-1000, -1000, 0, 0).
      The corresponding viewfinder rect should be (0, 0, 400, 240). It is
      guaranteed left < right and top < bottom. The coordinates can be
      smaller than -1000 or bigger than 1000. But at least one vertex will
      be within (-1000, -1000) and (1000, 1000).
     
      <p>The direction is relative to the sensor orientation, that is, what
      the sensor sees. The direction is not affected by the rotation or
      mirroring of {@link #setDisplayOrientation(int)}. The face bounding
      rectangle does not provide any information about face orientation.</p>
     
      <p>Here is the matrix to convert driver coordinates to View coordinates
      in pixels.</p>
      <pre>
      Matrix matrix = new Matrix();
      CameraInfo info = CameraHolder.instance().getCameraInfo()[cameraId];
      // Need mirror for front camera.
      boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT);
      matrix.setScale(mirror ? -1 : 1, 1);
      // This is the value for android.hardware.Camera.setDisplayOrientation.
      matrix.postRotate(displayOrientation);
      // Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
      // UI coordinates range from (0, 0) to (width, height).
      matrix.postScale(view.getWidth() / 2000f, view.getHeight() / 2000f);
      matrix.postTranslate(view.getWidth() / 2f, view.getHeight() / 2f);
      </pre>
     
      @see #startFaceDetection()
複製代碼

具體意思是在人臉使用的座標和安卓屏幕座標是不同的,而且舉了一個例子:若是屏幕尺寸是800*480,如今有一個矩形位置在人臉座標系中位置是(-1000,-1000,0,0),那麼在安卓屏幕座標的位置是(0,0,400,240)。

而且給了轉換座標的具體方法:

/** * 將相機中用於表示人臉矩形的座標轉換成UI頁面的座標 * * @param faces 人臉數組 * @return */
private ArrayList<RectF> transForm(Camera.Face[] faces) {
    Matrix matrix = new Matrix();
    boolean mirror;
    if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        mirror = true;
    } else {
        mirror = false;
    }
    //前置須要鏡像
    if (mirror) {
        matrix.setScale(-1f, 1f);
    } else {
        matrix.setScale(1f, 1f);
    }
    //後乘旋轉角度
    matrix.postRotate(Float.valueOf(orientation));
    //後乘縮放
    matrix.postScale(mSurfaceView.getWidth() / 2000f,mSurfaceView.getHeight() / 2000f);
    //再進行位移
    matrix.postTranslate(mSurfaceView.getWidth() / 2f, mSurfaceView.getHeight() / 2f);
    ArrayList<RectF> arrayList = new ArrayList<>();
    for (Camera.Face rectF : faces) {
        RectF srcRect = new RectF(rectF.rect);
        RectF dstRect = new RectF(0f, 0f, 0f, 0f);
        //經過Matrix映射 將srcRect放入dstRect中
        matrix.mapRect(dstRect, srcRect);
        arrayList.add(dstRect);
    }
    return arrayList;

}
複製代碼
3.15.3.實現自定義View
package com.knight.cameraone.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import java.util.ArrayList;

/** * @author created by knight * @organize * @Date 2019/10/11 13:54 * @descript:人臉框 */

public class FaceDeteView extends View {

    private Paint mPaint;
    private String mColor = "#42ed45";
    private ArrayList<RectF> mFaces = null;
    public FaceDeteView(Context context) {
        super(context);
        init(context);
    }

    public FaceDeteView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public FaceDeteView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }



    private void init(Context context){
        mPaint = new Paint();
        //畫筆顏色
        mPaint.setColor(Color.parseColor(mColor));
        //只繪製圖形輪廓
        mPaint.setStyle(Paint.Style.STROKE);
        //設置粗細
        mPaint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1f,context.getResources().getDisplayMetrics()));
        //設置抗鋸齒
        mPaint.setAntiAlias(true);
    }


    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        if(mFaces != null){
            for(RectF face:mFaces){
                canvas.drawRect(face,mPaint);
            }

        }
    }


    /** * 設置人人臉信息 */
    public void setFace(ArrayList<RectF> mFaces){
       this.mFaces = mFaces;
       //重繪矩形框
       invalidate();
    }

}

複製代碼

佈局文件:

<SurfaceView android:id="@+id/sf_camera" android:layout_width="match_parent" android:layout_height="match_parent"/>
    <!-- 新增 -->
    <com.knight.cameraone.view.FaceDeteView android:id="@+id/faceView" android:layout_width="match_parent" android:layout_height="match_parent"/>
複製代碼

並增長人臉檢測開關:

/** * 開啓人臉檢測 * */
    public void turnFaceDetect(boolean isDetect){
         mFaceView.setVisibility(isDetect ?  View.VISIBLE : View.GONE);
    }
複製代碼

這裏只是將自定義View不顯示,具體效果圖以下:

人臉檢測效果圖
查看具體打印數據:

人臉檢測數據
能夠發如今 vivo安卓7.1.1版本下,眼睛,嘴巴數據是獲取不到的。

5、知識點梳理

到這裏自定義相機Camera1步驟再次梳理以下:

  • 1.建立輸出圖片/視頻目錄
  • 2.建立佈局文件,將預覽界面和用戶界面綁定(通常用SurfaceView/TextureView),進行實時顯示相機預覽圖像
  • 3.經過SurfaceView獲取的SurfaceHolder設置SurfaceHolder.Callback監聽,實現surfaceCreatedsurfaceChangedsurfaceDestroyed方法/若是是TextureView的話就設置setSurfaceTextureListener監聽並實現onSurfaceTextureAvailableonSurfaceTextureSizeChangedonSurfaceTextureDestroyedonSurfaceTextureUpdated方法
  • 4.在SurfaceView->surfaceCreated/TextureView->onSurfaceTextureAvailable方法經過Camera.open(int cameraId)打開相機
  • 5.經過Camera.getParameters()獲取Parameters對象而且設置具體參數
  • 6.將Parameters對象經過Camera.setParameters(Parameters parames)設置進Camera中
  • 7.設置預覽圖片尺寸和保存圖片尺寸
  • 8.設置預覽回調和實時預覽
  • 9.調整預覽角度
  • 10.經過Camera.startPreview()開啓預覽
  • 11.拍照時實現takePicture(ShutterCallback shutter, PictureCallback raw,PictureCallback jpeg),在回調的onPictureTaken(byte[] data, Camera camera)返回的字節數組裏保存圖片,保存圖片按需是否須要旋轉設置
  • 12.在SurfaceView->surfaceDestroyed/TextureView->onSurfaceTextureUpdated進行資源釋放

6、參考資料

7、本文例子

相關文章
相關標籤/搜索