Android-自定義相機(jī)Camera

前言

由于最近一個項目需要自定義相機(jī)這塊,踩了很多坑凿可,在這里做個記錄孩等,以防忘記。

Android Camera 相關(guān)API可以說是Android 生態(tài)碎片化最嚴(yán)重的一塊
目前有兩套Camera Api 以android 5.0為分界線缰猴,5.0以下的是Camera ,5.0以上的是Camera2疤剑,然而Camera2 各個產(chǎn)商支持的各不相同滑绒,這就導(dǎo)致我們在相機(jī)開發(fā)中要花很大的精力去處理兼容性問題。

相機(jī)開發(fā)的流程

自定義相機(jī)開發(fā)流程大概可以分為5步

  • 1隘膘、檢測并訪問相機(jī)資源蹬挤,檢查手機(jī)是否存在相機(jī)資源,如果存在則請求訪問相機(jī)資源棘幸。
  • 2焰扳、創(chuàng)建預(yù)覽界面,將預(yù)覽畫面與設(shè)計好的用戶界面控件融合在一起误续,實時顯示相機(jī)的預(yù)覽圖像吨悍。
  • 3、設(shè)置拍照監(jiān)聽蹋嵌,給用戶界面控件綁定監(jiān)聽器育瓜,使其能響應(yīng)用戶操作, 開始拍照過程。
  • 4栽烂、拍照并保存文件躏仇,將拍攝獲得的圖像轉(zhuǎn)換成位圖文件,最終輸出保存成各種常用格式的圖片腺办。
  • 5焰手、釋放相機(jī)資源,相機(jī)是一個共享資源怀喉,當(dāng)相機(jī)使用完畢后书妻,必須正確地將其釋放,以免其它程序訪問使用時發(fā)生沖突躬拢。

Camera相關(guān)API了解

Camera Api中主要涉及以下幾個關(guān)鍵類

  • Camera:操作和管理相機(jī)資源躲履,支持相機(jī)資源切換见间,設(shè)置預(yù)覽和拍攝尺寸,設(shè)置光圈工猜、曝光等相關(guān)參數(shù)米诉。
  • SurfaceView:用于繪制相機(jī)預(yù)覽圖像,提供實時預(yù)覽的圖像篷帅。
  • SurfaceHolder:用于控制Surface的一個抽象接口荒辕,它可以控制Surface的尺寸、格式與像素等犹褒,并可以監(jiān)視Surface的變化。
  • SurfaceHolder.Callback:用于監(jiān)聽Surface狀態(tài)變化的接口弛针。

1叠骑、Camera

方法 說明
open(int cameraId) 獲取Camera實例 cameraId 的值有兩個,一個是CameraInfo.CAMERA_FACING_BACK,CameraInfo.CAMERA_FACING_FRONT 前置攝像頭和后置攝像頭
setPreviewDisplay 綁定繪制預(yù)覽圖像的surface削茁。
setPrameters 設(shè)置相機(jī)參數(shù)宙枷,包括前后攝像頭,閃光燈模式茧跋、聚焦模式慰丛、預(yù)覽和拍照尺寸等
startPreview() 開始預(yù)覽,將camera底層硬件傳來的預(yù)覽幀數(shù)據(jù)顯示在綁定的surface上
stopPreview() 停止預(yù)覽瘾杭,關(guān)閉camra底層的幀數(shù)據(jù)傳遞以及surface上的繪制诅病。
release() 釋放Camera實例
takePicture(ShutterCallback shutter, PictureCallback raw,PictureCallback jpeg) 這個是實現(xiàn)相機(jī)拍照的主要方法,包含了三個回調(diào)參數(shù)粥烁。shutter是快門按下時的回調(diào)贤笆,raw是獲取拍照原始數(shù)據(jù)的回調(diào),jpeg是獲取經(jīng)過壓縮成jpg格式的圖像數(shù)據(jù)的回調(diào)讨阻。

更多API 可以查看這篇博客

2芥永、SurfaceHolder.Callback

接口 說明
surfaceCreated(SurfaceHolder holder) 在surface第一次創(chuàng)建的時候調(diào)用。
surfaceChanged(SurfaceHolder holder, int format, int width, int height) 在surface的format或size等發(fā)生變化時調(diào)用钝吮。
surfaceDestroyed(SurfaceHolder holder) 在surface銷毀的時候被調(diào)用埋涧。

Camera2相關(guān)API了解

1、Camera2

  • CameraManager:攝像頭管理器奇瘦,用于打開和關(guān)閉系統(tǒng)攝像頭
  • CameraCharacteristics:描述攝像頭的各種特性棘催,我們可以通過CameraManager的getCameraCharacteristics(@NonNull String cameraId)方法來獲取。
  • CameraDevice:描述系統(tǒng)攝像頭耳标,類似于早期的Camera巧鸭。
  • CameraCaptureSession:Session類,當(dāng)需要拍照麻捻、預(yù)覽等功能時纲仍,需要先創(chuàng)建該類的實例呀袱,然后通過該實例里的方法進(jìn)行控制(例如:拍照 capture())。
  • CaptureRequest:描述了一次操作請求郑叠,拍照夜赵、預(yù)覽等操作都需要先傳入
  • CaptureRequest參數(shù),具體的參數(shù)控制也是通過CameraRequest的成員變量來設(shè)置乡革。
  • CaptureResult:描述拍照完成后的結(jié)果

關(guān)于SurfaceView/TextureView

SurfaceView是一個有自己Surface的View寇僧。界面渲染可以放在單獨線程而不是主線程中。它更像是一個Window沸版,自身不能做變形和動畫嘁傀。
TextureView同樣也有自己的Surface。但是它只能在擁有硬件加速層層的Window中繪制视粮,它更像是一個普通View细办,可以做變形和動畫。

更多關(guān)于SurfaceView與TextureView區(qū)別的內(nèi)容可以參考這篇文章Android 5.0(Lollipop)中的SurfaceTexture蕾殴,TextureView, SurfaceView和GLSurfaceView.

封裝

API大概熟悉了笑撞,那么重點來了。那該如何封裝呢钓觉?其實我們封裝相機(jī)無非就是需要以下功能

  • 打開相機(jī)
  • 開啟預(yù)覽
  • 拍照
    • 開關(guān)閃關(guān)燈
    • 聚焦
    • 等等
  • 關(guān)閉預(yù)覽
  • 關(guān)閉相機(jī)

那么如何封裝呢茴肥,官方的開源庫cameraview給出了方案。

圖片

查看源碼荡灾,大概的就是這幾個類瓤狐。

在這里插入圖片描述

類不多,大致使用的設(shè)計模式是抽象工廠模式批幌。這里也只是簡單的拍照功能芬首。
如果你不是重度的去定制相機(jī)的話,大概也只會用到預(yù)覽界面逼裆,保存圖片,以及閃光燈郁稍,聚焦這幾個,這幾個中最容易出現(xiàn)適配問題的就是預(yù)覽界面失真以及保存圖片的方向問題胜宇。保存圖片方向的問題其實也可以在最后輸出結(jié)果時進(jìn)行轉(zhuǎn)向耀怜,這樣的話又要多一步操作,能夠一步到位的事情桐愉,堅決不多一步财破。

那么接下來根據(jù)分析一下預(yù)覽界面失真問題。

public class CameraView extends FrameLayout {

    public CameraView(Context context) {
        this(context, null);
    }

    public CameraView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    @SuppressWarnings("WrongConstant")
    public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
        ....
        依據(jù)不同版本實例化不同的camera
           // Internal setup
        final PreviewImpl preview = createPreviewImpl(context);
        mCallbacks = new CallbackBridge();
        if (Build.VERSION.SDK_INT < 21) {
            mImpl = new Camera1(mCallbacks, preview);
        } else if (Build.VERSION.SDK_INT < 23) {
            mImpl = new Camera2(mCallbacks, preview, context);
        } else {
            mImpl = new Camera2Api23(mCallbacks, preview, context);
        }
        .....
        
    }
    
    //獲取相應(yīng)的預(yù)覽界面
    @NonNull
    private PreviewImpl createPreviewImpl(Context context) {
        PreviewImpl preview;
        if (Build.VERSION.SDK_INT >= 23) {
            preview = new SurfaceViewPreview(context, this);
        } else {
            preview = new TextureViewPreview(context, this);
        }
        return preview;
    }
    
    //根據(jù)設(shè)計稿設(shè)計的預(yù)覽界面尺寸去獲取相應(yīng)的
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (isInEditMode()) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }
        // Handle android:adjustViewBounds
        //需要調(diào)整預(yù)覽界面从诲。這個設(shè)計和UI設(shè)計的預(yù)覽會有點不同左痢,可能過高或者過矮。如果底部按鈕會相應(yīng)的適配可采用此方式
        if (mAdjustViewBounds) {
            if (!isCameraOpened()) {
                mCallbacks.reserveRequestLayoutOnOpen();
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                return;
            }
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
                final AspectRatio ratio = getAspectRatio();
                assert ratio != null;
                int height = (int) (MeasureSpec.getSize(widthMeasureSpec) * ratio.toFloat());
                if (heightMode == MeasureSpec.AT_MOST) {
                    height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
                }
                super.onMeasure(widthMeasureSpec,
                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
            } else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
                final AspectRatio ratio = getAspectRatio();
                assert ratio != null;
                int width = (int) (MeasureSpec.getSize(heightMeasureSpec) * ratio.toFloat());
                if (widthMode == MeasureSpec.AT_MOST) {
                    width = Math.min(width, MeasureSpec.getSize(widthMeasureSpec));
                }
                super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                        heightMeasureSpec);
            } else {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            }
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
        // Measure the TextureView
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        //獲取當(dāng)前設(shè)置的比例,當(dāng)前設(shè)置的比例也是通過預(yù)覽尺寸計算出來的
        AspectRatio ratio = getAspectRatio();
        if (mDisplayOrientationDetector.getLastKnownDisplayOrientation() % 180 == 0) {
            ratio = ratio.inverse();
        }
        assert ratio != null;
        //根據(jù)當(dāng)前的比例計算預(yù)覽View相應(yīng)的寬高俊性。同比例放大或縮小略步。保證preview不會出現(xiàn)圖形壓扁或者拉伸的情況
        if (height < width * ratio.getY() / ratio.getX()) {
            mImpl.getView().measure(
                    MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(width * ratio.getY() / ratio.getX(),
                            MeasureSpec.EXACTLY));
        } else {
            mImpl.getView().measure(
                    MeasureSpec.makeMeasureSpec(height * ratio.getX() / ratio.getY(),
                            MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        }
    }
    
}


class Camera1 extends CameraViewImpl {

    .....
    
    void adjustCameraParameters() {
        SortedSet<Size> sizes = mPreviewSizes.sizes(mAspectRatio);
        if (sizes == null) { // Not supported
            mAspectRatio = chooseAspectRatio();
            sizes = mPreviewSizes.sizes(mAspectRatio);
        }
        Size size = chooseOptimalSize(sizes);

        // Always re-apply camera parameters
        // Largest picture size in this ratio
        final Size pictureSize = mPictureSizes.sizes(mAspectRatio).last();
        if (mShowingPreview) {
            mCamera.stopPreview();
        }
        mCameraParameters.setPreviewSize(size.getWidth(), size.getHeight());
        mCameraParameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
        mCameraParameters.setRotation(calcCameraRotation(mDisplayOrientation));
        setAutoFocusInternal(mAutoFocus);
        setFlashInternal(mFlash);
        mCamera.setParameters(mCameraParameters);
        if (mShowingPreview) {
            mCamera.startPreview();
        }
    }
    
    //獲取最佳預(yù)覽尺寸。
    @SuppressWarnings("SuspiciousNameCombination")
    private Size chooseOptimalSize(SortedSet<Size> sizes) {
        //如果預(yù)覽界面的尺寸是0定页,0 那么就隨便取一個尺寸趟薄。
        if (!mPreview.isReady()) { // Not yet laid out
            return sizes.first(); // Return the smallest size
        }
        //等到View的繪制完成(onMeasure())后拿到預(yù)覽界面的view寬高去獲取相近的預(yù)覽寬高。
        int desiredWidth;
        int desiredHeight;
        final int surfaceWidth = mPreview.getWidth();
        final int surfaceHeight = mPreview.getHeight();
        if (isLandscape(mDisplayOrientation)) {
            desiredWidth = surfaceHeight;
            desiredHeight = surfaceWidth;
        } else {
            desiredWidth = surfaceWidth;
            desiredHeight = surfaceHeight;
        }
        Size result = null;
        for (Size size : sizes) { // Iterate from small to large
            if (desiredWidth <= size.getWidth() && desiredHeight <= size.getHeight()) {
                return size;

            }
            result = size;
        }
        return result;
    }
    .....
}

上面大概分析的是根據(jù)當(dāng)前設(shè)置的比例去計算camera預(yù)覽界面寬高典徊,然后根據(jù)預(yù)覽界面的寬高去獲取相近的預(yù)覽尺寸杭煎,這樣可以保證預(yù)覽時顯示的圖像不會出現(xiàn)失真。

網(wǎng)上比較多的方案是以下這個方法卒落,這個方式其實是這個根據(jù)這個項目open camera進(jìn)行修改的羡铲。是根據(jù)surfaceview的寬高比去獲取相camera對應(yīng)相近的預(yù)覽尺寸,但是這個有一個缺點儡毕,就是如果 SurfaceView的寬高比和camera對應(yīng)預(yù)覽尺寸的寬高比不一致也切,有一點點的誤差,就會出現(xiàn)一點點失真妥曲,而官方的開源庫cameraview 寬高比是根據(jù)預(yù)覽尺寸計算出來的,因此官方開源那種方式百分百不會出現(xiàn)失真钦购,當(dāng)然如果這個SurfaceView的寬高比和相機(jī)提供的預(yù)覽尺寸中的寬高比一致的話檐盟,那么也不會失真。因此下面這個方式寬高要設(shè)置好押桃。

    /**
     * 獲取最佳預(yù)覽大小
     *
     * @param sizes 所有支持的預(yù)覽大小
     * @param w     SurfaceView寬
     * @param h     SurfaceView高
     */
    private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
        final double ASPECT_TOLERANCE = 0.1;
        double targetRatio = (double) w / h;
        if (sizes == null)
            return null;
    
        Camera.Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;
    
        int targetHeight = h;
    
        // Try to find an size match aspect ratio and size
        for (Camera.Size size : sizes) {
            double ratio = (double) size.width / size.height;
            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
                continue;
            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }
    
        // Cannot find the one match the aspect ratio, ignore the requirement
        if (optimalSize == null) {
            minDiff = Double.MAX_VALUE;
            for (Camera.Size size : sizes) {
                if (Math.abs(size.height - targetHeight) < minDiff) {
                    optimalSize = size;
                    minDiff = Math.abs(size.height - targetHeight);
                }
            }
        }
        return optimalSize;
    }

照片保存方向的問題相對簡單一點葵萎,只需要給camera設(shè)置
setDisplayOrientation()就可以了。以下兩種方式均可以唱凯。

這個是官方的開源庫cameraview 提供的方式

    private int calcDisplayOrientation(int screenOrientationDegrees) {
        if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            return (360 - (mCameraInfo.orientation + screenOrientationDegrees) % 360) % 360;
        } else {  // back-facing
            return (mCameraInfo.orientation - screenOrientationDegrees + 360) % 360;
        }
    }

這個是根據(jù)open camera進(jìn)行相應(yīng)修改后的方式羡忘。


    /**
     * 照片方向
     */
    public static void onOrientationChanged(Activity activity, Camera.Parameters parameters, int cameraId) {
        Camera.CameraInfo info = new Camera.CameraInfo();
        Camera.getCameraInfo(cameraId, info);
        int camera_orientation = info.orientation;
        int result;
        int device_orientation = getDeviceDefaultOrientation(activity);
        if (device_orientation == Configuration.ORIENTATION_PORTRAIT) {
            // should be equivalent to onOrientationChanged(0)
            result = camera_orientation;
        } else {
            // should be equivalent to onOrientationChanged(90)
            if ((info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)) {
                result = (camera_orientation + 270) % 360;
            } else {
                result = (camera_orientation + 90) % 360;
            }
        }
        parameters.setRotation(result);
    }

關(guān)于Camera2 這個由于項目時間問題,目前沒有采用Camera2 相應(yīng)的API磕昼,官方開源庫這塊也是有點小問題的卷雕,在修改對應(yīng)的預(yù)覽比例也會出現(xiàn)失真,除了4:3的情況票从,因此本文沒有去分析camera2相應(yīng)的源碼漫雕。如果想要使用Camera2新特性的功能,那么建議可以去研究一下Jetpack 新出的CameraX
具體內(nèi)容請查看官方文檔代碼示例峰鄙。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浸间,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子吟榴,更是在濱河造成了極大的恐慌魁蒜,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異兜看,居然都是意外死亡锥咸,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門铣减,熙熙樓的掌柜王于貴愁眉苦臉地迎上來她君,“玉大人,你說我怎么就攤上這事葫哗〉奚玻” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵劣针,是天一觀的道長校镐。 經(jīng)常有香客問我,道長捺典,這世上最難降的妖魔是什么鸟廓? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮襟己,結(jié)果婚禮上引谜,老公的妹妹穿的比我還像新娘。我一直安慰自己擎浴,他們只是感情好员咽,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贮预,像睡著了一般贝室。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仿吞,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天滑频,我揣著相機(jī)與錄音,去河邊找鬼唤冈。 笑死峡迷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的你虹。 我是一名探鬼主播凉当,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼售葡!你這毒婦竟也來了看杭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤挟伙,失蹤者是張志新(化名)和其女友劉穎楼雹,沒想到半個月后模孩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡贮缅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年榨咐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谴供。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡块茁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出桂肌,到底是詐尸還是另有隱情数焊,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布崎场,位于F島的核電站佩耳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏谭跨。R本人自食惡果不足惜干厚,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望螃宙。 院中可真熱鬧蛮瞄,春花似錦、人聲如沸谆扎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽燕酷。三九已至籍凝,卻和暖如春周瞎,著一層夾襖步出監(jiān)牢的瞬間苗缩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工声诸, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留酱讶,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓彼乌,卻偏偏與公主長得像泻肯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子慰照,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355