玩轉Android Camera開發(一):Surfaceview預覽Camera,基礎拍照功能完整demo
雜家前文是在2012年的除夕之夜倉促完成,後來很多人指出了一些問題,瑣事纏身一直沒有進行升級。後來隨著我自己的使用,越來越發現不出個升級版的demo是不行了。有時候就連我自己用這個demo測一些效能、功能點,用著都不順手。當初程式碼是在linux下寫的,弄到windows裡下全是亂碼。還要自己改幾分鐘才能改好。另外,很多人說不能正常預覽,原因是我在佈局裡把Surfaceview的尺寸寫死了。再有就是initCamera()的時候設引數失敗,直接黑屏退出,原因也是我把預覽尺寸和照片尺寸寫死了。再有就是照片變形的問題。為此,今天出一個升級版的demo,爭取全面適配所有機型。
上圖為此次的程式碼結構,activity包裡就是放CameraActivity,日後新增相簿瀏覽功能再加GalleryActivity。為了使Camera的邏輯和介面的UI耦合度降至最低,封裝了CameraInterface類,裡面操作Camera的開啟、預覽、拍照、關閉。preview包裡是自定義的Surfaceview。在util包裡放著CamParaUtil是專門用來設定、列印Camera的PreviewSize、PictureSize、FocusMode的,並能根據Activity傳進來的長寬比(主要是16:9 或 4:3兩種尺寸)自動尋找適配的PreviewSize和PictureSize,消除變形。預設的是全屏,因為一些手機全屏時,螢幕的長寬比不是16:9或4:3所以在找尺寸時也是存在一些偏差的。其中有個值,就是判斷兩個float是否相等,這個引數比較關鍵,裡面設的0.03.經我多個手機測試,這個引數是最合適的,否則的話有些奇葩手機得到的尺寸拍出照片變形。下面上原始碼:
一、佈局 activity_camera.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CameraActivity" >
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<org.yanzi.camera.preview.CameraSurfaceView
android:id="@+id/camera_surfaceview"
android:layout_width="0dip"
android:layout_height="0dip" />
</FrameLayout>
<ImageButton
android:id="@+id/btn_shutter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/btn_shutter_background"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="10dip"/>
</RelativeLayout>
二、AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.yanzi.playcamera"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="9"
android:targetSdkVersion="17" />
<!-- 增加檔案儲存和訪問攝像頭的許可權 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher_icon"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="org.yanzi.activity.CameraActivity"
android:label="@string/app_name"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
三、下面是java程式碼
1、CameraActivity.java
package org.yanzi.activity;
import org.yanzi.camera.CameraInterface;
import org.yanzi.camera.CameraInterface.CamOpenOverCallback;
import org.yanzi.camera.preview.CameraSurfaceView;
import org.yanzi.playcamera.R;
import org.yanzi.util.DisplayUtil;
import android.app.Activity;
import android.graphics.Point;
import android.os.Bundle;
import android.view.Menu;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageButton;
public class CameraActivity extends Activity implements CamOpenOverCallback {
private static final String TAG = "yanzi";
CameraSurfaceView surfaceView = null;
ImageButton shutterBtn;
float previewRate = -1f;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Thread openThread = new Thread(){
@Override
public void run() {
// TODO Auto-generated method stub
CameraInterface.getInstance().doOpenCamera(CameraActivity.this);
}
};
openThread.start();
setContentView(R.layout.activity_camera);
initUI();
initViewParams();
shutterBtn.setOnClickListener(new BtnListeners());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.camera, menu);
return true;
}
private void initUI(){
surfaceView = (CameraSurfaceView)findViewById(R.id.camera_surfaceview);
shutterBtn = (ImageButton)findViewById(R.id.btn_shutter);
}
private void initViewParams(){
LayoutParams params = surfaceView.getLayoutParams();
Point p = DisplayUtil.getScreenMetrics(this);
params.width = p.x;
params.height = p.y;
previewRate = DisplayUtil.getScreenRate(this); //預設全屏的比例預覽
surfaceView.setLayoutParams(params);
//手動設定拍照ImageButton的大小為120dip×120dip,原圖片大小是64×64
LayoutParams p2 = shutterBtn.getLayoutParams();
p2.width = DisplayUtil.dip2px(this, 80);
p2.height = DisplayUtil.dip2px(this, 80);;
shutterBtn.setLayoutParams(p2);
}
@Override
public void cameraHasOpened() {
// TODO Auto-generated method stub
SurfaceHolder holder = surfaceView.getSurfaceHolder();
CameraInterface.getInstance().doStartPreview(holder, previewRate);
}
private class BtnListeners implements OnClickListener{
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
switch(v.getId()){
case R.id.btn_shutter:
CameraInterface.getInstance().doTakePicture();
break;
default:break;
}
}
}
}
2、CameraInterface.java
package org.yanzi.camera;
import java.io.IOException;
import java.util.List;
import org.yanzi.util.CamParaUtil;
import org.yanzi.util.FileUtil;
import org.yanzi.util.ImageUtil;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.hardware.Camera.PictureCallback;
import android.hardware.Camera.ShutterCallback;
import android.hardware.Camera.Size;
import android.util.Log;
import android.view.SurfaceHolder;
public class CameraInterface {
private static final String TAG = "yanzi";
private Camera mCamera;
private Camera.Parameters mParams;
private boolean isPreviewing = false;
private float mPreviwRate = -1f;
private static CameraInterface mCameraInterface;
public interface CamOpenOverCallback{
public void cameraHasOpened();
}
private CameraInterface(){
}
public static synchronized CameraInterface getInstance(){
if(mCameraInterface == null){
mCameraInterface = new CameraInterface();
}
return mCameraInterface;
}
/**開啟Camera
* @param callback
*/
public void doOpenCamera(CamOpenOverCallback callback){
Log.i(TAG, "Camera open....");
mCamera = Camera.open();
Log.i(TAG, "Camera open over....");
callback.cameraHasOpened();
}
/**開啟預覽
* @param holder
* @param previewRate
*/
public void doStartPreview(SurfaceHolder holder, float previewRate){
Log.i(TAG, "doStartPreview...");
if(isPreviewing){
mCamera.stopPreview();
return;
}
if(mCamera != null){
mParams = mCamera.getParameters();
mParams.setPictureFormat(PixelFormat.JPEG);//設定拍照後儲存的圖片格式
CamParaUtil.getInstance().printSupportPictureSize(mParams);
CamParaUtil.getInstance().printSupportPreviewSize(mParams);
//設定PreviewSize和PictureSize
Size pictureSize = CamParaUtil.getInstance().getPropPictureSize(
mParams.getSupportedPictureSizes(),previewRate, 800);
mParams.setPictureSize(pictureSize.width, pictureSize.height);
Size previewSize = CamParaUtil.getInstance().getPropPreviewSize(
mParams.getSupportedPreviewSizes(), previewRate, 800);
mParams.setPreviewSize(previewSize.width, previewSize.height);
mCamera.setDisplayOrientation(90);
CamParaUtil.getInstance().printSupportFocusMode(mParams);
List<String> focusModes = mParams.getSupportedFocusModes();
if(focusModes.contains("continuous-video")){
mParams.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
mCamera.setParameters(mParams);
try {
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();//開啟預覽
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
isPreviewing = true;
mPreviwRate = previewRate;
mParams = mCamera.getParameters(); //重新get一次
Log.i(TAG, "最終設定:PreviewSize--With = " + mParams.getPreviewSize().width
+ "Height = " + mParams.getPreviewSize().height);
Log.i(TAG, "最終設定:PictureSize--With = " + mParams.getPictureSize().width
+ "Height = " + mParams.getPictureSize().height);
}
}
/**
* 停止預覽,釋放Camera
*/
public void doStopCamera(){
if(null != mCamera)
{
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
isPreviewing = false;
mPreviwRate = -1f;
mCamera.release();
mCamera = null;
}
}
/**
* 拍照
*/
public void doTakePicture(){
if(isPreviewing && (mCamera != null)){
mCamera.takePicture(mShutterCallback, null, mJpegPictureCallback);
}
}
/*為了實現拍照的快門聲音及拍照儲存照片需要下面三個回撥變數*/
ShutterCallback mShutterCallback = new ShutterCallback()
//快門按下的回撥,在這裡我們可以設定類似播放“咔嚓”聲之類的操作。預設的就是咔嚓。
{
public void onShutter() {
// TODO Auto-generated method stub
Log.i(TAG, "myShutterCallback:onShutter...");
}
};
PictureCallback mRawCallback = new PictureCallback()
// 拍攝的未壓縮原資料的回撥,可以為null
{
public void onPictureTaken(byte[] data, Camera camera) {
// TODO Auto-generated method stub
Log.i(TAG, "myRawCallback:onPictureTaken...");
}
};
PictureCallback mJpegPictureCallback = new PictureCallback()
//對jpeg影象資料的回撥,最重要的一個回撥
{
public void onPictureTaken(byte[] data, Camera camera) {
// TODO Auto-generated method stub
Log.i(TAG, "myJpegCallback:onPictureTaken...");
Bitmap b = null;
if(null != data){
b = BitmapFactory.decodeByteArray(data, 0, data.length);//data是位元組資料,將其解析成點陣圖
mCamera.stopPreview();
isPreviewing = false;
}
//儲存圖片到sdcard
if(null != b)
{
//設定FOCUS_MODE_CONTINUOUS_VIDEO)之後,myParam.set("rotation", 90)失效。
//圖片竟然不能旋轉了,故這裡要旋轉下
Bitmap rotaBitmap = ImageUtil.getRotateBitmap(b, 90.0f);
FileUtil.saveBitmap(rotaBitmap);
}
//再次進入預覽
mCamera.startPreview();
isPreviewing = true;
}
};
}
3、CameraSurfaceView.java
package org.yanzi.camera.preview;
import org.yanzi.camera.CameraInterface;
import android.content.Context;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private static final String TAG = "yanzi";
CameraInterface mCameraInterface;
Context mContext;
SurfaceHolder mSurfaceHolder;
public CameraSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
mContext = context;
mSurfaceHolder = getHolder();
mSurfaceHolder.setFormat(PixelFormat.TRANSPARENT);//translucent半透明 transparent透明
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mSurfaceHolder.addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// TODO Auto-generated method stub
Log.i(TAG, "surfaceCreated...");
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
// TODO Auto-generated method stub
Log.i(TAG, "surfaceChanged...");
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// TODO Auto-generated method stub
Log.i(TAG, "surfaceDestroyed...");
CameraInterface.getInstance().doStopCamera();
}
public SurfaceHolder getSurfaceHolder(){
return mSurfaceHolder;
}
}
4、CamParaUtil.java
package org.yanzi.util;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.hardware.Camera;
import android.hardware.Camera.Size;
import android.util.Log;
public class CamParaUtil {
private static final String TAG = "yanzi";
private CameraSizeComparator sizeComparator = new CameraSizeComparator();
private static CamParaUtil myCamPara = null;
private CamParaUtil(){
}
public static CamParaUtil getInstance(){
if(myCamPara == null){
myCamPara = new CamParaUtil();
return myCamPara;
}
else{
return myCamPara;
}
}
public Size getPropPreviewSize(List<Camera.Size> list, float th, int minWidth){
Collections.sort(list, sizeComparator);
int i = 0;
for(Size s:list){
if((s.width >= minWidth) && equalRate(s, th)){
Log.i(TAG, "PreviewSize:w = " + s.width + "h = " + s.height);
break;
}
i++;
}
if(i == list.size()){
i = 0;//如果沒找到,就選最小的size
}
return list.get(i);
}
public Size getPropPictureSize(List<Camera.Size> list, float th, int minWidth){
Collections.sort(list, sizeComparator);
int i = 0;
for(Size s:list){
if((s.width >= minWidth) && equalRate(s, th)){
Log.i(TAG, "PictureSize : w = " + s.width + "h = " + s.height);
break;
}
i++;
}
if(i == list.size()){
i = 0;//如果沒找到,就選最小的size
}
return list.get(i);
}
public boolean equalRate(Size s, float rate){
float r = (float)(s.width)/(float)(s.height);
if(Math.abs(r - rate) <= 0.03)
{
return true;
}
else{
return false;
}
}
public class CameraSizeComparator implements Comparator<Camera.Size>{
public int compare(Size lhs, Size rhs) {
// TODO Auto-generated method stub
if(lhs.width == rhs.width){
return 0;
}
else if(lhs.width > rhs.width){
return 1;
}
else{
return -1;
}
}
}
/**列印支援的previewSizes
* @param params
*/
public void printSupportPreviewSize(Camera.Parameters params){
List<Size> previewSizes = params.getSupportedPreviewSizes();
for(int i=0; i< previewSizes.size(); i++){
Size size = previewSizes.get(i);
Log.i(TAG, "previewSizes:width = "+size.width+" height = "+size.height);
}
}
/**列印支援的pictureSizes
* @param params
*/
public void printSupportPictureSize(Camera.Parameters params){
List<Size> pictureSizes = params.getSupportedPictureSizes();
for(int i=0; i< pictureSizes.size(); i++){
Size size = pictureSizes.get(i);
Log.i(TAG, "pictureSizes:width = "+ size.width
+" height = " + size.height);
}
}
/**列印支援的聚焦模式
* @param params
*/
public void printSupportFocusMode(Camera.Parameters params){
List<String> focusModes = params.getSupportedFocusModes();
for(String mode : focusModes){
Log.i(TAG, "focusModes--" + mode);
}
}
}
5、DisplayUtil.java
package org.yanzi.util;
import android.content.Context;
import android.graphics.Point;
import android.util.DisplayMetrics;
import android.util.Log;
public class DisplayUtil {
private static final String TAG = "DisplayUtil";
/**
* dip轉px
* @param context
* @param dipValue
* @return
*/
public static int dip2px(Context context, float dipValue){
final float scale = context.getResources().getDisplayMetrics().density;
return (int)(dipValue * scale + 0.5f);
}
/**
* px轉dip
* @param context
* @param pxValue
* @return
*/
public static int px2dip(Context context, float pxValue){
final float scale = context.getResources().getDisplayMetrics().density;
return (int)(pxValue / scale + 0.5f);
}
/**
* 獲取螢幕寬度和高度,單位為px
* @param context
* @return
*/
public static Point getScreenMetrics(Context context){
DisplayMetrics dm =context.getResources().getDisplayMetrics();
int w_screen = dm.widthPixels;
int h_screen = dm.heightPixels;
Log.i(TAG, "Screen---Width = " + w_screen + " Height = " + h_screen + " densityDpi = " + dm.densityDpi);
return new Point(w_screen, h_screen);
}
/**
* 獲取螢幕長寬比
* @param context
* @return
*/
public static float getScreenRate(Context context){
Point P = getScreenMetrics(context);
float H = P.y;
float W = P.x;
return (H/W);
}
}
6、FileUtil.java
package org.yanzi.util;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import android.graphics.Bitmap;
import android.os.Environment;
import android.util.Log;
public class FileUtil {
private static final String TAG = "FileUtil";
private static final File parentPath = Environment.getExternalStorageDirectory();
private static String storagePath = "";
private static final String DST_FOLDER_NAME = "PlayCamera";
/**初始化儲存路徑
* @return
*/
private static String initPath(){
if(storagePath.equals("")){
storagePath = parentPath.getAbsolutePath()+"/" + DST_FOLDER_NAME;
File f = new File(storagePath);
if(!f.exists()){
f.mkdir();
}
}
return storagePath;
}
/**儲存Bitmap到sdcard
* @param b
*/
public static void saveBitmap(Bitmap b){
String path = initPath();
long dataTake = System.currentTimeMillis();
String jpegName = path + "/" + dataTake +".jpg";
Log.i(TAG, "saveBitmap:jpegName = " + jpegName);
try {
FileOutputStream fout = new FileOutputStream(jpegName);
BufferedOutputStream bos = new BufferedOutputStream(fout);
b.compress(Bitmap.CompressFormat.JPEG, 100, bos);
bos.flush();
bos.close();
Log.i(TAG, "saveBitmap成功");
} catch (IOException e) {
// TODO Auto-generated catch block
Log.i(TAG, "saveBitmap:失敗");
e.printStackTrace();
}
}
}
7、ImageUtil.java
package org.yanzi.util;
import android.graphics.Bitmap;
import android.graphics.Matrix;
public class ImageUtil {
/**
* 旋轉Bitmap
* @param b
* @param rotateDegree
* @return
*/
public static Bitmap getRotateBitmap(Bitmap b, float rotateDegree){
Matrix matrix = new Matrix();
matrix.postRotate((float)rotateDegree);
Bitmap rotaBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), matrix, false);
return rotaBitmap;
}
}
幾點說明:
1、包括我之前的博文在內的大量網上鍊接,都是在Surfaceview create的時候進行開啟Camera的操作,在Surfaceview Changed的時候進行開預覽。而Surfaceview create的時候一定是在setContentView之後,Surfaceview例項化之後。為了優化開啟Camera時間,我再setContentView之前new了一個執行緒專門去Open Camera。經過測試,但就執行Camera.open()這句話一般需要140ms左右。如果放在主執行緒裡無疑是一種浪費。而在140ms之後,Surfaceview裡因為無需觸發關於Camera的操作,所以載入的特別快。也就是說Open完後,Surfaceview一定完成了例項化。所以我設定了CamOpenOverCallback回撥,在Camera開啟完畢後通知Activity立即執行開預覽的操作。
2、開預覽因為用Surfaceview預覽,需傳遞Surfaceview的SurfaceHolder。
3、CameraInterface是個單例模式,所有關於Camera的流程性操作一律封裝在這裡面。
4、Activity設定了全屏無標題且強制豎屏,像這種操作能再xml寫就不要再java程式碼里弄。
圖片資源上,雜家還真是一番精心挑選,對比了Camera360、相機360、美顏相機,UI上總的來說三個app感覺都很垃圾,都整的太複雜了,圖片也不好看。最後勉強用了相機360裡的一個button,自己想用PS把按鍵點選時的圖示色彩P亮點,一個沒留神,還給P的更暗了色彩。汗,不過按鍵的對比效果更明顯了。日後,會將一些OpenCV4Android的一些小demo都整合到PlayCamera系列。
下為效果圖:
------------本文系原創,轉載請註明作者yanzi1225627
版本號:PlayCamera_V1.0.0[2014-6-22].zip
百度雲盤: