Android的camera開發是經常能接觸到的,但是由於多次迭代,camera的適配是一個很煩的事情,偶然的機會在github上看到google的一個關於camera的demo,這個demo兼容了各個版本的Android系統,谷歌出品必屬精品。這篇文章就來研究一下這個demo。
github地址
這篇文章針對兩類人,一類是想要直接使用camera,儘量快的完成開發,不關心內部原理的人;另一類就是想研究camera在各個版本中的適配以及他們的區別的人。我會針對不同人的需求來寫這篇文章。
第一類
其實想要使用它是非常簡單,首先將demo下載下來解壓,目錄如下:
library裏面的就是我們需要的東西里,將libary作爲module導入你的項目中就可以使用它了,使用起來也非常方便,demo中就有它的使用方法,我拿出來講一下。
首先創建一個佈局文件(也可以創建兩個,因爲相機橫屏和豎屏時的顯示方式不一定一樣,這個根據需求來):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primary"
tools:context=".MainActivity">
<com.google.android.cameraview.CameraView
android:id="@+id/camera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:adjustViewBounds="true"
android:background="@android:color/black"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/take_picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
app:srcCompat="@drawable/ic_camera"
app:useCompatPadding="true"/>
</RelativeLayout>
界面就是一個CameraView、一個顯示菜單選項Toolbar和一個拍照用的FloatingActionButton。
再來看一下Activity:
public class MainActivity extends AppCompatActivity implements
ActivityCompat.OnRequestPermissionsResultCallback {
private static final String TAG = "MainActivity";
private static final int REQUEST_CAMERA_PERMISSION = 1;
private CameraView mCameraView;
//用於在子線程中處理圖片數據
private Handler mBackgroundHandler;
//拍照的點擊事件
private View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.take_picture:
if (mCameraView != null) {
mCameraView.takePicture();
}
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCameraView = (CameraView) findViewById(R.id.camera);
//添加相機監聽回調
if (mCameraView != null) {
mCameraView.addCallback(mCallback);
}
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.take_picture);
if (fab != null) {
fab.setOnClickListener(mOnClickListener);
}
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
}
}
@Override
protected void onResume() {
super.onResume();
//檢查權限,如果有權限就啓動相機,沒有就去請求權限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
mCameraView.start();
} else {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA_PERMISSION);
}
}
@Override
protected void onPause() {
//關閉相機
mCameraView.stop();
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mBackgroundHandler != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mBackgroundHandler.getLooper().quitSafely();
} else {
mBackgroundHandler.getLooper().quit();
}
mBackgroundHandler = null;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CAMERA_PERMISSION:
if (permissions.length != 1 || grantResults.length != 1) {
throw new RuntimeException("Error on requesting camera permission.");
}
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, R.string.camera_permission_not_granted,
Toast.LENGTH_SHORT).show();
}
// No need to start camera here; it is handled by onResume
break;
}
}
private Handler getBackgroundHandler() {
if (mBackgroundHandler == null) {
HandlerThread thread = new HandlerThread("background");
thread.start();
mBackgroundHandler = new Handler(thread.getLooper());
}
return mBackgroundHandler;
}
//相機 監聽回調事件
private CameraView.Callback mCallback = new CameraView.Callback() {
@Override
public void onCameraOpened(CameraView cameraView) {
Log.d(TAG, "onCameraOpened");
}
@Override
public void onCameraClosed(CameraView cameraView) {
Log.d(TAG, "onCameraClosed");
}
@Override
public void onPictureTaken(CameraView cameraView, final byte[] data) {
Log.d(TAG, "onPictureTaken " + data.length);
Toast.makeText(cameraView.getContext(), R.string.picture_taken, Toast.LENGTH_SHORT)
.show();
getBackgroundHandler().post(new Runnable() {
@Override
public void run() {
//在子線程中保存圖片
File file = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES),
"picture.jpg");
OutputStream os = null;
try {
os = new FileOutputStream(file);
os.write(data);
os.close();
} catch (IOException e) {
Log.w(TAG, "Cannot write to " + file, e);
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
// Ignore
}
}
}
}
});
}
};
}
整個Activity的代碼邏輯比較簡單,首先在onCreate中初始相機和添加回調以及FloatingActionButton的點擊事件監聽,Toolbar的功能主要是對相機一些屬性(閃光燈)之類的設置,這裏就不講那麼多了,代碼我也已經刪掉了,需要這部分內容的可以直接去看github上下載的源碼。然後在onResume中啓動相機,在onPause中關閉相機。FloatingActionButton的點擊事件會調用CameraView的拍照方法進行拍照,拍照後會在CameraView.Callback中回調拍照的結果以及數據。大致上就這麼多,用起來還是比較簡單的。
第二類
下面我們來深入研究一下這個開源項目的源碼。首先說一下不同版本下相機開發的大致區別:
- Android 5.0以下的API爲Camera 而 Android 5.0以上的API爲Camera2,並且各大手機廠商對於Camera2的支持程度也不同。對於不支持Camera2的設備來說,需要降級使用Camera.
- 界面渲染主要涉及到SurfaceView 和 TextureView , 在4.0以上才能使用TextureView 。
SurfaceView不受View Hierarchy約束,擁有自己的Surface,可以理解爲是另一個Window。因此一些View特性無法使用,但也因此不會影響主線程,可以放到其它的線程進行渲染,性能友好。
TextureView 則與普通的View類似,受View Hierarchy約束,相機發送過來的數據經由SurfaceTexture交接,讓TextureView能以硬件加速渲染的方式存在視圖樹,也因此更耗費性能。
面對以上問題,CameraView 提出的方案如圖:
在這個項目中所有的適配都由系統完成,用戶只需要關心CameraView 這個類,那麼這是怎麼實現的呢?
先來看一下源碼的目錄結構:
我們來捋一下上面的文件邏輯,CameraViewImpl和PreviewImpl是兩個抽象類,CameraViewImpl封裝了所有關於相機的方法,Camera1和Camera2都繼承這個類,實現其中的方法(當然他們的實現方式不一樣,但是對外的接口是一樣,也就是實現了一樣的功能);PreviewImpl封裝了所有關於界面的方法,TextureViewPreview和SurfaceViewPreview繼承這個類,原理和CameraViewImpl一樣。
在CameraViewImpl中會有一個PreviewImpl的實例,用來將CameraViewImpl中的畫面繪製到PreviewImpl上,而CameraView中會有一個CameraViewImpl的實例,這樣就可以在CameraView中控制相機,而且根據系統版本的不同CameraView會初始化不同的CameraViewImpl實例。大致過程就是這樣,是不是沒聽懂,沒關係,下面有源碼分析。
先來看一下PreviewImpl這個類:
abstract class PreviewImpl {
interface Callback {
// surface發生了變動
void onSurfaceChanged();
}
private Callback mCallback;
// 預覽視圖高度
private int mWidth;
// 預覽視圖寬度
private int mHeight;
void setCallback(Callback callback) {
mCallback = callback;
}
abstract Surface getSurface();
// 獲取實際的渲染View
abstract View getView();
// 輸出源
abstract Class getOutputClass();
// 預覽方向
abstract void setDisplayOrientation(int displayOrientation);
// 渲染視圖是否達到可用狀態
abstract boolean isReady();
// 分發surface 的更變
protected void dispatchSurfaceChanged() {
mCallback.onSurfaceChanged();
}
// 主要是爲了由SurfaceView渲染的情況
SurfaceHolder getSurfaceHolder() {
return null;
}
// 主要是爲了由TextureView渲染的情況
Object getSurfaceTexture() {
return null;
}
// 設置緩衝區大小
void setBufferSize(int width, int height) {
}
void setSize(int width, int height) {
mWidth = width;
mHeight = height;
}
int getWidth() {
return mWidth;
}
int getHeight() {
return mHeight;
}
}
PreviewImpl的主要作用,是提供了必要信息使能接收CameraImpl給予的信息,並進行渲染。
看一下PreviewImpl的一個繼承類SurfaceViewPreview:
class SurfaceViewPreview extends PreviewImpl {
final SurfaceView mSurfaceView;
SurfaceViewPreview(Context context, ViewGroup parent) {
final View view = View.inflate(context, R.layout.surface_view, parent);
mSurfaceView = (SurfaceView) view.findViewById(R.id.surface_view);
final SurfaceHolder holder = mSurfaceView.getHolder();
//noinspection deprecation
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
holder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder h) {
}
@Override
public void surfaceChanged(SurfaceHolder h, int format, int width, int height) {
setSize(width, height);
if (!ViewCompat.isInLayout(mSurfaceView)) {
dispatchSurfaceChanged();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder h) {
setSize(0, 0);
}
});
}
@Override
Surface getSurface() {
return getSurfaceHolder().getSurface();
}
@Override
SurfaceHolder getSurfaceHolder() {
return mSurfaceView.getHolder();
}
@Override
View getView() {
return mSurfaceView;
}
@Override
Class getOutputClass() {
return SurfaceHolder.class;
}
@Override
void setDisplayOrientation(int displayOrientation) {
}
@Override
boolean isReady() {
return getWidth() != 0 && getHeight() != 0;
}
}
這個類具體實現了PreviewImpl中的方法,在視圖刷新的時候調用dispatchSurfaceChanged()方法,這個方法會回調到CameraView中。關於TextureViewPreview是一樣的,只是具體的實現方法不一樣,但對外的接口都是一樣的。
再來看一下CameraViewImpl這個類:
abstract class CameraViewImpl {
// 相機基礎事件回調
protected final Callback mCallback;
// 渲染視圖
protected final PreviewImpl mPreview;
CameraViewImpl(Callback callback, PreviewImpl preview) {
mCallback = callback;
mPreview = preview;
}
// 獲取渲染視圖
View getView() {
return mPreview.getView();
}
// 啓動相機
abstract boolean start();
// 暫停相機
abstract void stop();
// 相機使用狀態
abstract boolean isCameraOpened();
// 設置使用哪一個相機,簡單如前置相機、後置相機
abstract void setFacing(int facing);
// 獲取當前相機標識
abstract int getFacing();
// 獲取相機支持的預覽比例
abstract Set<AspectRatio> getSupportedAspectRatios();
// 設置拍攝照片比例
abstract boolean setAspectRatio(AspectRatio ratio);
// 獲取相機當前攝照片比例
abstract AspectRatio getAspectRatio();
// 設置自動聚焦
abstract void setAutoFocus(boolean autoFocus);
// 獲取自動聚焦
abstract boolean getAutoFocus();
// 設置閃光狀態
abstract void setFlash(int flash);
// 獲取閃光狀態
abstract int getFlash();
// 獲取靜態圖片,即拍照
abstract void takePicture();
// 設置相機方向
abstract void setDisplayOrientation(int displayOrientation);
// 相機基礎回調接口
interface Callback {
// 相機已打開
void onCameraOpened();
// 相機已關閉
void onCameraClosed();
// 相機獲取到靜態圖片
void onPictureTaken(byte[] data);
}
}
CameraViewImpl陳列了共性的可能的相機操作,挑一些做說明:
setFacing()
設置使用具體相機,簡單的如前置相機、後置相機。在相機更變後,原本的預覽視圖可能因爲Rotate而呈現出了不一樣的視圖,因此不僅需要更具需求切換到正確的相機,還需將預覽視圖進行矯正。
setAutoFocu()
一般來說,相機設備自動聚焦是默認開啓的。當然如更多的聚焦模式如固定聚焦、景深、遠景、微焦等也是可以另外支持的。當然,對於我等沒有攝友的人來說,玩轉不來。
setFlash()
閃光燈狀態一般有如自動、關閉、拍照、防紅眼等。
關於CameraViewImpl的繼承類這裏就不講了,其實這裏纔是自定義相機的核心,後面如果有時間再來補充,這裏留個坑。
我們着重來講一下CameraView,CameraView是所有類的集合,封裝就在這裏體現。
由於代碼太多,這裏就不把所有的代碼展示出來了,我們一個個的看。
首先看一下構造函數:
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (isInEditMode()){
mCallbacks = null;
mDisplayOrientationDetector = null;
return;
}
// 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);
}
// Attributes
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView, defStyleAttr,
R.style.Widget_CameraView);
mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, false);
setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));
String aspectRatio = a.getString(R.styleable.CameraView_aspectRatio);
if (aspectRatio != null) {
setAspectRatio(AspectRatio.parse(aspectRatio));
} else {
setAspectRatio(Constants.DEFAULT_ASPECT_RATIO);
}
setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));
setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));
a.recycle();
// Display orientation detector
mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
@Override
public void onDisplayOrientationChanged(int displayOrientation) {
mImpl.setDisplayOrientation(displayOrientation);
}
};
}
看第9行的代碼,這裏通過createPreviewImpl方法創建了一個PreviewImpl實例,我們到這個方法裏看一下:
@NonNull
private PreviewImpl createPreviewImpl(Context context) {
PreviewImpl preview;
if (Build.VERSION.SDK_INT < 14) {
preview = new SurfaceViewPreview(context, this);
} else {
preview = new TextureViewPreview(context, this);
}
return preview;
}
其實就是根據android版本創建不同的view,這兩個view都是我們之前創建的。
再看第10行代碼,這裏創建了一個CallbackBridge,我們來看一下這個CallbackBridge:
private class CallbackBridge implements CameraViewImpl.Callback {
private final ArrayList<Callback> mCallbacks = new ArrayList<>();
private boolean mRequestLayoutOnOpen;
CallbackBridge() {
}
public void add(Callback callback) {
mCallbacks.add(callback);
}
public void remove(Callback callback) {
mCallbacks.remove(callback);
}
@Override
public void onCameraOpened() {
if (mRequestLayoutOnOpen) {
mRequestLayoutOnOpen = false;
requestLayout();
}
for (Callback callback : mCallbacks) {
callback.onCameraOpened(CameraView.this);
}
}
@Override
public void onCameraClosed() {
for (Callback callback : mCallbacks) {
callback.onCameraClosed(CameraView.this);
}
}
@Override
public void onPictureTaken(byte[] data) {
for (Callback callback : mCallbacks) {
callback.onPictureTaken(CameraView.this, data);
}
}
public void reserveRequestLayoutOnOpen() {
mRequestLayoutOnOpen = true;
}
}
這個Callback繼承 CameraViewImpl.Callback ,主要用來監聽相機的打開關閉拍照事件。
再看第11行,這裏根據不同的版本號創建不同的Camera,然後把前面創建的Callback和preview傳入其中,這樣我們的Camera就創建成功了,接下來就是通過它進行各種操作。
構造函數裏還初始化了方向,對焦模式,閃光燈等參數,這裏就不展開講了。
整體來看這個demo的架構還是比較簡單明瞭的,不過看起來簡單,想自己寫出來還是挺難的,什麼時候這樣的架構能夠信手拈來的時候技術也就到達一定的水平了。
就講到這吧,其實根本就沒有滿足第二類人的需要,但是隻要你搞懂了這個demo的架構,就完全有能力自己搞懂Camera,都在代碼裏,read the fuck code