update: 這篇blog沒有處理android sdk api>=23時的動態權限問題。建議直接使用這一篇:Android SurfaceView Tutorial With Example
對於基於攝像頭的Android應用,實時取景是一個基本前提,通過前置或后置攝像頭持續獲取捕獲到的內容,可以進一步做處理(人臉檢測、美顏、濾鏡等)。
所謂實時取景,簡單說就是調用android的攝像頭,把攝像頭捕獲的內容顯示在apk的界面上。只要應用不關閉,相機就持續捕獲,apk上看到的就是實時的取景了。
采用SurfaceView和Camera來做這件事。
是SDK自帶的SurfaceView類而不是實現它的子類;在布局XML文件中使用SurfaceView而不是FrameLayout。因此,代碼量很少也很容易理解。
從View到SurfaceView
android應用,和用戶交互的GUI界面,是搭載在Activity上的。Activity創建的時候,往往會做setContentView(R.id.main_layout)
,這是根據xml布局文件設定要預先確定好的各種view對象,這些組件在xml中進行設計、設定。當然也可以在Java代碼中進一步動態增加view對象。相當於layout作為各種view的容器。
android sdk自帶了很多view的子類供使用。
View本身:繼承自java.lang.Object類,實現了Drawable.Callback, KeyEvent.Callback, AccessibilitiyEventSource接口。
直接子類有:
AnalogClock:模擬時鍾,有3個指針那種。
ImageView:顯示圖像。其實,任何Drawable對象都可以用ImageView來顯示。
KeyboardView:內置鍵盤。
MediaRouteButton:媒體路由按鈕(不太懂。似乎和多媒體、網絡路由相關)
ProgressBar:(可視化)進度條展示。
Space:空白視圖。輕量級視圖。作用:在不同組件之間插入縫隙、間隔。
SurfaceView:提供了一個專門用於繪制的Surface。Surface的格式、尺寸可以控制。SurfaceView控制這個surface的繪制位置。。。中文翻譯
TextView:文本視圖。
TextureView:紋理視圖。sdk4.0之后的API中可用。常被拿來和SurfaceView比較。
ViewGroup:用來容納其他view對象。是layout(布局)和view containers(視圖容器)的基類。
ViewStub:視圖存根。大小為0、不可見,用來占坑的,apk運行時把坑交給資源。
間接子類有:
AbsListView
AbsSeekBar
AbsSpinner
AbsoluteLayout
AdapterView
AdapterViewAnimator
AdapterViewFlipper
以及其他56個間接子類。
可以看到,SurfaceView和TextureView兩個view子類,都能用於實時取景框的顯示。但是考慮到TextureView需要開啟硬件加速的支持,不考慮。以及,目前看來SurfaceView本身也能勝任實時取景的任務。
代碼
layout文件:surfaceview_main.xml
看到很多教程用的都是FrameLayout,而不是SurfaceView。我很不理解:為什么不用SurfaceView呢?不好用嗎?
anyway,我這里就用SurfaceView了,在我測試過的代碼中是完全可用的。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
tools:context="com.example.chris.myapplication.MainActivity">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
java代碼 ChrisActivity.java
Surface、SurfaceView、SurfaceHolder這三者相當於MVC的存在,Surface是數據,SurfaceView負責顯示,SurfaceHolder控制了Surface。通過讓Activity實現SurfaceHolder.Callback接口,開發者自行實現下面三個函數,開發者就完成內容的處理:
public void surfaceCreated(SurfaceHolder holder);
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height);
public void surfaceDestroyed(SurfaceHolder holder);
而具體到實現,一些額外的細節也要考慮到:相機的初始化和釋放;應用暫停時釋放相機,恢復時獲取相機;屏幕方向與顯示方向的一致。所以有如下代碼:
package com.example.chris.myapplication;
import android.app.Activity;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import java.io.IOException;
/**
* Created by chris on 2017/6/25.
* 網上找了一些博客、教程和代碼,稍微有點頭緒了,現在寫自己的Activity代碼
*/
@SuppressWarnings("deprecation")
// TODO:把camera換成camera2接口??
public class ChrisActivity extends Activity implements SurfaceHolder.Callback{
private static final String TAG = "ChrisAcvitity";
private Camera mCamera;
private SurfaceHolder mHolder;
private SurfaceView mView;
@Override
// 創建Activity時執行的動作
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.surfaceview_main);
mView = (SurfaceView) findViewById(R.id.surfaceView);
mHolder = mView.getHolder();
mHolder.addCallback(this);
}
@Override
// apk暫停時執行的動作:把相機關閉,避免占用導致其他應用無法使用相機
protected void onPause() {
super.onPause();
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
@Override
// 恢復apk時執行的動作
protected void onResume() {
super.onResume();
if (null!=mCamera){
mCamera = getCameraInstance();
try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch(IOException e) {
Log.d(TAG, "Error setting camera preview: " + e.getMessage());
}
}
}
// SurfaceHolder.Callback必須實現的方法
public void surfaceCreated(SurfaceHolder holder){
mCamera = getCameraInstance();
try {
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch(IOException e) {
Log.d(TAG, "Error setting camera preview: " + e.getMessage());
}
}
// SurfaceHolder.Callback必須實現的方法
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height){
refreshCamera(); // 這一步是否多余?在以后復雜的使用場景下,此步驟是必須的。
int rotation = getDisplayOrientation(); //獲取當前窗口方向
mCamera.setDisplayOrientation(rotation); //設定相機顯示方向
}
// SurfaceHolder.Callback必須實現的方法
public void surfaceDestroyed(SurfaceHolder holder){
mHolder.removeCallback(this);
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
// === 以下是各種輔助函數 ===
// 獲取camera實例
public static Camera getCameraInstance(){
Camera c = null;
try {
c = Camera.open();
} catch(Exception e){
Log.d("TAG", "camera is not available");
}
return c;
}
// 獲取當前窗口管理器顯示方向
private int getDisplayOrientation(){
WindowManager windowManager = getWindowManager();
Display display = windowManager.getDefaultDisplay();
int rotation = display.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;
}
android.hardware.Camera.CameraInfo camInfo =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, camInfo);
// 這里其實還是不太懂:為什么要獲取camInfo的方向呢?相當於相機標定??
int result = (camInfo.orientation - degrees + 360) % 360;
return result;
}
// 刷新相機
private void refreshCamera(){
if (mHolder.getSurface() == null){
// preview surface does not exist
return;
}
// stop preview before making changes
try {
mCamera.stopPreview();
} catch(Exception e){
// ignore: tried to stop a non-existent preview
}
// set preview size and make any resize, rotate or
// reformatting changes here
// start preview with new settings
try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (Exception e) {
}
}
}
AndroidManifest.xml 記得添加相機權限
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.chris.myapplication" >
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme" >
<activity android:name=".ChrisActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>