安卓基礎(chǔ)知識——Camera的使用詳解(附demo)

一砚亭、前言

一般APP開發(fā)或多或少會涉及到相機相關(guān)功能,對應(yīng)一般的功能,調(diào)用系統(tǒng)的拍攝功能能滿足要求捅膘,但是如果需要自定義UI添祸,或希望在本APP內(nèi)完成,這就需要了解Camera的使用了篓跛。
本篇將先介紹Camera相關(guān)的知識點膝捞,然后結(jié)合單例例子總結(jié)如何自定義Camera坦刀,最后梳理Camera開發(fā)需要注意的問題愧沟。
通過本篇可以了解一下知識點:
1、什么是SurfaceView鲤遥,有什么作用沐寺?何為雙緩沖機制?
2盖奈、Camera與Camera2對比混坞,如何選擇?
3钢坦、相機涉及的方向的概念究孕,如何旋轉(zhuǎn)到正確的方向?
4爹凹、Camera常用的方法及屬性有哪些厨诸?
5、Camera的調(diào)用流程禾酱?
6微酬、需要哪些權(quán)限,怎樣動態(tài)申請颤陶?
7颗管、如何設(shè)置參數(shù),適配預(yù)覽區(qū)域大凶易摺垦江?
8、什么時機打開和關(guān)閉攝像頭搅方?
9比吭、如何切換前后攝像頭?

二腰懂、理解SurfaceView

1梗逮、為什么需要SurfaceView

安卓系統(tǒng)設(shè)定的刷新頻率是60FPS(Frame Per Second),即每隔16.6ms底層會發(fā)出VSYNC信號重繪界面绣溜。如果繪制過于復(fù)雜慷彤,無法保證60FPS,則會出現(xiàn)卡頓現(xiàn)象。View,ViewGroup底哗,Animator的代碼執(zhí)行全部是在主線程中完成的岁诉,如果執(zhí)行復(fù)雜邏輯,輕則容易出現(xiàn)卡頓跋选,嚴(yán)重則可能導(dǎo)致ANR涕癣。為了解決這個問題,引入了SurfaceView前标,主要用于游戲坠韩、視頻等視覺效果復(fù)雜、刷新頻率高的場景炼列。
SurfaceView的改進(jìn)在于引入了雙緩沖機制只搁,及多線程繪制。

2俭尖、雙緩沖機制

如果不用畫布氢惋,直接在窗口上繪制叫無緩沖繪圖;
如果只有一個畫布稽犁,先將所有內(nèi)容繪制到畫布上焰望,后一次性繪制到窗口叫單緩沖繪圖,畫布是一層緩沖區(qū)已亥;
如果用2個畫布熊赖,先在一個緩沖畫布上繪制所有圖像,繪制好后將內(nèi)容拷貝到正式繪制的畫布上陷猫,這是雙緩沖機制秫舌,拷貝比直接繪制效率要高。
在SurfaceView中绣檬,一般會開啟一個新線程足陨,然后在新線程的中通過SurfaceHolder的lockCanvas方法獲取到Canvas進(jìn)行繪制操作,繪制完以后再通過SurfaceHolder的unlockCanvasAndPost方法釋放canvas并提交更改娇未,下次刷新顯示新內(nèi)容墨缘。

3、SurfaceView零抬、SurfaceHolder镊讼、Surface三者關(guān)系

典型的MVC關(guān)系:
Surface:Model層,持有緩沖畫布Canvas和繪圖內(nèi)容相關(guān)的各種信息平夜;
SurfaceView:View層蝶棋,與用戶交互,負(fù)責(zé)將Surface的內(nèi)容展示給用戶忽妒;
SurfaceHolder:Controller層玩裙,通過SurfaceHolder控制Surface中的數(shù)據(jù)兼贸。

三、Camera開發(fā)相關(guān)知識

1吃溅、Camera與Camera2

Google從android 5.0開始推出的一套新相機接口Camera2溶诞,并摒棄了舊的接口Camera,從Camera到Camera2整套相機框架都發(fā)生了變化决侈,所以接口有很大的不同螺垢,Camera2解決了Camera寥寥無幾的接口和靈活性低的問題,給應(yīng)用層提供了更多控制權(quán)限赖歌,以構(gòu)建更高質(zhì)量的相機應(yīng)用枉圃。Camera2有很多Camera不支持的特性,如更加先進(jìn)的框架俏站,可以控制每一預(yù)覽幀的參數(shù)讯蒲,高速連拍,調(diào)整focus distance肄扎,控制曝光時間等。
不過很多應(yīng)用要支持5.0以下設(shè)備赁酝,所以很多時候依然使用Camera開發(fā)相機應(yīng)用犯祠。
本篇主要介紹Camera的應(yīng)用。
Camera2的應(yīng)用可以參考Camera2 系列教程《Android Camera2 教程 · 第一章 · 概覽》

2酌呆、相機方向

相機方向比較難理解衡载,容易搞混,所以這里專門介紹隙袁。
首先理解幾個方向的概念痰娱。
自然方向
人自然站立的方向。

設(shè)備方向
設(shè)備方向是指設(shè)備方向與自然方向的順時針夾角菩收,例如手機豎著拿正對屏幕是手機的自然方向梨睁,即設(shè)備方向為0°,如果手機橫著拿正對屏幕且頂部在右邊娜饵,則設(shè)備方向為90°坡贺,以此類推,而平板橫著拿正對屏幕是平板的自然方向箱舞,即0°遍坟。
獲取方向可以通過
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
也可以通過OrientationEventListener 監(jiān)聽旋轉(zhuǎn)。

攝像頭方向
手機Camera的圖像數(shù)據(jù)都是來自于攝像頭硬件的圖像傳感器(Image Sensor)晴股,攝像頭的方向取決于圖像傳感器的安裝方向愿伴。安裝之后,有一個默認(rèn)的取景方向电湘,且不會被改變隔节。但為什么手機旋轉(zhuǎn)預(yù)覽畫面也能跟著旋轉(zhuǎn)到正確的畫面(自然方向)万搔?是因為Android系統(tǒng)底層根據(jù)當(dāng)前手機屏幕的方向?qū)D像Sensor采集到的數(shù)據(jù)進(jìn)行了旋轉(zhuǎn)處理,然后后才送給顯示系統(tǒng)官帘。
手機后置攝像頭一般是橫屏安裝的瞬雹,
CameraInfo.orientation表示相機圖像的方向。它的值是相機圖像順時針旋轉(zhuǎn)到設(shè)備自然方向一致時的角度刽虹。

注意:不同機型的攝像頭方向可能不一致酗捌,并不是所有都是 90°,也有小部分是 0° 的涌哲,所以我們要通過 Camera.CameraInfo.orientation 去判斷方向胖缤,而不是假設(shè)所有設(shè)備的攝像頭傳感器方向都是 90°。

攝像頭方向.png

預(yù)覽方向
系統(tǒng)提供了接口設(shè)置預(yù)覽方向阀圾,setDisplayOrientation哪廓,默認(rèn)情況是0°,即預(yù)覽方向與攝像頭方向一致初烘,對于橫屏應(yīng)用涡真,不需要設(shè)置預(yù)覽方向。而對于豎屏應(yīng)用肾筐,則需要通過該接口將Camera的預(yù)覽方向旋轉(zhuǎn)90哆料,與手機屏幕方向一致,這樣才會得到正確的預(yù)覽畫面吗铐。

預(yù)覽方向.png

拍攝方向
相機采集圖像后需要進(jìn)行順時針旋轉(zhuǎn)的角度东亦,即相機屬性的orientation的值。當(dāng)點擊拍照時唬渗,得到的照片方向不一定與預(yù)覽方向一致典阵,因為通過setDisplayOrientation僅僅修改了預(yù)覽圖像的方向,不會影響到實際拍攝圖像的方向镊逝,需要修改拍攝圖像方向可以通過camera.setRotation實現(xiàn)壮啊。

拍攝方向.png

3、適配預(yù)覽區(qū)域大小

一般手機會提供多個預(yù)覽和拍照尺寸蹋半,通過接口getSupportedPreviewSizes和getSupportedPictureSizes可以獲得這些尺寸列表他巨。如果previewSize比例與預(yù)覽區(qū)(SurfaceView)比例不一致,則看到的預(yù)覽圖像會變形拉伸减江。如何適配不同預(yù)覽區(qū)大小解決拉伸問題染突,一般有2種方案:
一是根據(jù)previewSize比例修改SurfaceView的比例,調(diào)整預(yù)覽區(qū)比例(只調(diào)整寬或高)為預(yù)覽尺寸比例辈灼,從而使圖像不發(fā)生變形份企。
二是根據(jù)SurfaceView大小固定,然后根據(jù)其比例選擇最佳的(比例最接近的)預(yù)覽尺寸巡莹。

4司志、YUV/NV21

通過相機預(yù)覽拿到的圖像幀默認(rèn)是NV21格式的byte數(shù)組甜紫,Google支持的Camera Preview Callback的YUV常用格式有兩種:NV21 / YV12,至于YUV的理解可以參考YUV骂远。而NV21格式數(shù)據(jù)需要經(jīng)過特定轉(zhuǎn)化才能轉(zhuǎn)化為BitMap:

public Bitmap nv21ToBitmap(byte[] data){
      try{
             YuvImage image = new YuvImage(data, ImageFormat.NV21, w, h, null);
             ByteArrayOutputStream os = new ByteArrayOutputStream(mData.length);
             if(!image.compressToJpeg(new Rect(0, 0, w, h), 100, os)){
                   return null;
             }
             byte[] tmp = os.toByteArray();
             os.close();
             return BitmapFactory.decodeByteArray(tmp, 0,tmp.length); 
      }catch(Exception e){
      }
      return null;
}

四囚霸、 Camera方法及內(nèi)部類介紹

1、常用方法介紹

getNumberOfCameras():獲取攝像頭個數(shù)
getCameraInfo(int cameraId, Camera.CameraInfo cameraInfo):獲取相機信息
open(int cameraId):打開相機
setPreviewDisplay(SurfaceHolder holder):設(shè)置預(yù)覽方向
startPreview():開始預(yù)覽
stopPreview():停止預(yù)覽
setPreviewCallback(Camera.PreviewCallback cb):設(shè)置預(yù)覽回調(diào)激才,callback中可以得到二進(jìn)制預(yù)覽幀
setPreviewCallbackWithBuffer(Camera.PreviewCallback cb):setPreviewCallback每產(chǎn)生一幀都開辟一個新的buffer拓型,會導(dǎo)致頻繁GC,影響效率瘸恼,如果對效率要求比較高劣挫,則用setPreviewCallbackWithBuffer,指定緩沖區(qū)东帅,通過內(nèi)存復(fù)用提高效率
autoFocus(Camera.AutoFocusCallback cb):自動聚焦
takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg):拍攝照片
setParameters(Camera.Parameters params):設(shè)置相機參數(shù)
Camera.Parameters getParameters():獲取相機參數(shù)

2压固、內(nèi)部類

CameraInfo
facing:攝像頭的方向,包括前置和后置方向靠闭,可選值有 Camera.CameraInfo.CAMERA_FACING_BACK 和Camera.CameraInfo.CAMERA_FACING_FRONT帐我。
orientation:攝像頭畫面經(jīng)過多少度旋轉(zhuǎn)可變?yōu)樽匀环较颉?/p>

Parameters
通過LinkedHashMap<String, String>存儲相機的參數(shù)。
get(String key):從map中讀取指定key的參數(shù)
setPreviewSize(int width, int height):設(shè)置預(yù)覽尺寸
Camera.Size getPreviewSize():獲取預(yù)覽尺寸
List<Size> getSupportedPreviewSizes:獲取相機支持的預(yù)覽尺寸阎毅,不同機型支持的參數(shù)不同
List<Size> getSupportedVideoSizes():獲取錄制視頻的尺寸焚刚,不同機型支持的參數(shù)不同
setPictureSize(int width, int height):設(shè)置拍攝圖像尺寸,是拍攝后圖像扇调,不是預(yù)覽
List<Size> getSupportedPictureSizes():獲取相機支持的拍攝尺寸,不同機型支持的參數(shù)不同
setRotation(int rotation):設(shè)置拍攝返回圖像的方向抢肛,不是預(yù)覽方向

六狼钮、相機調(diào)用流程

因為相機設(shè)計的接口比較多,為了職責(zé)分明捡絮,將相機相關(guān)的邏輯封裝到CameraManager類熬芜,相機參數(shù)設(shè)置封裝到CameraConfigurationManager類,這里直接參考Zxing開源代碼中的Camera部分福稳,并稍作修改涎拉。

1、Acitivty部分

注冊權(quán)限

因為拍攝照片要保存到本地的圆,所以除了相機權(quán)限還需要存儲權(quán)限鼓拧。

 <!-- 操作本地文件 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <!-- 攝像頭使用 -->
    <uses-permission android:name="android.permission.CAMERA"/>

動態(tài)申請權(quán)限

對于target sdk 為23(安卓6.0)以上的應(yīng)用,運行中6.0以上系統(tǒng)越妈,需要動態(tài)申請權(quán)限季俩。在打開相機前先判斷系統(tǒng)版本,如果大于6.0則判斷是否已經(jīng)申請權(quán)限梅掠,沒有則申請酌住。申請權(quán)限如果客戶拒絕了則退出拍攝頁面(也可以在跳著拍攝頁面前申請店归,不過需要在所有打開拍攝頁面的地方都申請),如果客戶選擇了不再提示酪我,則彈出提示框消痛,引導(dǎo)客戶到系統(tǒng)設(shè)置中開啟權(quán)限,

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
           String[] deniList = checkPermissionsGranted(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA});
           if (deniList != null && deniList.length > 0) { //未授權(quán)
               requestPermissions(deniList, REQ_PERMISSION);
           } else {
               openCamera();
           }
       } else {
           openCamera();
       }
    @TargetApi(23)
    public String[] checkPermissionsGranted(String[] permissions) {
        List<String> deniList = new ArrayList<>();

        // 遍歷每一個申請的權(quán)限都哭,把沒有通過的權(quán)限放在集合中
        for (String permission : permissions) {
            if (checkSelfPermission(permission) !=
                    PackageManager.PERMISSION_GRANTED) {
                deniList.add(permission);
            }
        }
        return deniList.toArray(new String[deniList.size()]);
    }
 @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (grantResults.length > 0) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                openCamera();
            } else {
                boolean camera = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA);
                if (!camera) { // 判斷是否勾選了不再提醒秩伞,如果有勾選,提權(quán)限用途质涛,點擊確定跳轉(zhuǎn)到App設(shè)置頁面
                    AlertDialog.Builder builder = new AlertDialog.Builder(this);
                    mDialog = builder.setMessage("請在設(shè)置-應(yīng)用-xxx中稠歉,設(shè)置運行使用攝像頭權(quán)限").setPositiveButton("取消", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            finish();
                        }
                    }).setNegativeButton("去設(shè)置", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            goIntentSetting();
                        }
                    }).create();
                    mDialog.setCancelable(false);
                    mDialog.show();
                } else { //拒絕了權(quán)限
                    finish();
                }
            }
        }
    }

    /**
     * 應(yīng)用設(shè)置頁面
     */
    private void goIntentSetting() {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", getPackageName(), null);
        intent.setData(uri);
        try {
            intent.addCategory(Intent.CATEGORY_DEFAULT);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

布局定義SurfaceView并設(shè)置Callback

protected void onCreate(@Nullable Bundle savedInstanceState) {
       ...
       mSurfaceView = findViewById(R.id.surfaceView);
       mSurfaceView.getHolder().addCallback(this);
       ...
}

  @Override
    public void surfaceCreated(SurfaceHolder holder) {
    }

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

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }

打開相機并預(yù)覽

在surfaceCreated回調(diào)中,執(zhí)行判斷權(quán)限汇陆,確保有權(quán)限后再打開相機怒炸,打開相機加try-catch以防遇到異常程序crash,遇到異常則彈出提示毡代。

   private void openCamera() {
        try {
            //設(shè)置前置或后置攝像頭
            mCameraManager.setManualCameraId(mIsFront ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK);
            //打開攝像頭
            mCameraManager.openDriver(mSurfaceView.getHolder());
            //開始預(yù)覽
            mCameraManager.startPreview();
        } catch (Exception ioe) {
            //捕獲異常,提示并退出
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            mDialog = builder.setMessage("打開攝像頭失敗阅羹,請退出重試").setNegativeButton("確定", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    finish();
                }
            }).create();
            mDialog.setCancelable(false);
            mDialog.show();
        }
    }

拍照

mShoot = findViewById(R.id.shoot);//拍攝按鈕,點擊拍攝
mShoot.setOnClickListener(new View.OnClickListener() {
      @Override
       public void onClick(View view) {
                mCameraManager.getCamera().takePicture(null, null, new Camera.PictureCallback() {
                @Override
                 public void onPictureTaken(byte[] bytes, Camera camera) {
                                       Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                        Camera.CameraInfo info = mCameraManager.getCameraInfo();
                        bitmap = BitmapUtil.rotateAndMirrorBitmap(bitmap, info.orientation, info.facing == 
                        Camera.CameraInfo.CAMERA_FACING_FRONT);
                        mImgResult.setImageBitmap(bitmap);//展示照片
                        mImgResult.setVisibility(View.VISIBLE);
                        mLayoutOpe.setVisibility(View.VISIBLE);
                        mShoot.setVisibility(View.GONE);
                        mImgChange.setVisibility(View.GONE);
                        mSurfaceView.setVisibility(View.GONE);
                        mCameraManager.stopPreview();//停止預(yù)覽
                    }
                });
            }
        });

切換前后攝像頭

每次切換攝像頭都需要先將當(dāng)前攝像頭關(guān)閉教寂,然后設(shè)置并開啟新的攝像頭捏鱼。

//點擊切換按鈕
 mImgChange.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mCameraManager.closeDriver();//關(guān)閉當(dāng)前攝像頭
                mIsFront = !mIsFront;
                openCamera();//開啟新攝像頭
            }
        });

關(guān)閉攝像頭

每次打開攝像頭后都需要主動關(guān)閉攝像頭,不然攝像頭沒有解鎖酪耕,下次不能正常打開导梆。這里在surfaceDestroyed回調(diào)中關(guān)閉,也可以在activity的onpause方法執(zhí)行迂烁,當(dāng)打開新的頁面看尼、退出頁面或者藏后臺都會調(diào)用關(guān)閉。

  @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mCameraManager.closeDriver();
    }

2盟步、CameraManager類

打開攝像頭

    public synchronized void openDriver(SurfaceHolder holder)
            throws IOException {
        Camera theCamera = camera;

        if (theCamera == null) {
            //打開攝像頭
            if (requestedCameraId >= 0) {
                theCamera = OpenCameraInterface.open(requestedCameraId);
            } else {
                theCamera = OpenCameraInterface.open();
            }

            if (theCamera == null) {
                throw new IOException();
            }
            camera = theCamera;
        }
        //設(shè)置SurfaceHolder
        theCamera.setPreviewDisplay(holder);

        if (!initialized) {
            initialized = true;
            //初始化相機參數(shù)藏斩,根據(jù)surfaceview大小判斷最近預(yù)覽尺寸(只是判斷纱兑,沒有實際設(shè)置)
            configManager.initFromCameraParameters(theCamera, holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height());
            if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
                setManualFramingRect(requestedFramingRectWidth,
                        requestedFramingRectHeight);
                requestedFramingRectWidth = 0;
                requestedFramingRectHeight = 0;
            }
        }

        Camera.Parameters parameters = theCamera.getParameters();
        String parametersFlattened = parameters == null ? null : parameters
                .flatten(); // Save these, temporarily
        try {
            //設(shè)置相機參數(shù),預(yù)覽方向
            configManager.setDesiredCameraParameters(theCamera, requestedCameraId);
        } catch (RuntimeException re) {
            // Driver failed
            Log.w(TAG,
                    "Camera rejected parameters. Setting only minimal safe-mode parameters");
            Log.i(TAG, "Resetting to saved camera params: "
                    + parametersFlattened);
            // Reset:
            if (parametersFlattened != null) {
                parameters = theCamera.getParameters();
                parameters.unflatten(parametersFlattened);
                try {
                    theCamera.setParameters(parameters);
                    configManager.setDesiredCameraParameters(theCamera, requestedCameraId);
                } catch (RuntimeException re2) {
                    // Well, darn. Give up
                    Log.w(TAG,
                            "Camera rejected even safe-mode parameters! No configuration");
                }
            }
        }
    }

開始嘉裤、停止預(yù)覽

    /**
     * Asks the camera hardware to begin drawing preview frames to the screen.
     */
    public synchronized void startPreview() {
        Camera theCamera = camera;
        if (theCamera != null && !previewing) {
            theCamera.startPreview();
            previewing = true;
            autoFocusManager = new AutoFocusManager(camera);
        }
    }

    /**
     * Tells the camera to stop drawing preview frames.
     */
    public synchronized void stopPreview() {
        if (autoFocusManager != null) {
            autoFocusManager.stop();
            autoFocusManager = null;
        }
        if (camera != null && previewing) {
            camera.stopPreview();
            previewCallback.setHandler(null, 0);
            previewing = false;
        }
    }

3狮含、CameraConfigurationManager類

判斷相機最佳預(yù)覽尺寸

從支持的尺寸列表中選出與預(yù)覽界面大小比例最接近的

 public void initFromCameraParameters(Camera camera, int width, int height) {
        Camera.Parameters parameters = camera.getParameters();

        WindowManager manager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
        Display display = manager.getDefaultDisplay();
        screenResolution = new Point(display.getWidth(), display.getHeight());
        Log.d(TAG, "Screen resolution: " + screenResolution);

        Point screenResolutionForCamera = new Point();
        if (width == 0 || height == 0) {
            screenResolutionForCamera.x = screenResolution.x;
            screenResolutionForCamera.y = screenResolution.y;
        } else {
            screenResolutionForCamera.x = width;
            screenResolutionForCamera.y = height;
        }

        if (screenResolution.x < screenResolution.y) {
            screenResolutionForCamera.x = screenResolution.y;
            screenResolutionForCamera.y = screenResolution.x;
        }

        previewResolution = getPreviewResolution(parameters, screenResolutionForCamera);
        pictureResolution = getPictureResolution(parameters, screenResolutionForCamera);
 }

獲取最近預(yù)覽尺寸:

private static Point findBestSizeValue(CharSequence sizeValueString, Point screenResolution) {
        int bestX = 0;
        int bestY = 0;
        int diff = Integer.MAX_VALUE;
        for (String previewSize : COMMA_PATTERN.split(sizeValueString)) {

            previewSize = previewSize.trim();
            int dimPosition = previewSize.indexOf('x');
            if (dimPosition < 0) {
                Log.w(TAG, "Bad preview-size: " + previewSize);
                continue;
            }

            int newX;
            int newY;
            try {
                newX = Integer.parseInt(previewSize.substring(0, dimPosition));
                newY = Integer.parseInt(previewSize.substring(dimPosition + 1));
            } catch (NumberFormatException nfe) {
                Log.w(TAG, "Bad preview-size: " + previewSize);
                continue;
            }

            int newDiff = Math.abs(newX - screenResolution.x) + Math.abs(newY - screenResolution.y);
            if (newDiff == 0) {
                bestX = newX;
                bestY = newY;
                break;
            } else if (newDiff < diff) {
                bestX = newX;
                bestY = newY;
                diff = newDiff;
            }

        }

        if (bestX > 0 && bestY > 0) {
            return new Point(bestX, bestY);
        }
        return null;
    }

獲取最佳拍攝尺寸:

  private Point getPictureResolution(Camera.Parameters parameters, Point screenSize) {
        String pictureSizeValueString = parameters.get("picture-size-values");
        // saw this on Xperia
        if (pictureSizeValueString == null) {
            pictureSizeValueString = parameters.get("picture-size-value");
        }

        Point pictureSize = null;

        if (pictureSizeValueString != null) {
            pictureSize = findBestSizeValue(pictureSizeValueString, screenSize);
        }

        if (pictureSize == null) {
            // Ensure that the camera resolution is a multiple of 8, as the screen may not be.
            pictureSize = new Point(
                    (screenSize.x >> 3) << 3,
                    (screenSize.y >> 3) << 3);
        }

        return pictureSize;
    }

判斷最佳尺寸:比例最接近的

 private static Point findBestSizeValue(CharSequence sizeValueString, Point screenResolution) {
        int bestX = 0;
        int bestY = 0;
        int diff = Integer.MAX_VALUE;
        for (String previewSize : COMMA_PATTERN.split(sizeValueString)) {

            previewSize = previewSize.trim();
            int dimPosition = previewSize.indexOf('x');
            if (dimPosition < 0) {
                Log.w(TAG, "Bad preview-size: " + previewSize);
                continue;
            }

            int newX;
            int newY;
            try {
                newX = Integer.parseInt(previewSize.substring(0, dimPosition));
                newY = Integer.parseInt(previewSize.substring(dimPosition + 1));
            } catch (NumberFormatException nfe) {
                Log.w(TAG, "Bad preview-size: " + previewSize);
                continue;
            }

            int newDiff = Math.abs(newX - screenResolution.x) + Math.abs(newY - screenResolution.y);
            if (newDiff == 0) {
                bestX = newX;
                bestY = newY;
                break;
            } else if (newDiff < diff) {
                bestX = newX;
                bestY = newY;
                diff = newDiff;
            }

        }

        if (bestX > 0 && bestY > 0) {
            return new Point(bestX, bestY);
        }
        return null;
  }

設(shè)置相機參數(shù)

設(shè)置預(yù)覽尺寸蜂科、拍攝尺寸科贬、焦距蒙秒、預(yù)覽方向

  public void setDesiredCameraParameters(Camera camera, int cameraId) {
        Camera.Parameters parameters = camera.getParameters();
        Log.d(TAG, "Setting preview size: " + previewResolution);
        parameters.setPreviewSize(previewResolution.x, previewResolution.y);
        parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
        setZoom(parameters);
        setCameraDisplayOrientation(camera, cameraId);
        camera.setParameters(parameters);
   }

設(shè)置預(yù)覽方向

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

旋轉(zhuǎn)照片

對于拍攝照片苔埋,一般可以對拍攝后照片根據(jù)相機角度(orientation)進(jìn)行旋轉(zhuǎn)到自然方向贱迟,特別地旬陡,對于前置攝像頭的照片拓颓,需要做鏡像對換。代碼如下:

  public static Bitmap rotateAndMirrorBitmap(Bitmap bm, int degree, boolean needMirror) {
        Bitmap newBm = null;
        Matrix matrix = new Matrix();
        matrix.postRotate(degree);
        if (needMirror) {
            matrix.postScale(-1, 1); // 鏡像水平翻轉(zhuǎn)
        }
        try {
            // 將原始圖片按照旋轉(zhuǎn)矩陣進(jìn)行旋轉(zhuǎn)描孟,并得到新的圖片
            newBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(),
                    bm.getHeight(), matrix, true);
        } catch (OutOfMemoryError e) {
        }
        if (newBm == null) {
            newBm = bm;
        }
        if (bm != newBm) {
            bm.recycle();
        }
        return newBm;
    }

七驶睦、測試效果

下面通過實驗測試各種設(shè)置的圖像效果砰左,測試機:華為P10。

1场航、預(yù)覽方向

如果不設(shè)置預(yù)覽方向:

    public void setDesiredCameraParameters(Camera camera, int cameraId) {
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPreviewSize(previewResolution.x, previewResolution.y);
        parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
        setZoom(parameters);
//        setCameraDisplayOrientation(camera, cameraId);
        camera.setParameters(parameters);
    }

如果Activity可隨手機方向旋轉(zhuǎn)的情況:


預(yù)覽效果(不設(shè)置預(yù)覽方向-后置-界面可旋轉(zhuǎn)).jpg

如果Activity不可隨手機方向旋轉(zhuǎn)的情況:


預(yù)覽效果(不設(shè)置預(yù)覽方向-后置-界面不旋轉(zhuǎn)).jpg

可以看到缠导,如果是橫屏應(yīng)用(Acitivity方向是橫向),預(yù)覽方向是自然方向溉痢,不需要旋轉(zhuǎn)僻造,如果是豎屏應(yīng)用(Acitivity豎直方向),預(yù)覽圖像需要旋轉(zhuǎn)90°孩饼。
前置攝像頭同樣效果髓削。

設(shè)置預(yù)覽方向后

無論Acitivity是否可隨手機方向旋轉(zhuǎn),預(yù)覽圖像都是自然方向镀娶。前置攝像頭也是自然方向立膛,但圖形是鏡像圖,左右對換的梯码,跟照鏡子一樣宝泵。


預(yù)覽效果(設(shè)置預(yù)覽方向- 后置-可旋轉(zhuǎn)).jpg

預(yù)覽效果(設(shè)置預(yù)覽方向-前置-可旋轉(zhuǎn)).jpg

2、預(yù)覽尺寸

如果不設(shè)置預(yù)覽尺寸:

    public void setDesiredCameraParameters(Camera camera, int cameraId) {
        Camera.Parameters parameters = camera.getParameters();
//        parameters.setPreviewSize(previewResolution.x, previewResolution.y);
        parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
        setZoom(parameters);
        setCameraDisplayOrientation(camera, cameraId);
        camera.setParameters(parameters);
    }

如果預(yù)覽界面大小是全屏轩娶,預(yù)覽尺寸沒有變形儿奶,說明相機默認(rèn)預(yù)覽尺寸比例與手機寬高比例一致的。
修改一下預(yù)覽界面大小鳄抒,測試一下預(yù)覽圖像效果闯捎。


預(yù)覽效果(不設(shè)置預(yù)覽尺寸).jpg

設(shè)置最佳預(yù)覽尺寸后

預(yù)覽效果(設(shè)置預(yù)覽尺寸).jpg

可以發(fā)現(xiàn),如果不設(shè)置最佳預(yù)覽尺寸许溅,預(yù)覽圖像可能會嚴(yán)重變形隙券,測試中橫屏特別明顯,變形程度與預(yù)覽寬高比例與SurfaceView寬高比例差異大小有關(guān)闹司。

拍照方向

不設(shè)置拍照方向

如果不設(shè)置拍照方向,對于后置攝像頭沐飘,無論界面是否可以旋轉(zhuǎn)的效果:


拍照效果(后置-不設(shè)置拍照方向).jpg

可以看到游桩,對于橫屏拍攝,拍出的照片都是自然方向的耐朴,而對于豎屏拍攝借卧,拍出來的照片需要旋轉(zhuǎn)后90°才是自然方向。(一般手機是旋轉(zhuǎn)90°筛峭,但也有少數(shù)是270°铐刘,如Nexus 5X)。

再看下前置攝像頭的效果:


拍攝效果(前置-不設(shè)置拍照方向).jpg

界面不隨手機旋轉(zhuǎn)的效果同樣影晓。
可以看到镰吵,橫屏拍攝是自然方向檩禾,豎屏拍照,照片需要旋轉(zhuǎn)270°才是自然方向(一般手機是旋轉(zhuǎn)270°疤祭,但也有少數(shù)是90°)

對拍攝照片旋轉(zhuǎn)后

無論前后攝像頭盼产,無論橫屏還是豎屏,拍照后需要根據(jù)相機方向調(diào)整照片到自然方向勺馆,另外戏售,前置攝像頭調(diào)整到自然方向發(fā)現(xiàn)跟預(yù)覽圖像剛好左右相反的,正常應(yīng)該跟預(yù)覽草穆,或者照鏡子看到的一致灌灾,所以還需要做鏡像對換。鏡像對換前(左)和鏡像對換后(右)的效果如下:


拍攝效果(前置-鏡像對換).jpg

八悲柱、其他問題

1锋喜、預(yù)覽模糊/拍照照片模糊
設(shè)置的previewSize和pictureSize太小,或者沒有對焦诗祸。
2跑芳、預(yù)覽/拍攝照片已經(jīng)對焦,但光線很暗
可能跟預(yù)覽分辨率有關(guān)直颅,調(diào)整試下博个。
如測試過程發(fā)現(xiàn),Honor 8C(EMUI 8.2.0功偿,android8.1.0) 這款手機盆佣,預(yù)覽分別是1280*720的時候,預(yù)覽會很暗械荷。

九共耍、總結(jié)

1、SurfaceView相比一般的View改進(jìn)在于引入了雙緩沖機制吨瞎,多線程繪制痹兜,主要用于視頻、游戲等視覺復(fù)雜颤诀、刷新頻率高的場景字旭;
2、雙緩沖機制簡單來說就是使用多個畫布崖叫,在新線程繪制到緩沖畫布上遗淳,然后直接拷貝到正式繪制畫布上,實現(xiàn)高效繪制顯示心傀;
3屈暗、Surface、SurfaceView、SurfaceHolder三者是典型的MVC關(guān)系养叛;
4种呐、Camera2支持更多的相機特性,但不支持5.0以下手機一铅,為了兼容低版本陕贮,很多應(yīng)用依然使用Camera1;
5潘飘、攝像頭一般是橫屏安裝的肮之,決定了取景方向,且不會隨設(shè)備改變方向卜录。CameraInfo.orientation表示相機圖像的方向戈擒,表示相機圖像順時針旋轉(zhuǎn)到設(shè)備自然方向一致時的角度,一般是90°艰毒;
6筐高、camera.setDisplayOrientation可以設(shè)置預(yù)覽方向,但不會影響拍攝照片的方向丑瞧,橫屏應(yīng)用不需要設(shè)置柑土,豎屏需要;
7绊汹、camera.setRotation可以設(shè)置拍攝照片的方向稽屏,另一種方法是根據(jù)CameraInfo.orientation對拍攝后的照片進(jìn)行旋轉(zhuǎn)到自然方向;
8西乖、為了適配不同預(yù)覽區(qū)域大小狐榔、解決預(yù)覽拉伸問題,一種方案是從相機支持的預(yù)覽尺寸中選出寬高比例最接近預(yù)覽區(qū)域比例的尺寸获雕;另外一種是相機預(yù)覽尺寸不變薄腻,調(diào)整預(yù)覽區(qū)域?qū)捇蚋叩囊贿叀?br> 9、預(yù)覽返回的圖像是NV21格式的届案,需要轉(zhuǎn)換才能變?yōu)锽itMap庵楷。

文中demo:STakePicture

參考

Android自定義View之雙緩沖機制和SurfaceView
Android: Camera相機開發(fā)詳解(上) —— 知識儲備

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市楣颠,隨后出現(xiàn)的幾起案子嫁乘,更是在濱河造成了極大的恐慌,老刑警劉巖球碉,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異仓蛆,居然都是意外死亡睁冬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來豆拨,“玉大人直奋,你說我怎么就攤上這事∈┖蹋” “怎么了脚线?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長弥搞。 經(jīng)常有香客問我邮绿,道長,這世上最難降的妖魔是什么攀例? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任船逮,我火速辦了婚禮,結(jié)果婚禮上粤铭,老公的妹妹穿的比我還像新娘挖胃。我一直安慰自己,他們只是感情好梆惯,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布酱鸭。 她就那樣靜靜地躺著,像睡著了一般垛吗。 火紅的嫁衣襯著肌膚如雪凹髓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天职烧,我揣著相機與錄音扁誓,去河邊找鬼。 笑死蚀之,一個胖子當(dāng)著我的面吹牛蝗敢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播足删,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼寿谴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了失受?” 一聲冷哼從身側(cè)響起讶泰,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拂到,沒想到半個月后痪署,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡兄旬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年狼犯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡悯森,死狀恐怖宋舷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瓢姻,我是刑警寧澤祝蝠,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站幻碱,受9級特大地震影響绎狭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜收班,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一坟岔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧摔桦,春花似錦社付、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至兄世,卻和暖如春啼辣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背御滩。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工鸥拧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人削解。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓富弦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親氛驮。 傳聞我的和親對象是個殘疾皇子腕柜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容