看到有些讀者對博客提出的疑問,在此真誠說一句實在抱歉,本人是一名在校非科班出身大學生,之前 由于是期末季,所以非常忙,沒有空更新,一月二十幾號才放假,非常抱歉,評論所訴內(nèi)容應該都已解決,只是我沒有更新,近期將會更新!再次致歉!
前言:原有的android.hardware.camera已經(jīng)被google廢除,改用功能更強大的camera2,camera2的復雜度遠遠超過camera,所以寫這篇博客之前也是經(jīng)歷了各種心態(tài)爆炸。非常費解,谷歌官網(wǎng)竟然沒有給出一個好的demo,導致我只能去網(wǎng)上各處搜博客,其間大多數(shù)博客真的讓我惱火,不是寫的有問題,就是介紹不全,還有就是完全不解釋,我很討厭這種不負責的博客,對我來說,簡直就是扯淡!(實在忍不住,所以爆了點粗口,望諒解)。出于以上原因,我會盡我最大努力,寫出一篇邏輯清晰,易于理解,代碼完整的博客。
一,讓我們先來了解一下其大致原理和使用流程
先看兩張圖:
CameraManager:管理Camera的類,通過這個類我們可以打開Camera——對應方法為CameraManager.openCamera;獲取各種Camera參數(shù)——對應方法為CameraManager.getCameraCharacteristics。這里我們先大概了解就行,稍后都會介紹。
其中打開一個Camera時,會有對應的回調(diào)接口——CameraDevice.StateCallback,處理相機打開失敗,成功后者錯誤的情況。CameraCharacteristics:含有Camera的各種參數(shù),如閃光燈,自動對焦,自動曝光等等。
CameraDevice:java代碼中代表Camera的對象,可以關閉相機,向相機硬件端發(fā)出請求等等。
CameraCaptureSession:session直譯為”會議“,這是一個很形象的詞,其實就是上文提到的pipeline,程序中通過創(chuàng)造一個CameraCaptureSession,在安卓端和相機硬件端建立管道,從而可以獲取拍攝的圖片信息。
在創(chuàng)造一個會議時,會回調(diào)兩個接口——
StateCallback:處理session建立成功和失敗的情況,通常在這里會進行預覽的一些初始化設置。
CaptureCallback:捕獲圖像成功、失敗、進行時等情況的處理。CameraRequest:安卓端相機參數(shù)的設定請求,會在創(chuàng)建session時被當作參數(shù)。
CameraMetadata:控制相機和帶有相機參數(shù)的基礎類,它的子類是:
CameraCharacteristics,CaptureRequest,CaptureResult。CaptureResult:從圖像傳感器捕獲單個圖像的結(jié)果的子集。包含捕獲硬件(傳感器,鏡頭,閃存),處理流水線,控制算法和輸出緩沖區(qū)的最終配置的一個子集。概念有點難懂,可以去官網(wǎng)仔細了解下。
compile 'de.hdodenhof:circleimageview:2.1.0'
這是一個具有將任何形狀圖片變?yōu)閳A形功能的開源View,圖片中左下角的圓形圖片就是調(diào)用了這個控件。
在AndroidManifest.xml添加相關權(quán)限:
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
這里的讀取和讀入功能以后擴充的代碼要用到。
(二)為你的相機創(chuàng)建個性布局
activity_main.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <SurfaceView android:id="@+id/mFirstSurfaceView" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="100dp" android:background="#0E0E0E"> <!--提供預覽功能,以及從相冊選取圖片進行識別功能--> <de.hdodenhof.circleimageview.CircleImageView android:id="@+id/img_show" android:layout_width="60dp" android:layout_height="60dp" android:layout_alignParentStart="true" android:layout_alignParentBottom="true" android:src="@mipmap/ic_launcher" android:layout_marginStart="10dp"/> <Button android:id="@+id/take_picture" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:layout_alignParentBottom="true" android:text="拍照"/> <!--點擊觀看識別結(jié)果--> <Button android:id="@+id/recognition_result" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_alignParentBottom="true" android:text="結(jié)果" android:layout_marginEnd="10dp"/> </RelativeLayout></LinearLayout>
其中SurfaceView是用來預覽拍攝圖片的。CircleImageView是我們第一步提到的開源庫。
(三)在MainActivity中實現(xiàn)相關功能
老規(guī)矩,先貼整體代碼:
MainActivity.class
package com.example.wordrecognition;import android.annotation.TargetApi;import android.content.Context;import android.content.pm.PackageManager;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.Camera;import android.graphics.ImageFormat;import android.hardware.camera2.*;import android.media.Image;import android.media.ImageReader;import android.os.Handler;import android.os.HandlerThread;import android.support.annotation.IntDef;import android.support.annotation.NonNull;import android.support.v4.app.ActivityCompat;import android.support.v7.app.ActionBar;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.Surface;import android.view.SurfaceHolder;import android.view.SurfaceView;import android.view.View;import android.widget.Button;import android.widget.ImageView;import android.widget.Toast;import android.Manifest.permission.*;import java.nio.ByteBuffer;import java.util.Arrays;import java.util.jar.Manifest;public class MainActivity extends AppCompatActivity { private CameraManager mCameraManager; private SurfaceView mSurfaceView; private SurfaceHolder mSurfaceViewHolder; private Handler mHandler; private String mCameraId; private ImageReader mImageReader; private CameraDevice mCameraDevice; private CaptureRequest.Builder mPreviewBuilder; private CameraCaptureSession mSession; private ImageView img_show; private Button take_picture_bt; private Handler mainHandler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ActionBar actionBar=getSupportActionBar(); if(actionBar!=null){ actionBar.hide(); } img_show= (ImageView) findViewById(R.id.img_show); take_picture_bt=(Button)findViewById(R.id.take_picture); initSurfaceView();//初始化SurfaceView /** * 拍照按鈕監(jiān)聽 */ take_picture_bt.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { takePicture(); } }); }public void initSurfaceView(){ mSurfaceView = (SurfaceView) findViewById(R.id.mFirstSurfaceView); mSurfaceViewHolder = mSurfaceView.getHolder();//通過SurfaceViewHolder可以對SurfaceView進行管理 mSurfaceViewHolder.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { initCameraAndPreview(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { //釋放camera if (mCameraDevice != null) { mCameraDevice.close(); mCameraDevice = null; } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } });} @TargetApi(19) public void initCameraAndPreview() { HandlerThread handlerThread = new HandlerThread("My First Camera2"); handlerThread.start(); mHandler = new Handler(handlerThread.getLooper()); mainHandler = new Handler(getMainLooper());//用來處理ui線程的handler,即ui線程 try { mCameraId = "" + CameraCharacteristics.LENS_FACING_FRONT; mImageReader = ImageReader.newInstance(mSurfaceView.getWidth(), mSurfaceView.getHeight(), ImageFormat.JPEG,/*maxImages*/7); mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);//這里必須傳入mainHandler,因為涉及到了Ui操作 mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE); if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { return;//按理說這里應該有一個申請權(quán)限的過程,但為了使程序盡可能最簡化,所以先不添加 } mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler); } catch (CameraAccessException e) { Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show(); } } private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { //進行相片存儲 mCameraDevice.close(); Image image = reader.acquireNextImage(); ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes);//將image對象轉(zhuǎn)化為byte,再轉(zhuǎn)化為bitmap final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); if (bitmap != null) { img_show.setImageBitmap(bitmap); } } }; private CameraDevice.StateCallback deviceStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { mCameraDevice = camera; try { takePreview(); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onDisconnected(@NonNull CameraDevice camera) { if (mCameraDevice != null) { mCameraDevice.close(); mCameraDevice = null; } } @Override public void onError(CameraDevice camera, int error) { Toast.makeText(MainActivity.this, "打開攝像頭失敗", Toast.LENGTH_SHORT).show(); } }; public void takePreview() throws CameraAccessException { mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface()); mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler); } private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { mSession = session; //配置完畢開始預覽 try { /** * 設置你需要配置的參數(shù) */ //自動對焦 mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); //打開閃光燈 mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); //無限次的重復獲取圖像 mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { Toast.makeText(MainActivity.this, "配置失敗", Toast.LENGTH_SHORT).show(); } }; private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { mSession = session; } @Override public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) { mSession = session; } @Override public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) { super.onCaptureFailed(session, request, failure); } }; public void takePicture() { try { CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//用來設置拍照請求的request captureRequestBuilder.addTarget(mImageReader.getSurface()); // 自動對焦 captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); // 自動曝光 captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); int rotation = getWindowManager().getDefaultDisplay().getRotation(); CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId); captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(cameraCharacteristics, rotation));//使圖片做順時針旋轉(zhuǎn) CaptureRequest mCaptureRequest = captureRequestBuilder.build(); mSession.capture(mCaptureRequest, null, mHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }//獲取圖片應該旋轉(zhuǎn)的角度,使圖片豎直 public int getOrientation(int rotation) { switch (rotation) { case Surface.ROTATION_0: return 90; case Surface.ROTATION_90: return 0; case Surface.ROTATION_180: return 270; case Surface.ROTATION_270: return 180; default: return 0; } } //獲取圖片應該旋轉(zhuǎn)的角度,使圖片豎直 private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) { if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return 0; int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION); // Round device orientation to a multiple of 90 deviceOrientation = (deviceOrientation + 45) / 90 * 90; // LENS_FACING相對于設備屏幕的方向,LENS_FACING_FRONT相機設備面向與設備屏幕相同的方向 boolean facingFront = c.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT; if (facingFront) deviceOrientation = -deviceOrientation; // Calculate desired JPEG orientation relative to camera orientation to make // the image upright relative to the device orientation int jpegOrientation = (sensorOrientation + deviceOrientation + 360) % 360; return jpegOrientation; }}
代碼有點長,下面我們來分解它的邏輯
1,隱藏ActionBar等以及基本部件初始化操作
ActionBar actionBar=getSupportActionBar(); if(actionBar!=null){ actionBar.hide(); } img_show= (ImageView) findViewById(R.id.img_show); take_picture_bt=(Button)findViewById(R.id.take_picture);
2,初始化surfaceView,即照相機預覽界面
mSurfaceView = (SurfaceView) findViewById(R.id.mFirstSurfaceView); mSurfaceViewHolder = mSurfaceView.getHolder();//通過SurfaceViewHolder可以對SurfaceView進行管理 mSurfaceViewHolder.addCallback(new SurfaceHolder.Callback() { //SurfaceView被成功創(chuàng)建 @Override public void surfaceCreated(SurfaceHolder holder) { initCameraAndPreview(); }//SurfaceView被銷毀 @Override public void surfaceDestroyed(SurfaceHolder holder) { //釋放camera if (mCameraDevice != null) { mCameraDevice.close(); mCameraDevice = null; } }//SurfaceView內(nèi)容發(fā)生改變 @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } });
首先我們獲取到了SurfaceView實例,然后通過SurfaceView獲取SurfaceViewHolder,SurfaceViewHolder是用來管理SurfaceView的類,別的類通過SurfaceViewHolder可以對SurfaceView進行編輯。
然后我們通過SurfaceViewHolder為SurfaceView添加回調(diào)接口,三個回調(diào)接口方法的功能注釋已經(jīng)解釋了。在第二個方法中,釋放了Camera資源。
而在第一個方法中,SurfaceView被成功創(chuàng)建后,我們緊接著調(diào)用initCameraAndPreView,初始化Camera和將相機拍攝界面投射到SurfaceView上。
3,初始化相機和實現(xiàn)相機預覽功能
@TargetApi(19) public void initCameraAndPreview() { HandlerThread handlerThread = new HandlerThread("My First Camera2"); handlerThread.start(); mHandler = new Handler(handlerThread.getLooper()); mainHandler = new Handler(getMainLooper());//用來處理ui線程的handler,即ui線程 try { mCameraId = "" + CameraCharacteristics.LENS_FACING_FRONT; mImageReader = ImageReader.newInstance(mSurfaceView.getWidth(), mSurfaceView.getHeight(), ImageFormat.JPEG,/*maxImages*/7); mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);//這里必須傳入mainHandler,因為涉及到了Ui操作 mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE); if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { return;//按理說這里應該有一個申請權(quán)限的過程,但為了使程序盡可能最簡化,所以先不添加 } mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler); } catch (CameraAccessException e) { Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show(); } } private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { //進行相片存儲 mCameraDevice.close(); //mSurfaceView.setVisibility(View.GONE);//存疑 Image image = reader.acquireNextImage(); ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes);//將image對象轉(zhuǎn)化為byte,再轉(zhuǎn)化為bitmap final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); if (bitmap != null) { img_show.setImageBitmap(bitmap); } } };
在這步中首先我們初始化了兩個線程:
HandlerThread handlerThread = new HandlerThread("My First Camera2"); handlerThread.start(); mHandler = new Handler(handlerThread.getLooper()); mainHandler = new Handler(getMainLooper());//用來處理ui線程的handler,即ui線程
關于HandlerThread不熟悉的讀者可以看下鴻洋大神的博客——HandlerThread.
mHandler用來處理普通線程,mainHandler用來處理主線程——ui線程。
然后我們設置了CameraId:
前置攝像頭:LENS_FACING_BACK,名字不要弄反了。
后置攝像頭:LENS_FACING_FRONT。
設置ImageReader,用來讀取拍攝圖像的類,ImageReader.newInstance方法的原型:
ImageReader.newInstance(int width,int height,int format,int maxImages);其它參數(shù)好理解,maxImages代表用戶想讀取的最大Image對象數(shù)量。
然后我們?yōu)镮mageReader設置了監(jiān)聽接口:
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);//這里必須傳入mainHandler,因為涉及到了Ui操作
mOnImageAvailiableListener對象如下:
private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { //進行相片存儲和展示 mCameraDevice.close(); Image image = reader.acquireNextImage(); ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes);//將image對象轉(zhuǎn)化為byte,再轉(zhuǎn)化為bitmap final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); if (bitmap != null) { img_show.setImageBitmap(bitmap); } } };
這個接口在圖片拍攝完成后就會回調(diào),這個方法中我們關閉了相機,并進行了圖片的讀取,其中buffer.remaining返回buffer的元素數(shù)量,image.getPlanes()代表獲取圖片像素信息組成的數(shù)組。
同時還要注意到,我們設置監(jiān)聽接口時還傳入了mainHandler,這代表我們可以在回調(diào)方法中進行ui操作。
緊接著獲取CameraManager,并打開了Camera:
mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE); if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { return;//按理說這里應該有一個申請權(quán)限的過程,但為了使程序盡可能最簡化,所以先不添加 } mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler); } catch (CameraAccessException e) { Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show(); }
注意打開Camera之前一定要動態(tài)申請權(quán)限,這里暫時還沒寫,運行時我是直接在手機打開相關權(quán)限的。否則無法發(fā)運行!?。?/strong>
在打開Camera時傳入了CameraDevice.StateCallback,用來反饋相機工作狀態(tài):
private CameraDevice.StateCallback deviceStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { mCameraDevice = camera; try { takePreview(); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onDisconnected(@NonNull CameraDevice camera) { if (mCameraDevice != null) { mCameraDevice.close(); mCameraDevice = null; } } @Override public void onError(CameraDevice camera, int error) { Toast.makeText(MainActivity.this, "打開攝像頭失敗", Toast.LENGTH_SHORT).show(); } };
相信聰明的讀者從字面意思就可以理解代碼的意義了,就不細說了。
4,顯示預覽界面,進行正式預覽
在成功打開相機后,回調(diào)onOpened方法,調(diào)用takePreview()方法,進行正式的預覽,經(jīng)過這步,我們就可以實時看到拍攝的畫面。
public void takePreview() throws CameraAccessException { mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface()); mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler); } private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { mSession = session; //配置完畢開始預覽 try { /** * 設置你需要配置的參數(shù) */ //自動對焦 mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); //打開閃光燈 mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); //無限次的重復獲取圖像 mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { Toast.makeText(MainActivity.this, "配置失敗", Toast.LENGTH_SHORT).show(); } }; private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { mSession = session; } @Override public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) { mSession = session; } @Override public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) { super.onCaptureFailed(session, request, failure); } };
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewBuilder是前文講過的CaptureRequest的Builder(Builder設計模式),用來對CaptureRequest進行編輯。這里有個參數(shù)——CameraDevice.TEMPLATE_PREVIEW:看名字可以猜到,這是表明這個request是針對于相機預覽界面的。
mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface());
將request設置的參數(shù)數(shù)據(jù)應用于SurfaceView對應的surface,request設置的參數(shù)形成的圖像數(shù)據(jù)都會保存在SurfaceView的surface中。
來簡單介紹下Surface:
應用產(chǎn)生的圖像數(shù)據(jù)保存的地方,SurfaceView和ImageReader都有一個對應的surface。
mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler);
這個在前文也提到過,在安卓端和相機硬件端建立通道,進行信息的交換。
我們來看看這個方法的原型:
createCaptureSession(List<Surface> outputs, CameraCaptureSession.StateCallback callback, Handler handler)
只有第一個參數(shù)有點難理解,它是在聲明相機拍攝圖像數(shù)據(jù)的存儲點——Surface對象。
再回到我們程序中,第一個參數(shù)是:
Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()
即是SurfaceView和ImageReader的Surface組成的一個數(shù)組,表明圖像數(shù)據(jù)將會輸出到這兩個地方。
再看mSessionPreviewStateCallback:
private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { mSession = session; //配置完畢開始預覽 try { /** * 設置你需要配置的參數(shù) */ //自動對焦 mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); //打開閃光燈 mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); //無限次的重復獲取圖像 mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { Toast.makeText(MainActivity.this, "配置失敗", Toast.LENGTH_SHORT).show(); } };
這是創(chuàng)建session的一個回調(diào),前文也提到過。
方法中分別對session建立成功和失敗的情況進行了處理,成功后,設置了一系列參數(shù),最后調(diào)用:
//無限次的重復獲取圖像 mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
這個很好理解,預覽的過程其實就是不斷重復請求圖像數(shù)據(jù)的過程,所以叫”repeating”。
其原型為:
setRepeatingRequest(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)
注意第二個參數(shù),雖然我們目前的代碼設置為null沒有用到,但是還是給出了這個回調(diào)接口的代碼:
private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() { //拍攝完全完成并且成功,拍攝圖像數(shù)據(jù)可用時回調(diào) @Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { mSession = session; }//拍攝進行中,但拍攝圖像數(shù)據(jù)部分可用時回調(diào) @Override public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) { mSession = session; } }; //拍攝失敗時回調(diào) @Override public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) { super.onCaptureFailed(session, request, failure); }
我們可以在這個回調(diào)做圖片的保存工作,但當然還是出于最簡化原則,暫時不寫。
5,一番辛苦,我們終于可以拍照片了!??!
public void takePicture() { try { CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//用來設置拍照請求的request captureRequestBuilder.addTarget(mImageReader.getSurface()); // 自動對焦 captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); // 自動曝光 captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); int rotation = getWindowManager().getDefaultDisplay().getRotation(); CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId); captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(cameraCharacteristics, rotation));//使圖片做順時針旋轉(zhuǎn) //captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,getOrientaion(rotation))兩種方法都可以。 CaptureRequest mCaptureRequest = captureRequestBuilder.build(); mSession.capture(mCaptureRequest, null, mHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }
CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//用來設置拍照請求的request
這個之前講過,只是參數(shù)改變了——CameraDevice.TEMPLATE_STILL_CAPTURE,這次是針對拍攝圖片的request。
接下來挑關鍵的講:
int rotation = getWindowManager().getDefaultDisplay().getRotation();
獲取屏幕的方向,你旋轉(zhuǎn)屏幕這個值會發(fā)生相應變化,因為對這個理解也不深,不透徹,不敢亂講,大家可以google一下,很多這個的講解,如果我以后理解深了,就會寫進博客。
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(cameraCharacteristics, rotation));//使圖片做順時針旋轉(zhuǎn) //captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,getOrientaion(rotation))兩種方法都可以。
這個是設置圖片的方向使圖片豎直放置,注意第二個參數(shù),代表應該圖片應該旋轉(zhuǎn)的角度,因為這個比較復雜(真的復雜,,,),大家想了解可以看看這篇博客——Android相機開發(fā)那些坑。
代碼中我提供了兩種第二個參數(shù):
網(wǎng)友提出的:
//獲取圖片應該旋轉(zhuǎn)的角度,使圖片豎直 public int getOrientation(int rotation) { switch (rotation) { case Surface.ROTATION_0: return 90; case Surface.ROTATION_90: return 0; case Surface.ROTATION_180: return 270; case Surface.ROTATION_270: return 180; default: return 0; } }
Google官網(wǎng)上的:
//獲取圖片應該旋轉(zhuǎn)的角度,使圖片豎直 private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) { if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return 0; int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION); // Round device orientation to a multiple of 90 deviceOrientation = (deviceOrientation + 45) / 90 * 90; // LENS_FACING相對于設備屏幕的方向,LENS_FACING_FRONT相機設備面向與設備屏幕相同的方向 boolean facingFront = c.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT; if (facingFront) deviceOrientation = -deviceOrientation; // Calculate desired JPEG orientation relative to camera orientation to make // the image upright relative to the device orientation int jpegOrientation = (sensorOrientation + deviceOrientation + 360) % 360; return jpegOrientation; }
目前發(fā)現(xiàn)兩種都可以,所以兩種都放上來,但個人也不是全部理解,所以很抱歉,暫時還不能給大家做解釋,不過相信聰明的你們可以通過閱讀上面的博客理解它!
mSession.capture(mCaptureRequest, null, mHandler);
這是最關鍵的一步,通過這步,拍照動作才真正完成。
其原型是:
capture(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)
相對簡單,就不解釋了。
經(jīng)過上述有點復雜的步驟,相信大家都可以做出一個最簡單的相機應用!
三,進階應用之圖片的保存
我將它集成在一個方法中,方便需要時插入到代碼中:
//將照片存儲在相機照片存儲位置,這里采用bitmap方式保存 public String savePicture(byte[] imgBytes) { pictureId++; String imgPath = Environment.getExternalStorageDirectory() + "/DCIM/Camera/WordRecognition_picture" + pictureId + ".jpg"; Bitmap bitmap = BitmapFactory.decodeByteArray(imgBytes, 0, imgBytes.length);//圖像數(shù)據(jù)被轉(zhuǎn)化為bitmap File outputImage = new File(imgPath); FileOutputStream outputStream = null; try { if (outputImage.exists()) { outputImage.delete();//存在就刪除 } outputImage.createNewFile(); } catch (IOException e) { e.printStackTrace(); } try { outputStream = new FileOutputStream(outputImage); bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);//第二個參數(shù)為壓縮質(zhì)量 } catch (FileNotFoundException e) { e.printStackTrace(); } try { outputStream.flush(); outputStream.close(); } catch (IOException e) { e.printStackTrace(); }//及時更新到系統(tǒng)相冊 MediaScannerConnection.scanFile(this, new String[]{Environment.getExternalStorageDirectory() + "http://DCIM//Camera//WordRecogniton_picture" + pictureId + ".jpg"}, null, null);//"http://"可以用File.separator代替 return imgPath; }
同樣,我們接下來分開講解:
public String savePicture(byte[] imgBytes)
參數(shù)部分我傳入了圖片的byte數(shù)據(jù)格式,那么它是如何獲得的呢——
private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { byte[] imgBytes; imgBytes = getImagBytes(reader);//獲取img的bytes數(shù)據(jù)格式 filePath = savePicture(imgBytes); } };
這個就是從之前代碼改過來的,一般都在這個回調(diào)方法中保存照片。
pictureId++; String imgPath = Environment.getExternalStorageDirectory() + "/DCIM/Camera/WordRecognition_picture" + pictureId + ".jpg";
pictureId就是一個為命名專門而設的一個int變量,初衷是為了不使圖片命名重復,但現(xiàn)在看來其實使用系統(tǒng)時間應該更明智。
imgPath是你的圖片要存放的目標文件地址,其中
Environment.getExternalStorageDirectory() :內(nèi)部存儲文件夾,其下是所有內(nèi)部存儲文件,你打開文件管理,應該可以看到這個內(nèi)部存儲文件夾,點進去是手機里各種文件夾。
DCIM/Camera:所有的安卓手機拍攝的照片都保存在這個文件夾中,大家可以去看看。
File outputImage = new File(imgPath); FileOutputStream outputStream = null; try { if (outputImage.exists()) { outputImage.delete();//存在就刪除 } outputImage.createNewFile(); } catch (IOException e) { e.printStackTrace(); }
創(chuàng)造對應文件及輸出流,對應文件不存在就創(chuàng)造,存在就刪除。
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);//第二個參數(shù)為壓縮質(zhì)量
Bitmap自帶的一個圖像壓縮方法,第一個參數(shù)為壓縮的格式,第二個參數(shù)為壓縮質(zhì)量(100表示無損),第三個參數(shù)表明壓縮數(shù)據(jù)的輸出對象。
//及時更新到系統(tǒng)相冊 MediaScannerConnection.scanFile(this, new String[]{Environment.getExternalStorageDirectory() + "http://DCIM//Camera//WordRecogniton_picture" + pictureId + ".jpg"}, null, null);//"http://"可以用File.separator代替
更新圖片到相冊的一個方法。
四,進階應用之調(diào)用系統(tǒng)相冊
我將其寫成一個方法:
public void openAlbum() { Intent intent = new Intent("android.intent.action.GET_CONTENT");//選擇照片后毀掉onActivityResult方法 intent.setType("image/*"); startActivityForResult(intent, CHOOSE_PHOTO); }@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case CHOOSE_PHOTO: String imgPath=null; if (resultCode == RESULT_OK) { if (Build.VERSION.SDK_INT > 19) { imgPath = handlerImgOnNewVersion(data); } else { imgPath = handlerImgOnOldVersion(data); } //以上獲取了選擇的圖片的路徑,在這里可以應用這個路徑,做一些想要做的東西 } break; default: } }private String handlerImgOnOldVersion(Intent data) { Uri uri = data.getData(); String imgPath = getImagePath(uri, null); return imgPath; } private String handlerImgOnNewVersion(Intent data) { String imgPath = null;//選擇的圖片的路徑 Uri uri = data.getData();//選擇圖片的結(jié)果,即圖片地址的封裝,接下來對其進行解析 if (DocumentsContract.isDocumentUri(this, uri)) {//判斷是否是document類型 String docId = DocumentsContract.getDocumentId(uri); switch (uri.getAuthority())//就是獲取uri的最開頭部分 { case "com.android.providers.media.documents": String id = docId.split(":")[1];//解析出數(shù)字格式的id String selection = MediaStore.Images.Media._ID + "=" + id; imgPath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection); break; case "com.android.providers.downloads.documents": Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId)); imgPath = getImagePath(contentUri, null); break; default: } } else if ("content".equalsIgnoreCase(uri.getScheme())) { imgPath = getImagePath(uri, null); } else if ("file".equalsIgnoreCase(uri.getScheme())) { imgPath = uri.getPath(); } return imgPath; }private String getImagePath(Uri uri, String selection) { String path = null; Cursor cursor = getContentResolver().query(uri, null, selection, null, null); if (cursor != null) { if (cursor.moveToFirst()) { path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); } cursor.close(); } return path; }
現(xiàn)在分開講解:
public void openAlbum() { Intent intent = new Intent("android.intent.action.GET_CONTENT");//選擇照片后毀掉onActivityResult方法 intent.setType("image/*"); startActivityForResult(intent, CHOOSE_PHOTO); }
此處intent啟動顯然是隱式啟動,傳入系統(tǒng)內(nèi)部定義的action:”android.intent.action.GET_CONTENT”:代表你想要獲取特定種類的數(shù)據(jù),setType方法確定你要獲取的類型。
再看回調(diào):
你選擇一張圖片時就會回調(diào)onActivityResult方法
if (resultCode == RESULT_OK) { if (Build.VERSION.SDK_INT > 19) { imgPath = handlerImgOnNewVersion(data); } else { imgPath = handlerImgOnOldVersion(data); }
這里做了個系統(tǒng)適配:
4.4系統(tǒng)以上調(diào)用handlerImgOnNewVersion方法:
private String handlerImgOnNewVersion(Intent data) { String imgPath = null;//選擇的圖片的路徑 Uri uri = data.getData();//選擇圖片的結(jié)果,即圖片地址的封裝,接下來對其進行解析 if (DocumentsContract.isDocumentUri(this, uri)) {//判斷是否是document類型,對應DocumentProvider String docId = DocumentsContract.getDocumentId(uri);//代表文件的唯一id switch (uri.getAuthority())//就是獲取uri的最開頭部分 { case "com.android.providers.media.documents"://多媒體文件類型 String id = docId.split(":")[1];//解析出數(shù)字格式的id String selection = MediaStore.Images.Media._ID + "=" + id;//篩選條件 imgPath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection); break; case "com.android.providers.downloads.documents"://下載內(nèi)容 Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));//將給定的id加到路徑的末尾,相當于構(gòu)造了這類文件的唯一id imgPath = getImagePath(contentUri, null); break; default: } } else if ("content".equalsIgnoreCase(uri.getScheme())) {//對應ContentProvider imgPath = getImagePath(uri, null); } else if ("file".equalsIgnoreCase(uri.getScheme())) {//對應FileProvider imgPath = uri.getPath(); } return imgPath; }
你所選擇的圖片的信息封裝在data中,通過getData方法你獲取到圖片對應的Uri對象,4.4系統(tǒng)以后Uri就被封裝了,不能直接提取圖片地址,必須解析。
一個Uri的數(shù)據(jù)格式分為下面三種:
[scheme:]scheme-specific-part[#fragment] [scheme:][//authority][path][?query][#fragment] [scheme:][//host:port][path][?query][#fragment]
顯然這里是第二種。
其實理解各類型uri是怎么劃分,哪種文件的uri屬于哪種類型是沒有什么意義的,我們只需要知道你獲取任意形式內(nèi)容時,Uri有三個大的類型來源——
DocumentProvider,ContentProvider,FileProvider(三種內(nèi)容提供器),即按Scheme可以分為這三大類。
然后每種可能按authority分小類,處理方法不同。其它在注釋中大都都解釋了,selection代表篩選的條件,系統(tǒng)通過這個條件和uri識別出對應文件。
private String getImagePath(Uri uri, String selection) { String path = null; Cursor cursor = getContentResolver().query(uri, null, selection, null, null); if (cursor != null) { if (cursor.moveToFirst()) { path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); } cursor.close(); } return path; }
query原型:
query(Uri,String[]projection,String selection,String[]selectionArgs,String sortOrder)
projection:所要搜尋的列的名字(columns),null表示所有列
selection:篩選行的條件,null表示所有行
selectionArgs:為selection中的占位符“?”提供具體的值
sorOrder:返回結(jié)果的排序方式,null表示默認順序
query返回Cursor用于遍歷數(shù)據(jù)。
cursor.getColumnIndex(MediaStore.Images.Media.DATA)
返回指定列的名稱對應的Cursor索引,用于搜尋對應數(shù)據(jù)。
MediaStore.Images.Media.DATA存儲了對應圖片的地址信息。
以上種種過程其實是查表的過程,你可以想象系統(tǒng)用了一張很大的表來存儲各種信息。
介紹到這,相信大家差不多理解了如何打開相冊。
馬上我會傳一下自己寫的一個小的文字識別的app.