小日子~~Android 自定義拍照圖片處理(一)

說到Android 自定義拍照嗅钻,很多人會想到調(diào)用系統(tǒng)相機(jī)唄~~使用系統(tǒng)相冊唄。
首先整體視覺不和諧店展,其次UED也會質(zhì)疑你的能力养篓,尷尬,要做咱就來一個實(shí)戰(zhàn)的貢獻(xiàn)給伙伴們赂蕴。我會把實(shí)際開發(fā)過程中遇到的問題列出一一解答柳弄。

First, photo image !

首先要考慮哪幾點(diǎn),Let's see:

  • 1睡腿、擁有一個實(shí)時獲取鏡頭拍攝圖像的界面
  • 2语御、擁有一個可以觸發(fā)獲取瞬時圖像的能力

以下內(nèi)容感謝:https://juejin.im/post/5d6d1155e51d4561ea1a94a4
收集了很多大佬們的心血,這里表示感謝席怪。

1应闯、擁有一個實(shí)時獲取鏡頭拍攝圖像的界面:

1、創(chuàng)建預(yù)覽界面挂捻,一般是繼承SurfaceView并且實(shí)現(xiàn)SurfaceHolder接口的拍攝預(yù)覽類碉纺,并且創(chuàng)建布局文件,將預(yù)覽界面和用戶界面綁定刻撒,進(jìn)行實(shí)時顯示相機(jī)預(yù)覽圖像
2骨田、創(chuàng)建拍照監(jiān)聽器來響應(yīng)用戶的不同操作,如開始拍照声怔,停止拍照等
3态贤、拍照成功后按照產(chǎn)品設(shè)計(jì)可保存文件,也可將拍攝獲得的圖像直接深度處理上傳等醋火。
4悠汽、釋放相機(jī)資源,當(dāng)相機(jī)不再使用時芥驳,進(jìn)行釋放

1.Surface

Surface根據(jù)英文直譯是表面的意思柿冲,在源碼中有這樣描述的:

/**
 * 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>
 */
復(fù)制代碼

上面的意思:Surface是用來處理屏幕顯示內(nèi)容合成器所管理的原始緩存區(qū)工具,它通常由圖像緩沖區(qū)的消費(fèi)者來創(chuàng)建如(SurfaceTexture兆旬,MediaRecorder)假抄,然后被移交給生產(chǎn)者(如:MediaPlayer)或者顯示到其上(如:CameraDevice),從上面可以得知:

  • Surface通常由SurfaceTexture或者M(jìn)ediaRecorder來創(chuàng)建
  • Surface最后顯示在MediaPlayer或者CameraDevice上
  • 通過Surface就可以獲得管理原始緩存區(qū)的數(shù)據(jù)
  • 原始緩沖區(qū)(rawbuffer)是用來保存當(dāng)前窗口的像素?cái)?shù)據(jù)

Surface內(nèi)有一個Canvas成員:

    private final Canvas mCanvas = new CompatibleCanvas();
復(fù)制代碼

我們知道,畫圖都是在Canvas對象上來畫的宿饱,因?yàn)?code>Suface持有Canvas熏瞄,那么我們可以這樣認(rèn)為,Surface是一個句柄谬以,而Canvas是開發(fā)者畫圖的場所巴刻,就像黑板,而原生緩沖器(rawbuffer)是用來保存數(shù)據(jù)的地方蛉签,所有得到Surface就能得到其中的Canvas和原生緩沖器等其他內(nèi)容胡陪。

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
 */
復(fù)制代碼

意思就是SurfaceView提供了嵌入視圖層級中的專用surface碍舍,你可以控制surface的格式或者大心(通過SurfaceView就可以看到Surface部分或者全部內(nèi)容),SurfaceView負(fù)責(zé)把surface顯示在屏幕的正確位置片橡。

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

SurfaceView繼承自View妈经,并且其中有兩個成員變量,一個是Surface對象捧书,一個是SurfaceHolder對象吹泡,SurfaceViewSurface顯示在屏幕上,SurfaceView通過SurfaceHolder得知Surface的狀態(tài)(創(chuàng)建经瓷、變化爆哑、銷毀),可以通過getHolder()方法獲得當(dāng)前SurfaceViewSurfaceHolder對象舆吮,然后就可以對SurfaceHolder對象添加回調(diào)來監(jiān)聽Surface的狀態(tài)揭朝。

Surface是從Object派生而來,實(shí)現(xiàn)了Parcelable接口色冀,看到Parcelable很容易讓人想到數(shù)據(jù)潭袱,而SurfaceView就是用來展示Surface數(shù)據(jù)的,兩者的關(guān)系可以用下面一張圖來描述:

image.png

Surface是通過SurfaceView才能展示其中內(nèi)容锋恬。

到這里也許大家會有一個疑問屯换,SurfaceView和普通的View有什么區(qū)別?相機(jī)開發(fā)就一定要用SurfaceView嗎与学?

首先普通的View和其派生類都是共享同一個Surface彤悔,所有的繪制必須在主線程(UI線程)進(jìn)行,通過Surface獲得對應(yīng)的Canvas癣防,完成繪制View的工作蜗巧。

SurfaceView是特殊的View掌眠,它不與其他普通的view共享Surface蕾盯,在自己內(nèi)部持有Surface可以在獨(dú)立的線程中進(jìn)行繪制,在自定義相機(jī)預(yù)覽圖像這塊,更新速度比較快和幀率要求比較高级遭,如果用普通的View去更新望拖,極大可能會阻塞UI線程,SurfaceView是在一個新起的線程去更新畫面并不會阻塞UI線程挫鸽。還有SurfaceView底層實(shí)現(xiàn)了雙緩沖機(jī)制说敏,雙緩沖技術(shù)主要是為了解決反復(fù)局部刷新帶來的閃爍問題,對于像游戲丢郊,視頻這些畫面變化特別頻繁盔沫,如果前面沒有顯示完,程序又重新繪制枫匾,這樣會導(dǎo)致屏幕不停得閃爍架诞,而雙緩沖及時會把要處理的圖片在內(nèi)存中處理后,把要畫的東西先畫到一個內(nèi)存區(qū)域里干茉,然后整體一次行畫處理谴忧,顯示在屏幕上。舉例說明: 在Android中角虫,如果自定義View大多數(shù)都會重寫onDraw方法沾谓,onDraw方法并不是繪制一點(diǎn)顯示一點(diǎn),而是繪制完成后一次性顯示到屏幕上戳鹅。因?yàn)镃PU訪問內(nèi)存的速度遠(yuǎn)遠(yuǎn)大于訪問屏幕的速度均驶,如果需要繪制大量復(fù)雜的圖形時,每次都一個個從內(nèi)存讀取圖形然后繪制到屏幕就會造成多次訪問屏幕枫虏,這些效率會很低辣恋。為了解決這個問題,我們可以創(chuàng)建一個臨時的Canvas對象模软,將圖像都繪制到這個臨時的Canvas對象中伟骨,繪制完成后通過drawBitmap方法繪制到onDraw方法中的Canvas對象中,這樣就相對于Bitmap的拷貝過程燃异,比直接繪制效率要高携狭。

所以相機(jī)開發(fā)中最適合用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);
        ...
    }
 }
復(fù)制代碼

這是一個抽象的接口給持有surface對象使用回俐,允許你控制surface的大小和格式逛腿,編輯surface中的像素和監(jiān)聽surface的變化,這個接口通常通過SurfaceView這個類來獲得仅颇。

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

  • public void surfaceCreated(SurfaceHolder holder)

    surface第一次創(chuàng)建回調(diào)

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

    surface變化的時候會回調(diào)

  • public void surfaceDestroyed(SurfaceHolder holder)

    surface銷毀的時候回調(diào)

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

  • public void addCallback(Callback callback)

    為SurfaceHolder添加回調(diào)接口

  • public void removeCallback(Callback callback)

    對SurfaceHolder移除回調(diào)接口

  • public Canvas lockCanvas()

    獲取Canvas對象并且對它上鎖

  • public Canvas lockCanvas(Rect dirty)

    獲取一個Canvas對象忘瓦,并且對它上鎖搁廓,但是所動的內(nèi)容是dirty所指定的矩形區(qū)域

  • public void unlockCanvasAndPost(Canvas canvas)

    當(dāng)修改Surface中的數(shù)據(jù)完成后,釋放同步鎖,并且提交改變境蜕,將新的數(shù)據(jù)進(jìn)行展示蝙场,同時Surface中的數(shù)據(jù)會丟失,加鎖的目的就是為了在繪制的過程中粱年,Surface數(shù)據(jù)不會被改變售滤。

  • public void setType(int type)

    設(shè)置Surface的類型,類型有以下幾種:

    SURFACE_TYPE_NORMAL:用RAM緩存原生數(shù)據(jù)的普通Surface

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

    SURFACE_TYPE_GPU:適用于GPU加速的Surface

    SURFACE_TYPE_PUSH_BUFFERS:表明該Surface不包含原生數(shù)據(jù)台诗,Surface用到的數(shù)據(jù)由其他對象提供完箩,在Camera圖像預(yù)覽中就使用該類型的Surface,有Camera負(fù)責(zé)提供給預(yù)覽Surface數(shù)據(jù)拉队,這樣圖像預(yù)覽會比較流暢嗜憔,如果設(shè)置這種類型就不能調(diào)用lockCanvas來獲取Canvas對象。

    到這里氏仗,會發(fā)現(xiàn)Surface吉捶、SurfaceViewSurfaceHolder就是典型的MVC模型。

    • Surface:原始數(shù)據(jù)緩沖區(qū)皆尔,MVC中的M
    • SurfaceView:用來繪制Surface的數(shù)據(jù)呐舔,MVC中的V
    • SurfaceHolder:控制Surface尺寸格式,并且監(jiān)聽Surface的更改慷蠕,MVC中的C

上面三者的關(guān)系可以用下面一張圖來表示:


image.png

2珊拼、擁有一個可以觸發(fā)獲取瞬時圖像的能力

Camera

查看源碼時,發(fā)現(xiàn)android.hardware.cameragoogle不推薦使用了:

image.png

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

image.png

1.CameraInfo

Camera類里流炕,CameraInfo是靜態(tài)內(nèi)部類:

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

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

        /**
         * The direction that the camera faces. It should be
         * CAMERA_FACING_BACK or CAMERA_FACING_FRONT.
         * 攝像機(jī)面對的方向每辟,它只能是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是相機(jī)收集圖片的角度剑辫,這個值是相機(jī)采集的圖片需要順時針旋轉(zhuǎn)才能正確顯示自
         * 然方向的圖像,它必須是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>
         * 舉個例子:假設(shè)現(xiàn)在豎著拿著手機(jī),后面攝像頭傳感器是橫向(水平方向)的挠将,你現(xiàn)在正在看屏幕
         * 如果攝像機(jī)傳感器的頂部在自然方向上右邊胳岂,那么這個值是90度(手機(jī)是豎屏,傳感器是橫屏的)* 
         * 如果前置攝像頭的傳感器頂部在手機(jī)屏幕的右邊舔稀,那么這個值就是270度乳丰,也就是說這個值是相機(jī)圖像順時針
         * 旋轉(zhuǎn)到設(shè)備自然方向一致時的角度。
         *
         */
        public int orientation;

        /**
         * <p>Whether the shutter sound can be disabled.</p>
         * 是否禁用開門聲音
         */
        public boolean canDisableShutterSound;
    };
復(fù)制代碼

1.1.orientation

可能很多人對上面orientation解釋有點(diǎn)懵内贮,這里重點(diǎn)講一下orientation产园,首先先知道四個方向:屏幕坐標(biāo)方向汞斧,自然方向圖像傳感器方向淆两,相機(jī)預(yù)覽方向

1.1.1.屏幕坐標(biāo)方向
image.png

在Android系統(tǒng)中拂酣,以屏幕左上角為坐標(biāo)系統(tǒng)的原點(diǎn)(0,0)坐標(biāo)秋冰,向右延伸是X軸的正方向,向下延伸是y軸的正方向婶熬,如上圖所示剑勾。

1.1.2.自然方向

每個設(shè)備都有一個自然方向,手機(jī)和平板自然方向不一樣赵颅,在Android應(yīng)用程序中虽另,android:screenOrientation來控制activity啟動時的方向,默認(rèn)值unspecified即為自然方向饺谬,當(dāng)然可以取值為:

  • unspecified捂刺,默認(rèn)值,自然方向
  • landscape募寨,強(qiáng)制橫屏顯示族展,正常拿設(shè)備的時候,寬比高長拔鹰,這是平板的自然方向
  • portrait仪缸,正常拿著設(shè)備的時候,寬比高短列肢,這是手機(jī)的自然方向
  • behind:和前一個Activity方向相同
  • sensor:根據(jù)物理傳感器方向轉(zhuǎn)動恰画,用戶90度,180度瓷马,270度旋轉(zhuǎn)手機(jī)方向
  • sensorLandScape:橫屏選擇拴还,一般橫屏游戲會這樣設(shè)置
  • sensorPortait:豎屏旋轉(zhuǎn)
  • nosensor:旋轉(zhuǎn)設(shè)備的時候,界面不會跟著旋轉(zhuǎn)欧聘,初始化界面方向由系統(tǒng)控制
  • user:用戶當(dāng)前設(shè)置的方向

默認(rèn)的話:平板的自然方向是橫屏自沧,而手機(jī)的自然方向是豎屏方向。

1.1.3.圖像傳感器方向

手機(jī)相機(jī)的圖像數(shù)據(jù)都是來自于攝像頭硬件的圖像傳感器树瞭,這個傳感器在被固定到手機(jī)上后有一個默認(rèn)的取景方向拇厢,方向一般是和手機(jī)橫屏方向一致,如下圖:


image.png

和豎屏應(yīng)用方向呈90度晒喷。

1.1.4.相機(jī)預(yù)覽方向

將圖像傳感器捕獲的圖像孝偎,顯示在屏幕上的方向。在默認(rèn)情況下凉敲,和圖像傳感器方向一致衣盾,在相機(jī)API中可以通過setDisplayOrientation(int degrees)設(shè)置預(yù)覽方向(順時針設(shè)置寺旺,不是逆時針)。默認(rèn)情況下势决,這個值是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.
     *
     * 設(shè)置預(yù)覽顯示的順時針旋轉(zhuǎn)角度,會影響預(yù)覽幀和拍拍照后顯示的圖片果复,這個方法對豎屏模式的應(yīng)用 * 很有用塌计,前置攝像頭進(jìn)行角度旋轉(zhuǎn)之前琴昆,圖像會進(jìn)行一個水平的鏡像翻轉(zhuǎn),用戶在看預(yù)覽圖像的時候* 就像鏡子一樣了,這個不影響PreviewCallback的回調(diào)府寒,生成JPEG圖片和錄像文件的方向茵乱。
     *
     */
復(fù)制代碼
1.1.4.1.后置

注意睁枕,對于手機(jī)來說:

  • 橫屏下:因?yàn)槠聊环较蚝拖鄼C(jī)預(yù)覽方向一致挎塌,所以預(yù)覽圖像和看到的實(shí)物方向一致
  • 豎屏下:屏幕方向和預(yù)覽方向垂直,會造成旋轉(zhuǎn)90度現(xiàn)象车酣,無論怎么旋轉(zhuǎn)手機(jī)曲稼,UI預(yù)覽界面和實(shí)物始終是90度,為了得到一致的預(yù)覽界面需要將相機(jī)預(yù)覽方向旋轉(zhuǎn)90度(setDisplayOrientation(90)),這樣預(yù)覽界面和實(shí)物方向一致湖员。

下面舉個簡單例子:

image.png

這里重點(diǎn)講解一下豎屏下:

image.png

image.png

需要結(jié)合上下兩張圖來看:

  • 當(dāng)圖像傳感器獲得圖像后躯肌,就會知道這幅圖像每個坐標(biāo)的像素值,但是要顯示到屏幕上就要根據(jù)屏幕自然方向的坐標(biāo)來顯示(豎屏下屏幕自然方向坐標(biāo)系和后置相機(jī)圖像傳感器方向呈90度)破衔,所以圖像會逆時針旋轉(zhuǎn)旋轉(zhuǎn)90度清女,顯示到屏幕坐標(biāo)系上。
  • 那么收集的圖像時逆時針旋轉(zhuǎn)了90度晰筛,那么這時候需要順時針旋轉(zhuǎn)90度才能和收集的自然方向保持一致嫡丙,也就是和實(shí)物圖方向一樣。
1.1.4.2.前置

Android中读第,對于前置攝像頭曙博,有以下規(guī)定:

  • 在預(yù)覽圖像是真實(shí)物體的鏡像
  • 拍出的照片和真實(shí)場景一樣
image.png

同理這里重點(diǎn)講一下,前置豎屏

image.png
image.png
image.png
image.png

在前置相機(jī)中怜瞒,預(yù)覽圖像相機(jī)收集圖像是鏡像關(guān)系父泳,上面圖中Android圖標(biāo)中前置收集圖像預(yù)覽圖像時相反的,前置相機(jī)圖像傳感器方向和前置相機(jī)預(yù)覽圖像方向是左右相反的吴汪,上圖也有體現(xiàn)惠窄。

  • 前置攝像頭收集到圖像后(沒有經(jīng)過鏡像處理),但是要顯示到屏幕上漾橙,就要按照屏幕自然方向的坐標(biāo)系來進(jìn)行顯示杆融,需要順時針旋轉(zhuǎn)270度(API沒有提供逆時針90度的方法),才能和手機(jī)自然方向一致霜运。
  • 在預(yù)覽的時候脾歇,做了鏡像處理蒋腮,所以只需要順時針旋轉(zhuǎn)90度,就能和自然方向一致藕各,因?yàn)閿z像圖像沒有做水平翻轉(zhuǎn)池摧,所以前置攝像頭拍出來的圖片,你會發(fā)現(xiàn)跟預(yù)覽的時候是左右翻轉(zhuǎn)的激况,自己可以根據(jù)需求做處理作彤。 上面把角度知識梳理了,后面會通過代碼一步一步驗(yàn)證誉碴,下面按照最開始的思維導(dǎo)圖繼續(xù)看Camera內(nèi)的方法:

1.2.facing

facing代表相機(jī)方向宦棺,可取值有二:

  • CAMREA_FACING_BACK瓣距,值為0黔帕,表示是后置攝像頭
  • CAMERA_FACING_FRONT,值為1蹈丸,表示是前置攝像頭

1.3.canDisableShutterSound

是否禁用快門聲音

2.PreviewCallback

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

PreviewCallback是一個接口成黄,可以給Camera設(shè)置Camrea.PreviewCallback,并且實(shí)現(xiàn)這個onPreviewFrame(byte[] data, Camera camera)這個方法逻杖,就可以去Camera預(yù)覽圖片時的數(shù)據(jù)奋岁,如果設(shè)置Camera.setPreviewCallback(callback)onPreviewFrame這個方法會被一直調(diào)用荸百,可以在攝像頭對焦成功后設(shè)置camera.setOneShotPreviewCallback(previewCallback)闻伶,這樣設(shè)置onPreviewFrame這個方法就會被調(diào)用異常,處理data數(shù)據(jù)够话,data是相機(jī)預(yù)覽到的原始數(shù)據(jù)蓝翰,可以保存下來當(dāng)做一張照片。

3.AutoFocusCallback

3.1.onAutoFocus(boolean success,Camera camera)

AutoFocusCallback是一個接口女嘲,用于在相機(jī)自動對焦完成后時通知回調(diào)畜份,第一個參數(shù)是相機(jī)是否自動對焦成功,第二個參數(shù)是相機(jī)對象欣尼。

4.Face

作為靜態(tài)內(nèi)部類爆雹,用來描述通過相機(jī)人臉檢測識別的人臉信息。

4.1.rect

Rect對象愕鼓,表示檢測到人臉的區(qū)域钙态,這個Rect對象中的坐標(biāo)并不是安卓屏幕中的坐標(biāo),需要進(jìn)行轉(zhuǎn)換才能使用菇晃。

4.2.score

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

4.3.leftEye

是一個Point對象谋旦,表示的是檢測到左眼的位置坐標(biāo)

4.4.rightEye

是一個Point對象剩失,表示的是檢測到右眼的位置坐標(biāo)

4.5.mouth

同時一個Point對象屈尼,表示的是檢測到嘴的位置坐標(biāo) leftEyerightEye拴孤,mouth有可能獲得不到脾歧,并不是所有相機(jī)支持,不支持情況下演熟,獲取為空鞭执。

5.Size

代表拍照圖片的大小。

5.1.width

拍照圖片的寬

5.2.height

拍照圖片的高

6.FaceDetectionListener

這是一個接口芒粹,當(dāng)開始預(yù)覽(人臉識別)的時候開始回調(diào)

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

通知監(jiān)聽器預(yù)覽幀檢測到的人臉兄纺,Face[]是一個數(shù)組,用來存放檢測的人臉(存放多張人臉)化漆,第二個參數(shù)是識別人臉的相機(jī)估脆。

7.Parameters

Camera作為內(nèi)部類存在,是相機(jī)配置設(shè)置類座云,不同設(shè)備可能具有不同的照相機(jī)功能疙赠,如圖片大小或者閃光模式。

7.1.setPreviewSize(int width,int height)

設(shè)置預(yù)覽相機(jī)圖片的大小朦拖,width是圖片的寬圃阳,height是圖片的高

7.2.setPreviewFormat(int pixel_format)

設(shè)置預(yù)覽圖片的格式,有以下格式:

  • ImageFormat.NV16
  • ImageFormat.NV21
  • ImageFormat.YUY2
  • ImageFormat.YV12
  • ImgaeFormat.RGB_565
  • ImageFormat.JPEG 如果不設(shè)置返回的數(shù)據(jù)璧帝,會默認(rèn)返回NV21編碼數(shù)據(jù)捍岳。

7.3.setPictureSize(int width,int height)

設(shè)置保存圖片的大小,width圖片的寬度睬隶,以像素為單位锣夹,height是圖片的高度,以像素為單位理疙。

7.4.setPictureFormat(int pixel_format)

設(shè)置保存圖片的格式晕城,取值和setPreviewFormat格式一樣。

7.5.setRotation(int degree)

上面已經(jīng)講過窖贤,設(shè)置相機(jī)采集照片的角度砖顷,這個值是相機(jī)所采集的圖片需要順時針選擇到自然方向的角度值,它必須是0赃梧,90滤蝠,180或者270中的一個。

7.6.setFocusMode(String value)

設(shè)置相機(jī)對焦模式授嘀,對焦模式有以下:

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

7.7.setZoom(int value)

設(shè)置縮放系數(shù)物咳,也就是平常所說的變焦。

7.8.getSupportedPreviewSizes()

返回相機(jī)支持的預(yù)覽圖片大小蹄皱,返回值是一個List<Size>數(shù)組览闰,至少有一個元素芯肤。

7.9.getSupportedVideoSizes()

返回獲取相機(jī)支持的視頻幀大小,可以通過MediaRecorder來使用压鉴。

7.10.getSupportedPreviewFormats()

返回相機(jī)支持的圖片預(yù)覽格式崖咨,所有相機(jī)都支持ImageFormat.NV21,返回是集合類型并且返回至少包含一個元素油吭。

7.11.getSupportedPictureSize()

以集合的形式返回相機(jī)支持采集的圖片大小击蹲,至少返回一個元素。

7.12.getSupportedPictureFormats()

以集合的形式返回相機(jī)支持的圖片(拍照后)格式婉宰,至少返回一個元素歌豺。

7.13.getSupportedFocusModes()

以集合的形式返回相機(jī)支持的對焦模式,至少返回一個元素心包。

7.14.getMaxNumDetectedFaces()

返回相機(jī)所支持的最多人臉檢測數(shù)类咧,如果返回0,則說明制定類型的不支持人臉識別谴咸。如果手機(jī)攝像頭支持最多的人臉檢測個數(shù)是5個轮听,當(dāng)畫面超出5個人臉數(shù)骗露,還是檢測到5個人臉數(shù)岭佳。

7.15.getZoom()

返回當(dāng)前縮放值,這個值的范圍在0到getMaxZoom()之間萧锉。

8.getNumberOfCameras()

返回當(dāng)前設(shè)備可用的攝像頭個數(shù)珊随。

9.getCameraInfo(int cameraId,CameraInfo cameraInfo)

返回指定id所表示的攝像頭信息,如果getNumberOfCameras()返回N柿隙,那么有效的id值為0~(N-1)叶洞,一般手機(jī)至少有前后兩個攝像頭。

10.open(int cameraId)

使用傳入的id所表示的攝像頭來創(chuàng)建Camera對象禀崖,如果這個id所表示的攝像頭被其他應(yīng)用程序打開調(diào)用此方法會跑出異常衩辟,當(dāng)使用完相機(jī)后,必須調(diào)用release()來釋放資源波附,否則它會保持鎖定狀態(tài)艺晴,不可用其他應(yīng)用程序。

11.setPreviewDisplay(SurfaceHolder holder)

根據(jù)所傳入的SurfaceHolder對象來設(shè)置實(shí)時預(yù)覽掸屡。

12.setPreviewCallback(PreviewCallback cb)

根據(jù)傳入的PreviewCallback對象來監(jiān)聽相機(jī)預(yù)覽數(shù)據(jù)的回調(diào)封寞,PreviewCallback再上面已經(jīng)講過。

13.setParameters(Parameters params)

根據(jù)傳入的Parameters對象來設(shè)置當(dāng)前相機(jī)的參數(shù)信息仅财。

14.getParameters()

根據(jù)傳入的Parameters對象來返回當(dāng)前相機(jī)的參數(shù)信息

15.startPreview()

開始預(yù)覽狈究,在屏幕上繪制預(yù)覽幀,如果沒有調(diào)用setPreviewDisplay(SurfaceHolder)或者setPreviewTexture(SurfaceTexture)直接調(diào)用這個方法是沒有任何效果的盏求,如果啟動預(yù)覽失敗抖锥,則會引發(fā)RuntimeException亿眠。

16.stopPreview()

停止預(yù)覽,停止繪制預(yù)覽幀到屏幕磅废,如果停止失敗缕探,會引發(fā)RuntimeException。

17.startFaceDetection()

開始人臉識別还蹲,這個要調(diào)用startPreview之后調(diào)用爹耗,也就是在預(yù)覽之后才能進(jìn)行人臉識別,如果不支持人臉識別谜喊,調(diào)用此方法會拋出IllegalArgumentException潭兽。

18.stopFaceDetection()

停止人臉識別。

19.setFaceDetectionListener()

給人臉檢測設(shè)置監(jiān)聽斗遏,以便提供預(yù)覽幀山卦。

20.release()

斷開并且釋放相機(jī)對象資源。

21.setDisplayOrientation(int degress)

設(shè)置相機(jī)預(yù)覽畫面旋轉(zhuǎn)的角度诵次,在剛開始講述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)旋轉(zhuǎn)方向
     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;
     //計(jì)算圖像所要旋轉(zhuǎn)的角度
     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;
     }
     //調(diào)整圖像旋轉(zhuǎn)角度
     camera.setDisplayOrientation(result);
 }
復(fù)制代碼

上面已經(jīng)描述過在豎屏下逾一,對于后置相機(jī)來講:

只需要旋轉(zhuǎn)后置相機(jī)的orientation也就是90度即可和屏幕方向保持一致铸本;

對于前置相機(jī)預(yù)覽方向,相機(jī)預(yù)覽的圖像是相機(jī)采集到的圖像鏡像遵堵,所以旋轉(zhuǎn)orientation 270-180=90度才和屏幕方向一致箱玷。 CameraInfo是實(shí)例化的相機(jī)類,info.orientation是相機(jī)對于屏幕自然方向(左上角坐標(biāo)系)的旋轉(zhuǎn)角度數(shù)陌宿。 那下面跟著官方適配方法走:

  • int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); rotation是預(yù)覽Window的旋轉(zhuǎn)方向锡足,對于手機(jī)而言,當(dāng)在清單文件設(shè)置Activity的screenOrientation="portait"時壳坪,rotation=0舶得,這時候沒有旋轉(zhuǎn),當(dāng)screenOrientation="landScape"時爽蝴,rotation=1沐批。

  • 對于后置攝像頭,手機(jī)豎屏顯示時霜瘪,預(yù)覽圖像旋轉(zhuǎn)的角度:result=(90-0+360)%360=90珠插;手機(jī)橫屏顯示時,預(yù)覽圖像旋轉(zhuǎn):result = (90-0+360)%360 = 0;

  • camera.setDisplayOrientation(int param)這個方法是圖片輸出后所旋轉(zhuǎn)的角度數(shù)颖对,旋轉(zhuǎn)值可以是0捻撑,90,180,270顾患。

注意: camera.setDisplayOrientation(int param)這個方法僅僅是修改相機(jī)的預(yù)覽方向番捂,不會影響到PreviewCallback回調(diào)、生成的JPEG圖片和錄像視頻的方向江解,這些數(shù)據(jù)的方向會和圖像Sensor方向一致设预。

四、具體實(shí)踐

1.權(quán)限處理

需要申請拍照權(quán)限和外部存儲權(quán)限:

    <!--權(quán)限申請 相機(jī)-->
    <uses-permission android:name="android.permission.CAMERA"/>
    <!--使用uses-feature指定需要相機(jī)資源-->
    <uses-feature android:name="android.hardware.Camera"/>
    <!--需要自動聚焦 -->
    <uses-feature android:name="android.hardware.camera.autofocus"/>
    <!--存儲圖片或者視頻-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
復(fù)制代碼

onCreate檢查權(quán)限:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initBind();
        initListener();
        checkNeedPermissions();
    }
復(fù)制代碼
    /**
     * 檢測需要申請的權(quán)限
     *
     */
    private void checkNeedPermissions(){
        //6.0以上需要動態(tài)申請權(quán)限 動態(tài)權(quán)限校驗(yàn) Android 6.0 的 oppo & vivo 手機(jī)時犁河,始終返回 權(quán)限已被允許 但是當(dāng)真正用到該權(quán)限時鳖枕,卻又彈出權(quán)限申請框。
        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) {
                //多個權(quán)限一起申請
                ActivityCompat.requestPermissions(this, new String[]{
                        Manifest.permission.CAMERA,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE
                }, 1);

            } else {
                //已經(jīng)全部申請 初始化相機(jī)資源
                initCamera();
            }

        }else{
            //6.0以下不用動態(tài)申請
            initCamera();
        }
    }
復(fù)制代碼

onRequestPermissionsResult處理回調(diào):

    /**
     * 動態(tài)處理申請權(quán)限的結(jié)果
     * 用戶點(diǎn)擊同意或者拒絕后觸發(fā)
     *
     * @param requestCode 請求碼
     * @param permissions 權(quán)限
     * @param grantResults 結(jié)果碼
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case 1:
                //獲取權(quán)限一一驗(yàn)證
                if (grantResults.length > 1) {
                    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        if (grantResults[1] == PackageManager.PERMISSION_GRANTED) {
                            initCamera();
                        } else {
                            //拒絕就要強(qiáng)行跳轉(zhuǎn)設(shè)置界面
                            Permissions.showPermissionsSettingDialog(this, permissions[1]);
                        }
                    } else {
                        //拒絕就要強(qiáng)行跳轉(zhuǎn)設(shè)置界面
                        Permissions.showPermissionsSettingDialog(this, permissions[0]);
                    }
                } else {
                    ToastUtil.showShortToast(this, "請重新嘗試~");
                }
                break;
        }
    }
復(fù)制代碼

2.調(diào)用系統(tǒng)相機(jī)

    /**
     * 調(diào)用系統(tǒng)相機(jī)
     *
     */
    private void goSystemCamera(){
        //在根目錄創(chuàng)建jpg文件
        cameraSavePath = new File(Environment.getExternalStorageDirectory().getPath() + "/" + System.currentTimeMillis() +".jpg");
        //指定跳到系統(tǒng)拍照
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        //適配Android 7.0以上版本應(yīng)用私有目錄限制被訪問
        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);
    }
復(fù)制代碼

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){
            //處理調(diào)用相冊返回的路徑
            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);

    }
復(fù)制代碼

2.1.實(shí)際效果

調(diào)用系統(tǒng)相機(jī)效果

上面是調(diào)用系統(tǒng)相機(jī)拍照后的效果桨螺,另外照片存儲到了外部存儲的根目錄位置:


image.png

3.自定義相機(jī)

下面按照以下步驟來實(shí)現(xiàn)自定義相機(jī)開發(fā):

  • 在布局xml文件中定義SurfaceView用于預(yù)覽宾符,通過SurfaceView.getHolder獲取SurfaceHolder對象
  • 給SurfaceHolder對象設(shè)置監(jiān)聽回調(diào),實(shí)現(xiàn)三個方法surfaceCreated(SurfaceHolder holder)灭翔、surfaceChanged(SurfaceHolder holder, int format, int width, int height)魏烫、surfaceDestroyed(SurfaceHolder holder)
  • 在surfaceCreated(SurfaceHolder holder)方法里通過傳入的相機(jī)id來Camera.open(int cameraId)打開相機(jī)
  • 給相機(jī)設(shè)置具體參數(shù),如:預(yù)覽格式肝箱,對焦模式
  • 通過Camera.setPreviewDisplay(SurfaceHolder holder)設(shè)置實(shí)時預(yù)覽
  • 根據(jù)官方方法來設(shè)置正確的照片預(yù)覽方向
  • 調(diào)用Camera.startPreview()開始預(yù)覽
  • 同時可以調(diào)用Camera.startFaceDetection來人臉檢測哄褒,并設(shè)置回調(diào),重寫onFaceDetection(Camera.Face[] faces, Camera camera)得到檢測人臉數(shù)量
  • 調(diào)用Camera.takePicture來進(jìn)行拍照
  • 處理保存的照片煌张,旋轉(zhuǎn)或者壓縮
  • 當(dāng)相機(jī)不再調(diào)用時呐赡,釋放相機(jī)資源

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>
復(fù)制代碼

布局文件主要有拍照預(yù)覽控件SurfaceView、拍照后顯示的圖片Imageview唱矛、拍照按鈕Textview組成罚舱。

3.2.初始化SurfaceHolder

新增相機(jī)業(yè)務(wù)邏輯類CameraPresenter井辜,目的是將業(yè)務(wù)和界面顯示分開绎谦,Activity負(fù)責(zé)UI的顯示,業(yè)務(wù)邏輯在CameraPresenter粥脚,新增構(gòu)造函數(shù)窃肠,構(gòu)造函數(shù)有兩個參數(shù),分別是持有手機(jī)界面的ActivitySurfaceView對象刷允,并根據(jù)傳入的SurfaceView對象通過SurfaceView.getHolder方法獲取SurfaceHolder對象:

    public CameraPresenter(AppCompatActivity mAppCompatActivity, SurfaceView mSurfaceView) {
        this.mAppCompatActivity = mAppCompatActivity;
        this.mSurfaceView = mSurfaceView;
        mSurfaceHolder = mSurfaceView.getHolder();
    }
復(fù)制代碼

SurfaceHolder對象設(shè)置監(jiān)聽回調(diào):

    public CameraPresenter(AppCompatActivity mAppCompatActivity, SurfaceView mSurfaceView) {
        this.mAppCompatActivity = mAppCompatActivity;
        this.mSurfaceView = mSurfaceView;
        mSurfaceHolder = mSurfaceView.getHolder();
        init();
    }
    /**
     * 初始化增加回調(diào)
     */
    private void init() {
        mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                //surface創(chuàng)建時執(zhí)行
            }

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

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                //surface銷毀時執(zhí)行
            }
        });
    }
復(fù)制代碼

3.3.打開相機(jī)

surfaceCreated(SurfaceHolder holder)方法里調(diào)用打開相機(jī):

    //攝像頭Id 默認(rèn)后置 0,前置的值是1
    private int mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
     @Override
     public void surfaceCreated(SurfaceHolder holder) {
         //surface創(chuàng)建時執(zhí)行
         if (mCamera == null) {
             // mCameraId是后置還是前置 0是后置 1是前置
             openCamera(mCameraId);
            }
        }
     /**
     * 打開相機(jī) 并且判斷是否支持該攝像頭
     *
     * @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, "打開相機(jī)失敗~");
                return false;
            }

        }

        return isSupportCamera;
    }
復(fù)制代碼

3.4.設(shè)置相機(jī)具體參數(shù)

調(diào)用Camera.open(int cameraId)后返回具體的Camera對象后冤留,還需要設(shè)置相機(jī)一些參數(shù),如預(yù)覽模式树灶,對焦模式等:

/**
 * 打開相機(jī) 并且判斷是否支持該攝像頭
 *
 * @param FaceOrBack 前置還是后置
 * @return
 */
private boolean openCamera(int FaceOrBack) {
    //是否支持前后攝像頭
    boolean isSupportCamera = isSupport(FaceOrBack);
    //如果支持
    if (isSupportCamera) {
        try {
            mCamera = Camera.open(FaceOrBack);
            initParameters(mCamera);
            //設(shè)置預(yù)覽回調(diào)
            if (mCamera != null) {
                mCamera.setPreviewCallback(this);
            }
        } catch (Exception e) {
            e.printStackTrace();
            ToastUtil.showShortToast(mAppCompatActivity, "打開相機(jī)失敗~");
            return false;
        }

    }

    return isSupportCamera;
}

/**
 * 設(shè)置相機(jī)參數(shù)
 *
 * @param camera
 */
private void initParameters(Camera camera) {
    try {
        //獲取Parameters對象
        mParameters = camera.getParameters();
        //設(shè)置預(yù)覽格式
        mParameters.setPreviewFormat(ImageFormat.NV21);
        //判斷是否支持連續(xù)自動對焦圖像
        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);
        }
        //給相機(jī)設(shè)置參數(shù)
        mCamera.setParameters(mParameters);
    } catch (Exception e) {
        e.printStackTrace();
        ToastUtil.showShortToast(mAppCompatActivity, "初始化相機(jī)失敗");
    }
復(fù)制代碼

3.5.開始預(yù)覽

設(shè)置完相機(jī)參數(shù)之后纤怒,就可以需要相機(jī)調(diào)用Camera.setPreviewDisplay(SurfaceHolder holder)Camera.startPreview()開啟預(yù)覽:

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

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

surfaceCreated(SurfaceHolder holder)回調(diào)方法調(diào)用:

...
@Override
public void surfaceCreated(SurfaceHolder holder) {
       //surface創(chuàng)建時執(zhí)行
       if (mCamera == null) {
            //mCameraId是后置還是前置 0是后置 1是前置
            openCamera(mCameraId);
        }
           //并設(shè)置預(yù)覽
           startPreview();
       }
...
復(fù)制代碼

3.6.釋放相機(jī)資源

當(dāng)相機(jī)不再調(diào)用的時候天通,需要調(diào)用Camera.release()來釋放相機(jī)資源

/**
 * 釋放相機(jī)資源
 */
public void releaseCamera() {
    if (mCamera != null) {
        //停止預(yù)覽
        mCamera.stopPreview();
        mCamera.setPreviewCallback(null);
        //釋放相機(jī)資源
        mCamera.release();
        mCamera = null;
    }
}
復(fù)制代碼

surfaceDestroyed(SurfaceHolder holder)調(diào)用:

/**
 * 初始化增加回調(diào)
 */
private void init() {
    mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            //surface創(chuàng)建時執(zhí)行 mCameraId是后置還是前置 0是后置 1是前置
            if (mCamera == null) {
                openCamera(mCameraId);
            }
            //并設(shè)置預(yù)覽
            startPreview();
        }

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

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

/**
 * 設(shè)置前置還是后置
 *
 * @param mCameraId 前置還是后置
 */
public void setFrontOrBack(int mCameraId) {
    this.mCameraId = mCameraId;

}
復(fù)制代碼

在自定義相機(jī)的Activity界面進(jìn)行調(diào)用:

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

復(fù)制代碼

onDestroy()方法調(diào)用releaseCamera():

/**
 * Activity 銷毀回調(diào)方法 釋放各種資源
 */
@Override
protected void onDestroy(){
    super.onDestroy();
    if(mCameraPresenter != null){
        mCameraPresenter.releaseCamera();
    }
}
復(fù)制代碼

現(xiàn)在先看看效果:

效果一

3.7.調(diào)整預(yù)覽圖像角度

發(fā)現(xiàn)預(yù)覽效果圖逆時針旋轉(zhuǎn)了90度,當(dāng)你把手機(jī)橫屏擺放也是,上面已經(jīng)說過烘豹,因?yàn)槠聊蛔匀环较蚝蛨D像傳感器方向不一致造成的瓜贾,需要重新設(shè)置預(yù)覽時的角度,采用官方的推薦方法:

/**
 * 保證預(yù)覽方向正確
 *
 * @param appCompatActivity Activity
 * @param cameraId          相機(jī)Id
 * @param camera            相機(jī)
 */
private void setCameraDisplayOrientation(AppCompatActivity appCompatActivity, int cameraId, Camera camera) {
    Camera.CameraInfo info =
            new Camera.CameraInfo();
    Camera.getCameraInfo(cameraId, info);
    //rotation是預(yù)覽Window的旋轉(zhuǎn)方向携悯,對于手機(jī)而言祭芦,當(dāng)在清單文件設(shè)置Activity的screenOrientation="portait"時,
    //rotation=0憔鬼,這時候沒有旋轉(zhuǎn)龟劲,當(dāng)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;
    //計(jì)算圖像所要旋轉(zhuǎn)的角度
    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;
    //調(diào)整預(yù)覽圖像旋轉(zhuǎn)角度
    camera.setDisplayOrientation(result);

}
復(fù)制代碼

并在startPreview()方法里調(diào)用:

/**
 * 開始預(yù)覽
 */
private void startPreview() {
    try {
        //根據(jù)所傳入的SurfaceHolder對象來設(shè)置實(shí)時預(yù)覽
        mCamera.setPreviewDisplay(mSurfaceHolder);
        //調(diào)整預(yù)覽角度
        setCameraDisplayOrientation(mAppCompatActivity,mCameraId,mCamera);
        mCamera.startPreview();
        startFaceDetect();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
復(fù)制代碼

再次看下運(yùn)行效果:

image.png

3.8.調(diào)整預(yù)覽和保存圖像尺寸

上面調(diào)整了預(yù)覽角度的問題后,因?yàn)樵谑忻嫔习沧繖C(jī)型五花八門蕾羊,屏幕分辨率也很多侮叮,為了避免圖像變形避矢,需要調(diào)整預(yù)覽圖像和保存的圖像尺寸:

//獲取屏幕寬和高
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-手機(jī)寬高尺寸:",screenWidth +"*"+screenHeight);
    init();
}
/**
 * 
 * 設(shè)置保存圖片的尺寸
 */
private void setPictureSize() {
    List<Camera.Size> localSizes = mParameters.getSupportedPictureSizes();
    Camera.Size biggestSize = null;
    Camera.Size fitSize = null;// 優(yōu)先選預(yù)覽界面的尺寸
    Camera.Size previewSize = mParameters.getPreviewSize();//獲取預(yù)覽界面尺寸
    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;
            }

            // 選出與預(yù)覽界面等比的最高分辨率
            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);
    }

}

/**
 * 設(shè)置預(yù)覽界面尺寸
 */
private void setPreviewSize() {
    //獲取系統(tǒng)支持預(yù)覽大小
    List<Camera.Size> localSizes = mParameters.getSupportedPreviewSizes();
    Camera.Size biggestSize = null;//最大分辨率
    Camera.Size fitSize = null;// 優(yōu)先選屏幕分辨率
    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-系統(tǒng)支持的尺寸:",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-最佳預(yù)覽尺寸:",fitSize.width + "*" + fitSize.height);
        mParameters.setPreviewSize(fitSize.width, fitSize.height);
    }
}
復(fù)制代碼

這里額外要注意:對于相機(jī)來說,都是width是長邊囊榜,也就是width > height审胸,在上面setPreviewSize()方法里,獲取所支持的size.width要和screenHeight比較卸勺,size.height要和screenWidth砂沛,最后在設(shè)置相機(jī)里調(diào)用即可:

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

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

3.9.拍照

下面進(jìn)行拍照處理,拍照保存圖片有兩種方式:

  • 直接調(diào)用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) {
            ...
     }
復(fù)制代碼

三個參數(shù)的takePicture實(shí)際調(diào)用四個參數(shù)的takePicture曙求,只是帶有postview圖像數(shù)據(jù)的回調(diào)碍庵,設(shè)置為空了。

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

            }
        });
復(fù)制代碼

onPreviewFrame以字節(jié)數(shù)組形式返回具體照片數(shù)據(jù)悟狱,這個方法會不停的回調(diào)静浴,這里不演示這個方法,保存圖片的方法和第一個方法是一樣的挤渐。 首先先自定義回調(diào):

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

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

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

        //拍照路徑返回
        void getPhotoFile(String imagePath);
    }
復(fù)制代碼

調(diào)用Camera.takePicture方法:

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

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

            }
        }, new Camera.PictureCallback() {
            //回調(diào)圖片數(shù)據(jù) 點(diǎn)擊拍照后相機(jī)返回的照片byte數(shù)組苹享,照片數(shù)據(jù)
            @Override
            public void onPictureTaken(byte[] data, Camera camera) {
                //拍照后記得調(diào)用預(yù)覽方法,不然會停在拍照圖像的界面
                mCamera.startPreview();
                //回調(diào)
                mCameraCallBack.onTakePicture(data, camera);
                //保存圖片
                getPhotoPath(data);

            }
        });

    }
}
復(fù)制代碼

保存圖片目錄先放在app內(nèi):

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

創(chuàng)建目錄具體方法:

/**
 * 創(chuàng)建拍照照片文件夾
 */
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, "創(chuàng)建存放目錄失敗,請檢查磁盤空間~");
            mAppCompatActivity.finish();
        } finally {
            if (!isSuccess) {
                ToastUtil.showShortToast(mAppCompatActivity, "創(chuàng)建存放目錄失敗,請檢查磁盤空間~");
                mAppCompatActivity.finish();
            }
        }

    }
}
復(fù)制代碼

在初始化相機(jī)時先調(diào)用創(chuàng)建文件:

public CameraPresenter(AppCompatActivity mAppCompatActivity, SurfaceView mSurfaceView) {
    //...
    screenWidth = dm.widthPixels;
    screenHeight = dm.heightPixels;
    Log.d("sssd-手機(jī)寬高尺寸:",screenWidth +"*"+screenHeight);
    //創(chuàng)建文件夾目錄
    setUpFile();
    init();
}
復(fù)制代碼

拍照后保存圖片這種輸出耗時操作應(yīng)該用線程來處理浴麻,新建線程池類:

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

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

}
復(fù)制代碼

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);
            //拍照數(shù)量+1
            photoNum++;
            //圖片名字
            String name = SystemUtil.formatTime(timeMillis, SystemUtil.formatTime(photoNum) + ".jpg");
            //創(chuàng)建具體文件
            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 {
                    //將數(shù)據(jù)寫入文件
                    fos.write(data);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                //將圖片保存到手機(jī)相冊中
                SystemUtil.saveAlbum(Configuration.insidePath + file.getName(), file.getName(), mAppCompatActivity);
                //將圖片復(fù)制到外部
                SystemUtil.coptPicture(Configuration.insidePath + file.getName(),Configuration.OUTPATH,file.getName());
                //發(fā)消息給主線程
                Message message = new Message();
                message.what = 1;
                //文件路徑
                message.obj = Configuration.insidePath + file.getName();
                mHandler.sendMessage(message);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    });
}
復(fù)制代碼

上面代碼先把照片存到app包內(nèi)得问,再將照片復(fù)制到app包外,當(dāng)圖片保存處理完后软免,回調(diào)主線程進(jìn)行顯示圖片:

    @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;
            }
        }
    };
復(fù)制代碼

Activity中設(shè)置回調(diào):

//添加監(jiān)聽
mCameraPresenter.setCameraCallBack(this);
復(fù)制代碼

拍照后保存圖片后顯示在界面上,Activity實(shí)現(xiàn)照片顯示:

/**
 * 返回拍照后的照片
 * @param imagePath
 */
@Override
public void getPhotoFile(String imagePath) {
    //設(shè)置頭像
    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);

}
復(fù)制代碼

布局文件增加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>
復(fù)制代碼

效果如下:

效果二

看看拍照后存儲的照片:



image.png

發(fā)現(xiàn)拍照后存儲的照片經(jīng)過逆時針90度旋轉(zhuǎn)宫纬,需要將順時針90度,原因在上面分析orientation的時候講述過膏萧,雖然調(diào)整來預(yù)覽圖像角度漓骚,但是并不能調(diào)整圖片傳感器的圖片方向宣蔚,所以只能保存圖片后再將圖片旋轉(zhuǎn):

/**
 * 旋轉(zhuǎn)圖片
 * @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){
            //順時針旋轉(zhuǎn)270度 
            matrix.postRotate(270);
    }
    // 創(chuàng)建新的圖片
    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);
        //默認(rèn)jpg
        resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
        fos.flush();
        fos.close();
        resizedBitmap.recycle();
    }catch (Exception e){
        e.printStackTrace();
        return;
    }

}
復(fù)制代碼

在保存圖像后調(diào)用:

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

3.10.變換攝像頭

在布局文件添加TextView作為前后攝像頭轉(zhuǎn)換:

    <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"
        />
復(fù)制代碼

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

/**
 * 前后攝像切換
 */
public void switchCamera() {
    //先釋放資源
    releaseCamera();
    //在Android P之前 Android設(shè)備仍然最多只有前后兩個攝像頭认境,在Android p后支持多個攝像頭 用戶想打開哪個就打開哪個
    mCameraId = (mCameraId + 1) % Camera.getNumberOfCameras();
    //打開攝像頭
    openCamera(mCameraId);
    //切換攝像頭之后開啟預(yù)覽
    startPreview();
}
復(fù)制代碼

具體調(diào)用:

    case R.id.tv_change_camera:
        mCameraPresenter.switchCamera();
        break;
復(fù)制代碼

效果如下圖:

前后攝像頭變換

在看看拍照后存儲的照片:


image.png

這里可以發(fā)現(xiàn)胚委,在預(yù)覽的時候只是順時針調(diào)用setDisplayOrientation()設(shè)置預(yù)覽方向,并沒有做鏡面翻轉(zhuǎn)叉信,為什么切換前置時亩冬,預(yù)覽效果跟實(shí)物一樣呢,原來是在調(diào)用setDisplayOrientation()做了水平鏡面的翻轉(zhuǎn)硼身,但是拍照后保存下來的照片是沒有水平翻轉(zhuǎn)的硅急,所以同時要對拍照后的照片做水平方向鏡面翻轉(zhuǎn),那就在旋轉(zhuǎn)圖片里的方法加上翻轉(zhuǎn)處理:

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

    //新增 如果是前置 需要鏡面翻轉(zhuǎn)處理
    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);
        //默認(rèn)jpg
        resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
        fos.flush();
        fos.close();
        resizedBitmap.recycle();
    }catch (Exception e){
        e.printStackTrace();
        return;
    }

}
復(fù)制代碼

這樣就能保證預(yù)覽和拍攝后保存的照片和實(shí)物一樣了佳遂。

3.11.改變焦距

拍照必不可少的一個功能:改變焦距营袜。在Camera中的內(nèi)部類Camera.ParametersParameters.setZoom(int value)來調(diào)整預(yù)覽圖像縮放系數(shù),因?yàn)樵诓季?code>SurfaceView是全屏的丑罪,在OnTouch方法做處理荚板,并點(diǎn)擊屏幕進(jìn)行自動變焦處理:

//默認(rèn)狀態(tài)
private static final int MODE_INIT = 0;
//兩個觸摸點(diǎn)觸摸屏幕狀態(tài)
private static final int MODE_ZOOM = 1;
//標(biāo)識模式
private int mode = MODE_INIT;
...
/**
 *
 * 觸摸回調(diào)
 * @param v 添加Touch事件具體的view
 * @param event 具體事件
 * @return
 */
@Override
public boolean onTouch(View v, MotionEvent event) {
    //無論多少跟手指加進(jìn)來,都是MotionEvent.ACTION_DWON MotionEvent.ACTION_POINTER_DOWN
    //MotionEvent.ACTION_MOVE:
    switch (event.getAction() & MotionEvent.ACTION_MASK){
        //手指按下屏幕
        case MotionEvent.ACTION_DOWN:
           mode = MODE_INIT;
           break;
        //當(dāng)屏幕上已經(jīng)有觸摸點(diǎn)按下的狀態(tài)的時候吩屹,再有新的觸摸點(diǎn)被按下時會觸發(fā)
        case MotionEvent.ACTION_POINTER_DOWN:
           mode = MODE_ZOOM;
           //計(jì)算兩個手指的距離 兩點(diǎn)的距離
           startDis = SystemUtil.twoPointDistance(event);
           break;
        //移動的時候回調(diào)
        case MotionEvent.ACTION_MOVE:
            isMove = true;
           //這里主要判斷有兩個觸摸點(diǎn)的時候才觸發(fā)
           if(mode == MODE_ZOOM){
               //只有兩個點(diǎn)同時觸屏才執(zhí)行
               if(event.getPointerCount() < 2){
                 return true;
               }
               //獲取結(jié)束的距離
               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();
                   }
                   //如果系數(shù)小于0
                   if(zoom < 0 ){
                       zoom = 0;
                   }
                   //設(shè)置焦距
                   mCameraPresenter.setZoom(zoom);
                   //將最后一次的距離設(shè)為當(dāng)前距離
                   startDis = endDis;
               }
           }
           break;
        case MotionEvent.ACTION_UP:
            //判斷是否點(diǎn)擊屏幕 如果是自動聚焦
            if(isMove == false){
                //自動聚焦
                mCameraPresenter.autoFoucus();
            }
            isMove = false;
            break;
    }
    return true;
}
復(fù)制代碼

CameraPresenter內(nèi)調(diào)用:

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

}

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

            }
        });
    }
}
復(fù)制代碼

最終效果如下圖:

加入自動變焦效果圖

3.12.閃光燈設(shè)置

通過Parameters.setFlashMode(String value)來控制閃光燈跪另,參數(shù)類型有以下:

  • FLASH_MODE_OFF 關(guān)閉閃光燈
  • FLASH_MODE_AUTO 在預(yù)覽,自動對焦和快照過程中需要時煤搜,閃光燈會自動開啟免绿。
  • FLASH_MODE_ON 無論如何均使用閃光燈
  • FLASH_MODE_RED_EYE 仿紅眼模式,降低紅眼模式
  • FLASH_MODE_TORCH 系統(tǒng)會判斷需要補(bǔ)光而自動決定是否開啟閃光燈擦盾,手電筒模式嘲驾,自動對焦

在平時中,用FLASH_MODE_OFFFLASH_MODE_TORCH就行

    /**
     *
     * 閃光燈
     * @param turnSwitch true 為開啟 false 為關(guān)閉
     */
    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);
    }
復(fù)制代碼

具體調(diào)用:

@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;
        //關(guān)閉還是開啟閃光燈    
        case R.id.tv_flash:
            mCameraPresenter.turnLight(isTurn);
            tv_flash.setBackgroundResource(isTurn ? R.drawable.icon_turnon : R.drawable.icon_turnoff);
            isTurn = !isTurn;
        default:
            break;
    }
}
復(fù)制代碼

實(shí)際效果:

閃光燈效果

3.13.調(diào)整亮度

到這里可以發(fā)現(xiàn)迹卢,相比于調(diào)用系統(tǒng)拍照的清晰度辽故,自定義拍照就遜色一籌,感覺上面有一層蒙版罩著婶希。調(diào)用系統(tǒng)拍照可以發(fā)現(xiàn)榕暇,屏幕亮度故意調(diào)亮,那么是不是把自定義拍照的界面亮度調(diào)大喻杈,效果清晰度會不會好一些呢,下面試試狰晚,在CustomCameraActivity加入:

/**
 *
 * 加入調(diào)整亮度
 */
private void getScreenBrightness(){
    WindowManager.LayoutParams lp = getWindow().getAttributes();
    //screenBrightness的值是0.0-1.0 從0到1.0 亮度逐漸增大 如果是-1筒饰,那就是跟隨系統(tǒng)亮度 這里調(diào)成 0.78左右
    lp.screenBrightness = Float.valueOf(200) * (1f / 255f);
    getWindow().setAttributes(lp);
}
復(fù)制代碼

onCreate調(diào)用即可,最后效果如下:

自定義相機(jī)效果如下:

image.png

調(diào)用系統(tǒng)相機(jī)效果如下:
image.png

效果確實(shí)比之前好多了壁晒。

3.14.視頻錄制

下面簡單實(shí)現(xiàn)錄制視頻的功能瓷们,利用MediaRecorder來實(shí)現(xiàn)直接錄制視頻,這里要注意:MediaRecorder是不能對每一幀數(shù)據(jù)做處理的,錄制視頻需要用到以下工具:

  • MediaRecorder:視頻編碼的封裝
  • camera:視頻畫面原屬數(shù)據(jù)采集
  • SurfaceView:提供預(yù)覽畫面
3.14.1.MediaRecorder基本介紹

MediaRecorder是Android中面向應(yīng)用層的封裝谬晕,用于提供音視頻編碼的封裝操作的工具碘裕,下面直接上官方圖:


image.png

下面簡單介紹這幾個生命周期的狀態(tài)意思:

  • Initial:在MediaRecorder對象被創(chuàng)建時或者調(diào)用reset()方法后,會處于該狀態(tài)攒钳。
  • Initialized:當(dāng)調(diào)用setAudioSource()或者setVideoSource()后就會處于該狀態(tài)帮孔,這兩個方法主要用于設(shè)置音視頻的播放源配置,在該狀態(tài)下可以調(diào)用reset()回到Initial狀態(tài)不撑。
  • DataSourceConfigured:當(dāng)調(diào)用setOutputFormat方法后文兢,就會處于該狀態(tài),這個方法用來設(shè)置文件格式焕檬,如設(shè)置為mp4或者mp3姆坚,在這個狀態(tài)同時可以設(shè)置音視頻的封裝格式,采樣率实愚,視頻碼率兼呵,幀率等,可以通過調(diào)用reset()回到Initial狀態(tài)腊敲。
  • Prepared:當(dāng)調(diào)用上面幾個方法后萍程,就可以調(diào)用prepare()進(jìn)入這個狀態(tài),只有處于這個狀態(tài)才能調(diào)用start()方法兔仰。
  • Recording:通過調(diào)用start()來進(jìn)入該狀態(tài)茫负,處于這個狀態(tài)就是真正錄制音視頻,通過調(diào)用reset()或者stop()來回到Initial狀態(tài)乎赴。
  • error:當(dāng)錄制過程中發(fā)生錯誤忍法,就會進(jìn)入該狀態(tài),調(diào)用reset()回到Initial狀態(tài)榕吼。
  • release:釋放系統(tǒng)資源饿序,只有在Initial狀態(tài)才能調(diào)用release()回到該狀態(tài)。
3.14.2.調(diào)整輸出視頻尺寸的寬高

注意:要添加錄音權(quán)限羹蚣,這里不在講述原探。

/**
 * 獲取輸出視頻的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();
    //得到系統(tǒng)支持視頻格式
    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;

    }
}
復(fù)制代碼

在初始化相機(jī)方法調(diào)用,并且創(chuàng)建MediaRecorder對象:

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            //surface創(chuàng)建時執(zhí)行
            if (mCamera == null) {
                openCamera(mCameraId);
            }
            //并設(shè)置預(yù)覽
            startPreview();
            //新增獲取系統(tǒng)支持視頻
            getVideoSize();
            mediaRecorder = new MediaRecorder();
        }
復(fù)制代碼
3.14.3.設(shè)置MediaRecorder參數(shù)
    //解鎖Camera硬件
    mCamera.unlock();
    mediaRecorder.setCamera(mCamera);
    //音頻源 麥克風(fēng)
    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);
    //設(shè)置幀頻率
    mediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024 * 100);
    Log.d("sssd視頻寬高:","寬"+width+"高"+height+"");
    mediaRecorder.setVideoSize(width,height);
    //每秒的幀數(shù)
    mediaRecorder.setVideoFrameRate(24);
復(fù)制代碼
3.14.4.調(diào)整保存視頻角度

如果不設(shè)置調(diào)整保存視頻的角度顽素,用后置錄制視頻會逆時針翻轉(zhuǎn)90度咽弦,所以需要設(shè)置輸出順時針旋轉(zhuǎn)90度:

    //調(diào)整視頻旋轉(zhuǎn)角度 如果不設(shè)置 后置和前置都會被旋轉(zhuǎn)播放
    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);
        }
    }
復(fù)制代碼

整個錄制方法如下:

/**
 *
 * 錄制方法
 */
public void startRecord(String path,String name){
    //解鎖Camera硬件
    mCamera.unlock();
    mediaRecorder.setCamera(mCamera);
    //音頻源 麥克風(fēng)
    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);
    //設(shè)置幀頻率
    mediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024 * 100);
    Log.d("sssd視頻寬高:","寬"+width+"高"+height+"");
    mediaRecorder.setVideoSize(width,height);
    //每秒的幀數(shù)
    mediaRecorder.setVideoFrameRate(24);
    //調(diào)整視頻旋轉(zhuǎn)角度 如果不設(shè)置 后置和前置都會被旋轉(zhuǎn)播放
    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();
    }
    //設(shè)置輸出文件名字
    mediaRecorder.setOutputFile(path + File.separator + name + "mp4");
    File file1 = new File(path + File.separator + name + "mp4");
    if(file1.exists()){
        file1.delete();
    }
    //設(shè)置預(yù)覽
    mediaRecorder.setPreviewDisplay(mSurfaceView.getHolder().getSurface());
    try {
        //準(zhǔn)備錄制
        mediaRecorder.prepare();
        //開始錄制
        mediaRecorder.start();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
復(fù)制代碼
3.14.5.停止錄制

當(dāng)停止錄制后需要把MediaRecorder釋放,并且重新調(diào)用預(yù)覽方法:

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

    if(mCamera != null){
        mCamera.release();
    }
    openCamera(mCameraId);
    //并設(shè)置預(yù)覽
    startPreview();
}
復(fù)制代碼
3.14.6.具體調(diào)用
mCameraPresenter.startRecord(Configuration.OUTPATH,"video");
復(fù)制代碼
3.14.7.視頻播放

當(dāng)錄制完需要播放胁出,用新的界面來型型,用SurfaceView+MediaPlayer來實(shí)現(xiàn):

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);
        //下面開始實(shí)例化MediaPlayer對象
        player = new MediaPlayer();
        player.setOnCompletionListener(this);
        player.setOnPreparedListener(this);
        //設(shè)置數(shù)據(jù)數(shù)據(jù)源,也就播放文件地址全蝶,可以是網(wǎng)絡(luò)地址
        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關(guān)聯(lián)起來
                player.setDisplay(holder);
                //異步緩沖當(dāng)前視頻文件闹蒜,也有一個同步接口
                player.prepareAsync();
            }

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

            }
            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });

    }
    /**
     *
     * 設(shè)置循環(huán)播放
     * @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;

        }
    }
}
復(fù)制代碼

實(shí)際效果:

最終效果

視頻存放路徑信息:
image.png

3.15.人臉檢測

下面實(shí)現(xiàn)人臉檢測寺枉,注意是人臉檢測不是人臉識別,步驟如下:

  • 在相機(jī)預(yù)覽后绷落,調(diào)用startFaceDetection方法開啟人臉檢測
  • 調(diào)用setFaceDetectionListener(FaceDetectionListener listener)設(shè)置人臉檢測回調(diào)
  • 自定義View姥闪,用來繪制人臉大致區(qū)域
  • 在人臉回調(diào)中,所獲取的人臉信息傳遞給自定義View砌烁,自定義View根據(jù)人臉信息繪制大致區(qū)域
3.15.1.開啟人臉檢測

在相機(jī)調(diào)用開啟預(yù)覽后才能調(diào)用:

/**
 * 開始預(yù)覽
 */
private void startPreview() {
    try {
        //根據(jù)所傳入的SurfaceHolder對象來設(shè)置實(shí)時預(yù)覽
        mCamera.setPreviewDisplay(mSurfaceHolder);
        //調(diào)整預(yù)覽角度
        setCameraDisplayOrientation(mAppCompatActivity,mCameraId,mCamera);
        mCamera.startPreview();
        //開啟人臉檢測
        startFaceDetect();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
復(fù)制代碼
3.15.2.設(shè)置人臉檢測回調(diào)
/**
 * 人臉檢測
 */
private void startFaceDetect() {
    //開始人臉檢測筐喳,這個要調(diào)用startPreview之后調(diào)用
    mCamera.startFaceDetection();
    //添加回調(diào)
    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)+"張人臉","分?jǐn)?shù)"+faces[i].score+"左眼"+faces[i].leftEye+"右眼"+faces[i].rightEye+"嘴巴"+faces[i].mouth);
            }
        }
    });
}
復(fù)制代碼

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()
復(fù)制代碼

具體意思是在人臉使用的坐標(biāo)和安卓屏幕坐標(biāo)是不一樣的往弓,并且舉了一個例子:如果屏幕尺寸是800*480疏唾,現(xiàn)在有一個矩形位置在人臉坐標(biāo)系中位置是(-1000,-1000,0,0),那么在安卓屏幕坐標(biāo)的位置是(0,0,400,240)函似。

并且給了轉(zhuǎn)換坐標(biāo)的具體方法:

/**
 * 將相機(jī)中用于表示人臉矩形的坐標(biāo)轉(zhuǎn)換成UI頁面的坐標(biāo)
 *
 * @param faces 人臉數(shù)組
 * @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);
    }
    //后乘旋轉(zhuǎn)角度
    matrix.postRotate(Float.valueOf(orientation));
    //后乘縮放
    matrix.postScale(mSurfaceView.getWidth() / 2000f,mSurfaceView.getHeight() / 2000f);
    //再進(jìn)行位移
    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;

}
復(fù)制代碼
3.15.3.實(shí)現(xiàn)自定義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);
        //設(shè)置粗細(xì)
        mPaint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1f,context.getResources().getDisplayMetrics()));
        //設(shè)置抗鋸齒
        mPaint.setAntiAlias(true);
    }

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

        }
    }

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

}

復(fù)制代碼

布局文件:

    <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"/>
復(fù)制代碼

并增加人臉檢測開關(guān):

    /**
     * 開啟人臉檢測
     *
     */
    public void turnFaceDetect(boolean isDetect){
         mFaceView.setVisibility(isDetect ?  View.VISIBLE : View.GONE);
    }
復(fù)制代碼

這里只是將自定義View不顯示槐脏,具體效果圖如下:

人臉檢測效果圖

查看具體打印數(shù)據(jù):


image.png

可以發(fā)現(xiàn)在vivo安卓7.1.1版本下,眼睛撇寞,嘴巴數(shù)據(jù)是獲取不到的顿天。

五、知識點(diǎn)梳理

到這里自定義相機(jī)Camera1步驟再次梳理如下:

  • 1.創(chuàng)建輸出圖片/視頻目錄
  • 2.創(chuàng)建布局文件蔑担,將預(yù)覽界面和用戶界面綁定(一般用SurfaceView/TextureView)牌废,進(jìn)行實(shí)時顯示相機(jī)預(yù)覽圖像
  • 3.通過SurfaceView獲取的SurfaceHolder設(shè)置SurfaceHolder.Callback監(jiān)聽,實(shí)現(xiàn)surfaceCreated啤握、surfaceChangedsurfaceDestroyed方法/如果是TextureView的話就設(shè)置setSurfaceTextureListener監(jiān)聽并實(shí)現(xiàn)onSurfaceTextureAvailable鸟缕、onSurfaceTextureSizeChangedonSurfaceTextureDestroyedonSurfaceTextureUpdated方法
  • 4.在SurfaceView->surfaceCreated/TextureView->onSurfaceTextureAvailable方法通過Camera.open(int cameraId)打開相機(jī)
  • 5.通過Camera.getParameters()獲取Parameters對象并且設(shè)置具體參數(shù)
  • 6.將Parameters對象通過Camera.setParameters(Parameters parames)設(shè)置進(jìn)Camera中
  • 7.設(shè)置預(yù)覽圖片尺寸和保存圖片尺寸
  • 8.設(shè)置預(yù)覽回調(diào)和實(shí)時預(yù)覽
  • 9.調(diào)整預(yù)覽角度
  • 10.通過Camera.startPreview()開啟預(yù)覽
  • 11.拍照時實(shí)現(xiàn)takePicture(ShutterCallback shutter, PictureCallback raw,PictureCallback jpeg),在回調(diào)的onPictureTaken(byte[] data, Camera camera)返回的字節(jié)數(shù)組里保存圖片排抬,保存圖片按需是否需要旋轉(zhuǎn)設(shè)置
  • 12.在SurfaceView->surfaceDestroyed/TextureView->onSurfaceTextureUpdated進(jìn)行資源釋放

六懂从、參考資料

作者:真丶深紅騎士
鏈接:https://juejin.im/post/5d6d1155e51d4561ea1a94a4
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)蹲蒲,非商業(yè)轉(zhuǎn)載請注明出處番甩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市届搁,隨后出現(xiàn)的幾起案子缘薛,更是在濱河造成了極大的恐慌,老刑警劉巖卡睦,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宴胧,死亡現(xiàn)場離奇詭異,居然都是意外死亡么翰,警方通過查閱死者的電腦和手機(jī)牺汤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浩嫌,“玉大人檐迟,你說我怎么就攤上這事÷肽停” “怎么了追迟?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長骚腥。 經(jīng)常有香客問我敦间,道長,這世上最難降的妖魔是什么束铭? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任廓块,我火速辦了婚禮,結(jié)果婚禮上契沫,老公的妹妹穿的比我還像新娘带猴。我一直安慰自己,他們只是感情好懈万,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布拴清。 她就那樣靜靜地躺著,像睡著了一般会通。 火紅的嫁衣襯著肌膚如雪口予。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天涕侈,我揣著相機(jī)與錄音沪停,去河邊找鬼。 笑死裳涛,一個胖子當(dāng)著我的面吹牛木张,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播调违,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼窟哺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了技肩?” 一聲冷哼從身側(cè)響起且轨,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虚婿,沒想到半個月后旋奢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡然痊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年至朗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剧浸。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡锹引,死狀恐怖矗钟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嫌变,我是刑警寧澤吨艇,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站腾啥,受9級特大地震影響东涡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜倘待,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一疮跑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凸舵,春花似錦祖娘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至增热,卻和暖如春整以,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背峻仇。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工公黑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摄咆。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓凡蚜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吭从。 傳聞我的和親對象是個殘疾皇子朝蜘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348