Android CameraView 實用經驗分享
一、寫在前面
在日常開發中我們經常需要進行相機相關的開發,一般我們都是基於優秀的開源專案 ofollow,noindex">CameraView 去進行二次開發,這個元件幫助我們封裝好了非常複雜的相機 API 呼叫,使用十分簡單,但是簡單的東西相應的也有不好的地方,那就是它的功能相對單一,許多功能我們都需要自己去實現,這次我就分享一下兩個比較有意思的內容:1. 修復了一個可能是 BUG 的 BUG;2. 加入手動對焦功能,效果基本和原生相機一致。
二、使用非規整的 CameraView 尺寸
當我們需要自定義 CameraView 的尺寸時,比如不是規整的 4:3、16:9 等尺寸時,我們會發現,相機的預覽圖和我們拍攝的圖會不一致,拍攝完的圖會比預覽圖向右或者向下拓展一些內容,類似於下圖,這樣的話本來我們拍攝的在中央的主體就不在中央了,這顯然是不可以接受的,那我們應該如何將成像和預覽修正成中心對齊的呢(我認為這樣的效果可以滿足大多數需求)?

預覽與成像不匹配示意圖
通過閱讀原始碼,我們發現,CameraView 實際是一個 FrameLayout, 並通過在內部新增 TextureViewPreview(管理 TextureView) 或 SurfaceView/">SurfaceViewPreview(管理 SurfaceView)來控制相機展示,下文為方便我們將這兩個元件統稱為 Preview 元件。其內部主要實現了 onMeasure 方法,其中比較重要的程式碼就是如下:
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)); }
通過自身的寬高和相機當前預覽的尺寸比例,來給 Preview 元件設定寬高,保證其寬高比為 4:3、16:9 等這些規整的輸出尺寸,也就是說 Preview 元件展示出來的影象實際和我們成像後的影象大小比例是一致的,之所以我們在螢幕上看到的預覽圖的形狀和成像後的圖大小不一致,是因為 Preview 元件被螢幕截斷了,瞭解到這裡就好辦了,我們只需要給 Preview 元件設定居中,這樣我們拍攝的主體在成像後也將在中央,這樣基本滿足需求了,然後我們去閱讀Preview 元件的原始碼,以 TextureViewPreview 為例,我們發現如下程式碼:
final View view = View.inflate(context, R.layout.texture_view, parent);
繼續看這個佈局檔案我們看到:
<merge xmlns:android="http://schemas.android.com/apk/res/android"> <TextureView android:id="@+id/texture_view" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" /> </merge>
注意看倒數第二行,我們都知道,TextureView 是不需要設定 gravity 屬性的,設定了也沒有用,那作者為啥要這麼寫呢?那我們大膽的想一下,其實作者是想寫 layout_gravity 這個屬性的,但是一大意寫錯了呢?因為設定了 layout_gravity="center",後,TextureView 就可以在 CameraView(實際是 FrameLayout) 中居中了啊!然後我們在看下 SurfaceViewPreview 的原始碼,我們也發現了同樣的問題,於是我們修改程式碼如下:
<merge xmlns:android="http://schemas.android.com/apk/res/android"> <TextureView android:id="@+id/texture_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" /> </merge>
執行程式碼後發現,拍攝後的影象和預覽圖果然是中心對齊的了,這個問題就這麼解決了~我想作者一開始也是希望實現這樣的模式的。
三、加入手動對焦功能
在相機中,手動對焦功能也是十分重要的,但是 CameraView 沒有提供這個支援,所以我們就需要自己去實現。
首先我們要知道,相機的對焦區域是在一個獨立的座標系下的,其橫座標是-2000到2000,縱座標也是-2000到2000,然後我們需要知道手動對焦時我們需要經過如下幾個步驟:
- 通過手指觸控的位置計算對焦區域 focus;
- mCameraParameters.setFocusAreas(focus) 設定對焦區域;
- mCamera.setParameters(mCameraParameters) 設定相機引數;
- mCamera.autoFocus() 調起自動對焦,相機將在我們設定的對焦區域進行對焦。
看完這幾步,我們很容易發現第一步是最關鍵的,我們應該如何通過點選的位置去計算對焦區域呢?在計算之前我們需要明確到以下幾點:
- 經過 “二” 中的位置修正之後,我們現在 CameraView 與其子控制元件 Preview 元件的位置是以中心點對齊的;
- 我們點選的位置是相對於 CameraView 來獲取的,其參照的是螢幕座標系,並且以 CameraView 的左上角為 (0,0),在計算對焦區域時我們將需要進行座標系轉換;
- Preview 元件的大小即長寬比我們可以通過 Camera.getParameters().getPreviewSize() 拿到,這裡有一個比較關鍵的地方,PreviewSize 拿到之後我們會發現他的寬高比和我們 Preview 元件的寬高比是相反的,類似 width1/height1 = height2/width2 這種關係,所以對焦區域座標系和我們的螢幕座標系也有相反的關係,這也是我們將手指點選的點對映到對焦區域座標系的一個關鍵步驟。
下面我們就在程式碼裡走一下觸控拍拍照的流程,首先是計算對焦區域:
/** * 計算對焦區域 * @param displayWidth CameraView 的寬度 * @param displayHeight CameraView 的高度 * @param x CameraView 點選事件的 x 值 * @param y CameraView 點選事件的 y 值 */ @SuppressWarnings("SuspiciousNameCombination") private void calculateTapArea(int displayWidth, int displayHeight, float x, float y) { // 首先旋轉對焦區域座標系,將 CameraView 中的點選的 x,y 值對映為對焦區域座標系旋轉後的座標系中的 x,y 值 Camera.Size previewSize = mCameraParameters.getPreviewSize(); int previewWidth = previewSize.height; int previewHeight = previewSize.width; if (previewWidth / (float) previewHeight > displayWidth / (float) displayHeight) { x += (previewWidth * (displayHeight / (float) previewHeight) - displayWidth) / 2; } else { y += (previewHeight * (displayWidth / (float) previewWidth) - displayHeight) / 2; } // 將新得到的 x,y 值修正為 -2000到2000範圍內,並將其對映到未旋轉的對焦區域座標系中 int centerX = (int) (y / previewHeight * 2000 - 1000); int centerY = -(int) (x / previewWidth * 2000 - 1000); int halfAreaSize = 10; // 對焦區域矩形的邊長的一半 // 設定對焦區域 mFocusArea.set(Math.round(clampFocusArea(centerX - halfAreaSize)), Math.round(clampFocusArea(centerY - halfAreaSize)), Math.round(clampFocusArea(centerX + halfAreaSize)), Math.round(clampFocusArea(centerY + halfAreaSize))); } private int clampFocusArea(int x) { if (x > 1000) { return 1000; } if (x < -1000) { return -1000; } return x; }
有了計算對焦區域的過程,那麼我們要做的就是在點選時設定相機引數了,程式碼如下:
try { mCamera.cancelAutoFocus(); // 注意此處比較重要,CameraView 預設對焦模式不能支援手動對焦,必須設定這個值 mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); // 計算對焦區域 calculateTapArea(displayWidth, displayHeight, centerX, centerY); List<Camera.Area> focus = new ArrayList<>(); focus.add(new Camera.Area(mFocusArea, 1000)); mCameraParameters.setFocusAreas(focus); mCamera.setParameters(mCameraParameters); // 進行自動對焦 mCamera.autoFocus(new Camera.AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { } }); } catch (Exception e) { e.printStackTrace(); }
這樣就完成了一次手動對焦,這個地方還有一個比較需要注意的點,因為我們這裡設定了對焦區域和對焦模式,所以 CameraView 就丟失了原有的自動對焦能力,並且對焦區域也被限制死了,所以我們需要找一個比較合適的時機執行以下程式碼,從而恢復相機狀態,這些時機可以是手機移動時或者是拍照完成時等。
setAutoFocusInternal(mAutoFocus); mCameraParameters.setFocusAreas(null); mCameraParameters.setMeteringAreas(null); mCamera.setParameters(mCameraParameters);
這樣一整套的手動對焦流程就完成了,因為其中有些概念比較繞,所以可能有沒說清楚的地方希望大家見諒。
四、結束
對於相機開發裡一些比較有意思的內容就分享這倆把,希望可以和大家一起多多交流,減少一些踩坑的時間,高效開發~