1、背景介紹
去年年初的時(shí)候?qū)戇^一篇文章 《CameraX:Android 相機(jī)庫開發(fā)實(shí)踐》,那時(shí)我想自己寫一個(gè) Android 相機(jī)庫,但是因?yàn)槊趾凸雀桕P(guān)放的 CameraX 沖突了,所以現(xiàn)在我將自己的項(xiàng)目改名為 iCamera.
之前的文章中也交代過一些 Android 相機(jī)庫的背景,本身集成相機(jī)功能到自己的項(xiàng)目中并不復(fù)雜,但是如果設(shè)計(jì)一個(gè)功能全面的 Android 相機(jī)庫就沒那么簡單了——你要滿足更多用戶的需求,基本的縮放、閃光燈等這些在日常開發(fā)中不會(huì)涉及的功能都要支持;此外,你還要處理相機(jī)的各種支持尺寸和寬高比的計(jì)算問題,滿足用戶自定義的需求等等。
在 iCamera 之前,為了集成相機(jī)功能,我也找過一些開源的相機(jī)庫。比如 CameraView,雖然是掛名谷歌,但是并不算谷歌官方的庫,而且因?yàn)榇a設(shè)計(jì)的問題,本身性能并不好。再者 CameraFragment,雖然代碼結(jié)構(gòu)清晰得多,但是對(duì)很多功能的支持不夠完善。還有 CameraX,這個(gè)庫我沒有仔細(xì)研究它的代碼,但是它只支持 Camera2. Camera2 雖然 API 21 上就可以使用,但實(shí)際上很多手機(jī)對(duì) Camera2 的支持并不好,就比如在我的手機(jī)(OnePlus6, API 29)上 Camera1 的啟動(dòng)速率明顯高于 Camera2.
綜上,我決定自己寫一個(gè)性能更好、功能全面并且支持用戶自定義的相機(jī)庫。這個(gè)項(xiàng)目去年就開始寫了,但是因?yàn)楣ぷ鞯膯栴}一直沒時(shí)間完善。最近有了些自己的時(shí)間,于是我解決了之前遺留下來的各種問題并做了系統(tǒng)的測試。現(xiàn)在,第一個(gè)版本已經(jīng)正式發(fā)布可用了。
項(xiàng)目地址:iCamera
在這篇文章中,我重點(diǎn)介紹下是如何設(shè)計(jì)和實(shí)現(xiàn)這樣一款 Android 相機(jī)庫的,希望這能夠?qū)δ愕南到y(tǒng)設(shè)計(jì)有所啟發(fā),并且希望這能夠增進(jìn)你對(duì) iCamera 的了解。
2、整體的設(shè)計(jì)與實(shí)現(xiàn)
下面是這個(gè)項(xiàng)目整體的設(shè)計(jì)圖,相比于第一個(gè)版本的設(shè)計(jì),在下面的這個(gè)版本中我又新增了一些方法并對(duì)部分設(shè)計(jì)細(xì)節(jié)做了調(diào)整:
鏈接地址:https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049
了解了整體的設(shè)計(jì),我再來具體介紹下我是如何通過多種設(shè)計(jì)模式的綜合運(yùn)用來滿足用戶的自定義等需求的。
2.1 單例的應(yīng)用:緩存、自定義和預(yù)加載
最初我在設(shè)計(jì)的時(shí)候希望通過靜態(tài)字段緩存一些相機(jī)的信息,這樣一來可以避免多次從相機(jī)屬性中讀取和計(jì)算各種參數(shù),二來可以通過預(yù)加載操作來把讀取相機(jī)參數(shù)的操作提前到相機(jī)啟動(dòng)之前來達(dá)到加快相機(jī)啟動(dòng)速度的目的。不過后來我發(fā)現(xiàn)使用靜態(tài)單例一樣可以滿足這個(gè)需求并且應(yīng)該說更加合理一些。于是,我在項(xiàng)目中使用了單例的 ConfigurationProvider
來緩存計(jì)算結(jié)果并且提供方法提前讀取相機(jī)信息。該類如下:
public final class ConfigurationProvider {
/** The singleton */
private static volatile ConfigurationProvider configurationProvider;
/**
* The sizes map from a int value, which was calculated from:
* hash = {@link CameraFace} | {@link CameraSizeFor} | {@link CameraType} */
private SparseArray<List<Size>> sizeMap;
/**
* The room ratios map from a int value, which was calculated from:
* hash = {@link CameraFace} | {@link CameraType} */
private SparseArray<List<Float>> ratioMap;
private ConfigurationProvider() {
if (configurationProvider != null) {
throw new UnsupportedOperationException("U can't initialize me!");
}
initWithDefaultValues();
}
private void initWithDefaultValues() {
// initialize all kinds of parameters
}
public static ConfigurationProvider get() {
if (configurationProvider == null) {
synchronized (ConfigurationProvider.class) {
if (configurationProvider == null) {
configurationProvider = new ConfigurationProvider();
}
}
}
return configurationProvider;
}
}
對(duì)于 Camera2,因?yàn)楸旧硭恍枰蜷_相機(jī)就能從系統(tǒng)服務(wù)中讀取相機(jī)信息,所以我們可以方便地實(shí)現(xiàn)預(yù)加載的需求。按照下面這樣,我們只需要在打開相機(jī)之前調(diào)用 prepareCamera2()
就可以提前將相機(jī)的信息讀取并緩存起來,這樣就可以減少相機(jī)啟動(dòng)過程中的時(shí)間。我測試了下,這樣大概可以減少幾十毫秒的時(shí)間:
public final class ConfigurationProvider {
// ....
private int numberOfCameras;
private AtomicBoolean camera2Prepared = new AtomicBoolean();
private SparseArray<String> cameraIdCamera2 = new SparseArray<>();
private SparseArray<CameraCharacteristics> cameraCharacteristics = new SparseArray<>();
private SparseIntArray cameraOrientations = new SparseIntArray();
private SparseArray<StreamConfigurationMap> streamConfigurationMaps = new SparseArray<>();
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ConfigurationProvider prepareCamera2(Context context) {
if (!camera2Prepared.get()) {
CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
try {
assert cameraManager != null;
final String[] ids = cameraManager.getCameraIdList();
numberOfCameras = ids.length;
for (String id : ids) {
final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
final Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
cameraIdCamera2.put(CameraFace.FACE_FRONT, id);
Integer iFrontCameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
cameraOrientations.put(CameraFace.FACE_FRONT, iFrontCameraOrientation == null ? 0 : iFrontCameraOrientation);
cameraCharacteristics.put(CameraFace.FACE_FRONT, characteristics);
streamConfigurationMaps.put(CameraFace.FACE_FRONT, characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP));
} else if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK){
cameraIdCamera2.put(CameraFace.FACE_REAR, id);
Integer iRearCameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
cameraOrientations.put(CameraFace.FACE_REAR, iRearCameraOrientation == null ? 0 : iRearCameraOrientation);
cameraCharacteristics.put(CameraFace.FACE_REAR, characteristics);
streamConfigurationMaps.put(CameraFace.FACE_REAR, characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP));
}
}
camera2Prepared.set(true);
} catch (Exception e) {
XLog.e(TAG, "initCameraInfo error " + e);
}
}
return this;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public StreamConfigurationMap getStreamConfigurationMap(Context context, @CameraFace int cameraFace) {
return prepareCamera2(context).streamConfigurationMaps.get(cameraFace);
}
// ...
}
此外,后面會(huì)提到我們滿足用戶自定義需求的設(shè)計(jì)邏輯——策略接口,所以,我們又可以通過 ConfigurationProvider
暴露的方法設(shè)置全局的計(jì)算策略,比如具體使用 Camera1 還是 Camera2,使用 TextureView 還是 SurfaceView,各種寬高比、支持的尺寸如何進(jìn)行計(jì)算等:
public final class ConfigurationProvider {
/** The creator for {@link me.shouheng.icamera.manager.CameraManager}. */
private CameraManagerCreator cameraManagerCreator;
/** The creator for {@link me.shouheng.icamera.preview.CameraPreview}. */
private CameraPreviewCreator cameraPreviewCreator;
/** The calculator for camera size. */
private CameraSizeCalculator cameraSizeCalculator;
// ... setters & getters
}
用戶只需要實(shí)現(xiàn)這些接口并通過 ConfigurationProvider
暴露的方法在相機(jī)啟動(dòng)之前將其賦值給這個(gè)單例就可以實(shí)現(xiàn)相機(jī)默認(rèn)算法的替換。
2.2 策略模式應(yīng)用:滿足用戶自定義需求
之前我們也說過自己開發(fā)一個(gè)相機(jī)庫一個(gè)難點(diǎn)就是處理各種尺寸的計(jì)算問題。因?yàn)椋粋€(gè)相機(jī)支持的寬高比從 2:1 到 1:1 不等,每個(gè)比例下有多個(gè)支持的尺寸。另外,相機(jī)的尺寸又分為預(yù)覽的支持尺寸、照片的支持尺寸和視頻的支持尺寸。所以,要處理的數(shù)據(jù)比較多,問題是你如何整合這些需求。另外還有 Camera1 還是 Camera2,使用 TextureView 還是 SurfaceView 的問題。對(duì)于一般的相機(jī)開發(fā),可能默認(rèn)就是 4:3 了,所以我覺得這才是開發(fā)一個(gè)庫和一個(gè)應(yīng)用相比復(fù)雜的地方。
這里我的設(shè)計(jì)是使用策略接口提供接口給用戶進(jìn)行算法自定義。
1. CameraManager:相機(jī)的實(shí)現(xiàn)邏輯
對(duì)于 Camera1 和 Camera2 的邏輯,我們提供了 CameraManager 這個(gè)接口以及兩者分別的實(shí)現(xiàn) Camera1Manager 和 Camera2Manager,而兩者的實(shí)現(xiàn)又共同繼承自一個(gè)父類 BaseCameraManager. 因?yàn)槲覀兊膸煺w上還是將 CameraView 以一個(gè)控件的形式交付給用戶,所以 CameraView 中引用了 CameraManager. 可以說 CameraView 的每個(gè)方法都是直接或者間接調(diào)用了 CameraManager 的方法,因此 CameraManager 的方法非常多,我想一般人應(yīng)該不會(huì)想自己實(shí)現(xiàn)一個(gè) CameraManager 接口。所以,這個(gè)接口我不多介紹了。
2. CameraPreview:相機(jī)展示控件的邏輯
除了 Camera1 和 Camera2 的選擇,相機(jī)預(yù)覽控件也要在 TextureView 和 SurfaceView 之間進(jìn)行選擇。跟 CameraManager 一樣,我們提供了 CameraPreview 接口來滿足用戶的自定義需求。
在 CameraManager 和 CameraPreview 之上我們又定義了兩個(gè)工廠接口:CameraManagerCreator 和 CameraPreviewCreator,用戶可以通過實(shí)現(xiàn)這兩個(gè)接口并將其設(shè)置到 ConfigurationProvider 來實(shí)現(xiàn)定義自己的策略。
public interface CameraManagerCreator {
/**
* Method used to create {@link CameraManager}.
*
* @param context the context
* @param cameraPreview the {@link CameraPreview}
* @return CameraManager object.
*/
CameraManager create(Context context, CameraPreview cameraPreview);
}
public interface CameraPreviewCreator {
/**
* Method used to create {@link CameraPreview}.
*
* @param context the context to create the preview view.
* @param parent the parent view of the preview.
* @return CameraPreview object.
*/
CameraPreview create(Context context, ViewGroup parent);
}
比如,我們默認(rèn)提供的 CameraManagerCreator 的實(shí)現(xiàn)是:只要系統(tǒng) API≥21 并且支持 Camera2,我們就使用 Camera2Manager 作為自己的相機(jī)邏輯實(shí)現(xiàn)。如果你測試發(fā)現(xiàn)這無法滿足自己的需求,可以使用 ConfigurationProvider 的方法,將 Camera1OnlyCreator 作為自己的策略,這樣無論什么場景都使用 Camera1. 當(dāng)然,你也可以添加自定義的策略,只要實(shí)現(xiàn)自己的接口即可并塞到 ConfigurationProvider 中即可。
3. CameraSizeCalculator:綜合處理各種尺寸和寬高比的問題
與 CameraManager 類似,CameraSizeCalculator 是提供給用戶來自定義各種尺寸計(jì)算規(guī)則的接口。這個(gè)接口是我最近改動(dòng)比較大的接口,經(jīng)過調(diào)整之后接口內(nèi)的各個(gè)方法簡單清晰得多了:
public interface CameraSizeCalculator {
void init(@NonNull AspectRatio expectAspectRatio,
@Nullable Size expectSize,
@MediaQuality int mediaQuality,
@NonNull List<Size> previewSizes,
@NonNull List<Size> pictureSizes,
@NonNull List<Size> videoSizes);
void changeExpectAspectRatio(@NonNull AspectRatio expectAspectRatio);
void changeExpectSize(@Nullable Size expectSize);
void changeMediaQuality(@MediaQuality int mediaQuality);
Size getPictureSize(@CameraType int cameraType);
Size getPicturePreviewSize(@CameraType int cameraType);
Size getVideoSize(@CameraType int cameraType);
Size getVideoPreviewSize(@CameraType int cameraType);
}
用戶可以通過這里的方法含義如下:
- 我們通過
init()
方法將當(dāng)前系統(tǒng)相機(jī)的各種尺寸信息告知你 - 當(dāng)用戶改變了相機(jī)的參數(shù)的時(shí)候,我們通過這里三個(gè)以
change
開頭的方法進(jìn)行通知 - 這里的四個(gè)以
get
開頭的方法是需要你實(shí)現(xiàn)計(jì)算規(guī)則并返回計(jì)算結(jié)果的方法
這里我們要處理的參數(shù)包括:
- 系統(tǒng)支持的所有預(yù)覽尺寸列表:
previewSizes
- 系統(tǒng)支持的所有照片尺寸列表:
pictureSizes
- 系統(tǒng)支持的所有視頻尺寸列表:
videoSizes
- 用戶期望得到的視頻或者照片的尺寸:
expectSize
- 用戶期望得到的視頻或者照片的寬高比:
expectAspectRatio
- 用戶期望得到的視頻或者照片的質(zhì)量:
mediaQuality
該接口的一個(gè)默認(rèn)實(shí)現(xiàn)在 CameraSizeCalculatorImpl
中。在新版本中,我做了三處調(diào)整:
增加了尺寸的緩存信息,用戶期望的尺寸、寬高比和輸出質(zhì)量不變的時(shí)候只需要計(jì)算一次,以后可以復(fù)用,來實(shí)現(xiàn)相機(jī)啟動(dòng)速率的提升
實(shí)現(xiàn)了緩存的尺寸的隔離,之前我沒有將緩存的尺寸信息隔離開,導(dǎo)致在拍攝視頻和照片之間切換的時(shí)候出現(xiàn)畫面扭曲的問題,現(xiàn)在解決了這個(gè)問題
-
增加了新的計(jì)算算法,目前該庫中包含兩種尺寸計(jì)算算法:
第一種適用于輸出的圖片或者視頻的尺寸,我們會(huì)整合期望的輸出的尺寸、期望的輸出的寬高比和期望的輸出的圖片質(zhì)量來選擇一個(gè)最符合要求的輸出尺寸:首先尋找最接近期望的寬高比,然后尋找最接近的尺寸,如果沒有指定期望的尺寸會(huì)根據(jù)期望的輸出的質(zhì)量將所有支持尺寸劃分為不同的品質(zhì)之后選擇符合要求的尺寸。
第二種算法適用于預(yù)覽的尺寸。相比于輸出的圖片和視頻的尺寸,預(yù)覽的尺寸可能沒那么重要。我們只需要尋找一個(gè)符合接近于輸出的圖片或者視頻的尺寸的尺寸即可。因此,這里的算法是,首先匹配寬高比,其次匹配尺寸。
具體實(shí)現(xiàn)可以參考源碼,這里就不再一一說明了。
3、細(xì)節(jié)的優(yōu)化與設(shè)計(jì)
3.1 枚舉在 iCamera 中的應(yīng)用
以相機(jī)的尺寸為例,它分為預(yù)覽的尺寸、輸出視頻的尺寸和輸出照片的尺寸,當(dāng)我們要對(duì)外暴露一個(gè)獲取尺寸的方法的時(shí)候,按照一般的思路勢必要提供三個(gè)方法。但是,我們可以通過整數(shù)+注解來取代枚舉,從而實(shí)現(xiàn)將三個(gè)方法合并未一個(gè)的目標(biāo)。比如,
public Size getSize(@CameraSizeFor int sizeFor) {
return cameraManager.getSize(sizeFor);
}
public SizeMap getSizes(@CameraSizeFor int sizeFor) {
return cameraManager.getSizes(sizeFor);
}
此外,我們?cè)趹?yīng)用中還定義了其他的枚舉,比如表示前置和后置相機(jī)的 CameraFace
,表示 Camera1 還是 Camera2 的 CameraType 等,所以,在某些場合我們只需要進(jìn)行按位取或就可以實(shí)現(xiàn) hash 的鍵的區(qū)分,這樣設(shè)計(jì)使我們緩存隔離的時(shí)候的邏輯更加簡潔明了:
public List<Size> getSizes(android.hardware.Camera camera,
@CameraFace int cameraFace,
@CameraSizeFor int sizeFor) {
// calculate hash of map
int hash = cameraFace | sizeFor | CameraType.TYPE_CAMERA1;
XLog.d(TAG, "getSizes hash : " + Integer.toHexString(hash));
// try to get sizes from cache first.
if (useCacheValues) {
List<Size> sizes = sizeMap.get(hash);
if (sizes != null) {
return sizes;
}
}
// get sizes from parameters
android.hardware.Camera.Parameters parameters = camera.getParameters();
List<Size> sizes;
switch (sizeFor) {
case CameraSizeFor.SIZE_FOR_PICTURE:
sizes = Size.fromList(parameters.getSupportedPictureSizes());
break;
case CameraSizeFor.SIZE_FOR_PREVIEW:
sizes = Size.fromList(parameters.getSupportedPreviewSizes());
break;
case CameraSizeFor.SIZE_FOR_VIDEO:
sizes = Size.fromList(parameters.getSupportedVideoSizes());
break;
default:
throw new IllegalArgumentException("Unsupported size for " + sizeFor);
}
// cache the sizes in memory
if (useCacheValues) {
sizeMap.put(hash, sizes);
}
return sizes;
}
3.2 應(yīng)用內(nèi)部的耗時(shí)分析
在進(jìn)行相機(jī)開發(fā)的時(shí)候我們?cè)趹?yīng)用內(nèi)部進(jìn)行了許多的耗時(shí)分析,比如之前用 TraceView 進(jìn)行方法調(diào)用的分析,以及使用各種 Log 輸出日志的方式來統(tǒng)計(jì)應(yīng)用耗時(shí)的邏輯。上也提到過我們使用預(yù)加載和緩存等來實(shí)現(xiàn)系統(tǒng)的啟動(dòng)速率的提升等。
4、總結(jié)
在相機(jī)庫開發(fā)過程中還遇到一些其他的問題,比如橫豎屏切換和快門聲音處理等,這些都已經(jīng)反應(yīng)到了 iCamera 的源碼中。本來也想寫一下如何使用 Android 提供的 API 實(shí)現(xiàn)相機(jī)開發(fā)的,但是我覺得最好的教程就是源碼。既然項(xiàng)目已經(jīng)開源,直接讀源碼好了,并且我自認(rèn)為這個(gè)項(xiàng)目的代碼還是我比較滿意的。本來嘛,我們把別人的東西分析得頭頭是道但是卻做不成自己的東西,那分析了這么多又有什么用呢?其實(shí)比學(xué)習(xí)能力更高級(jí)的應(yīng)該是創(chuàng)造力。有人問我說,你寫這些開源項(xiàng)目和博客的目的是什么,我只能說,這是一種表達(dá),而生命本身就是一種表達(dá)。我們只不過是通過做這些事情來告訴這個(gè)世界,什么是對(duì)的,什么是美好的。