Android Camera 自動適配多種螢幕,解決預覽照片拉伸和儲存的圖片拉伸
最近公司需要做一個手機自拍照的功能,由於之前有做過類似手機拍照的功能,所以很快就實現了自定義手機拍的功能。但是後面發現部分手機出現預覽照片拉伸和儲存的圖片拉伸的情況。然後百度了一下,發現原理很好理解,也有一堆demo,然而並沒有解決拉伸的情況。下面總結一下我的解決方法,希望對你有用。
一、原理
我們首先來理解主要的三個大小:
1、SurfaceView的大小,螢幕上顯示攝像頭拍攝的影象的view
2、Camera中的Preview的大小,預覽影象大小
3、Camera中的Picture的大小,最終儲存的照片的大小
預覽照片出現拉伸的原因就是SurfaceView的大小跟Preview的大小的寬高比率不一樣,這個很容易理解,你攝像機拍出來的圖片跟你用來顯示這張圖片的view的寬高比率不一樣,那必然會出現拉伸的效果的。
儲存的圖片拉伸的原理跟上面的一樣,明白原理也就有了解決方式了,那就是呼叫Camera.Params 的setPreviewSize方法和 setPictureSize方法,進行設定Preview和Picture的大小,讓他跟SurfaceView的大小的寬高比率一致或者相似,就不會出現拉伸的情況了。
二、解決方式
由於直接呼叫parameters.setPreviewSize(size.width, size.height); 設定Preview的大小是不會生效的,必須先獲取該裝置支援的大小列表,再篩選合適的大小進行設定。由於parameters.setPreviewSize()和parameters.setPictureSize()方法裡面的數值不能隨便設定;所以我的解決方法是:
1、先呼叫parameters.getSupportedPreviewSizes();獲取該手機支援的Preview的大小的集合,根據螢幕大小篩選出最佳的一個Preview的大小。
2、再根據Preview的大小設定SurfaceView的大小,這樣Preview跟SurfaceView的寬高比率相似,則不會出現拉伸。
3、最後設定Picture大小,類似Preview,獲取裝置支援的大小列表,再篩選合適的大小進行設定。
三、程式碼,裡面都有註釋
Activity
public class Camera3Activity extends UIActivity { private FrameLayout camera_preview; private MySurfaceView cameraSurfaceView; private Camera camera; private Point mScreenResolution;//螢幕解析度 private Point previewSizeOnScreen;//相機預覽尺寸 private Point pictureSizeOnScreen;//圖片尺寸 private Bitmap bitmapCamera = null; private ImageView img_camera; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_camera3); camera_preview = (FrameLayout) findViewById(R.id.camera_preview); img_camera = (ImageView) findViewById(R.id.img_camera); findViewById(R.id.btn_start).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (camera != null) { // 控制攝像頭自動對焦後才拍攝 //關閉聲音 CameraUtil.setCameraSound(true, mContext); camera.takePicture(null, null, jpeg); } } }); initCamera(); init(); } private void initCamera() { // 此處預設開啟後置攝像頭 // 通過傳入引數可以開啟前置攝像頭 //判斷系統版本大於23,即24(7.0)和以上版本提示開啟許可權 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { String[] permissions = {Manifest.permission.CAMERA}; int check = ContextCompat.checkSelfPermission(mContext, permissions[0]); // 許可權是否已經 授權 GRANTED---授權 DINIED---拒絕 if (check == PackageManager.PERMISSION_GRANTED) { //呼叫相機 camera = CameraUtil.openFrontFacingCameraGingerbread(); } else { requestPermissions(new String[]{Manifest.permission.CAMERA}, 1); } } else { camera = CameraUtil.openFrontFacingCameraGingerbread(); } if (camera == null) { ToastUtil.MyToast(mContext, "攝像頭被佔用,攝像頭許可權沒開啟!"); return; } setCameraParameters(camera, camera.getParameters()); } private void init() { cameraSurfaceView = new MySurfaceView(mContext, camera); //設定介面展示大小 Point point = CameraUtils.calculateViewSize(previewSizeOnScreen, mScreenResolution); System.out.println(point.x + "," + point.y); FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(point.x, point.y); layoutParams.gravity = Gravity.CENTER; cameraSurfaceView.setLayoutParams(layoutParams); camera_preview.addView(cameraSurfaceView); } private void setCameraParameters(Camera camera, Camera.Parameters parameters) { WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); Display display = windowManager.getDefaultDisplay(); Point theScreenResolution = new Point(); display.getSize(theScreenResolution);//得到螢幕的尺寸,單位是畫素 mScreenResolution = theScreenResolution; previewSizeOnScreen = CameraUtils.findBestPreviewSizeValue(parameters, theScreenResolution);//通過相機尺寸、螢幕尺寸來得到最好的展示尺寸,此尺寸為相機的 parameters.setPreviewSize(previewSizeOnScreen.x, previewSizeOnScreen.y); pictureSizeOnScreen = CameraUtils.findBestPictureSizeValue(parameters, theScreenResolution);//通過相機尺寸、螢幕尺寸來得到最好的展示尺寸,此尺寸為相機的 parameters.setPictureSize(pictureSizeOnScreen.x, pictureSizeOnScreen.y); boolean isScreenPortrait = mScreenResolution.x < mScreenResolution.y; boolean isPreviewSizePortrait = previewSizeOnScreen.x < previewSizeOnScreen.y; if (isScreenPortrait != isPreviewSizePortrait) {//相機與螢幕一個方向,則使用相機尺寸 previewSizeOnScreen = new Point(previewSizeOnScreen.y, previewSizeOnScreen.x);//否則翻個 } // 設定照片的格式 parameters.setPictureFormat(ImageFormat.JPEG); CameraUtils.setFocus(parameters, true, false, true);//設定相機對焦模式 CameraUtils.setBarcodeSceneMode(parameters, Camera.Parameters.SCENE_MODE_BARCODE);//設定相機場景模式 CameraUtils.setBestPreviewFPS(parameters);//設定相機幀數 camera.setParameters(parameters); // 系統相機預設是橫屏的,我們要旋轉90° camera.setDisplayOrientation(90); } //建立jpeg圖片回撥資料物件 Camera.PictureCallback jpeg = new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { // TODO Auto-generated method stub try { bitmapCamera = BitmapFactory.decodeByteArray(data, 0, data.length); bitmapCamera = BitmapUtil.rotateImage(bitmapCamera); zipBitmap(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } camera.stopPreview();//關閉預覽 處理資料 CameraUtil.setCameraSound(false, mContext); } }; private void zipBitmap() { try { //bitmapCamera = BitmapUtil.zipBitmap(bitmapCamera, 100); img_camera.setImageBitmap(bitmapCamera); File file = FileUtil.saveImagePath(mContext, "et", "image"); if (file != null) { FileOutputStream fileOutStream = null; fileOutStream = new FileOutputStream(file); //把點陣圖輸出到指定的檔案中 bitmapCamera.compress(Bitmap.CompressFormat.JPEG, 100, fileOutStream); fileOutStream.close(); System.out.println("**********圖片儲存成功***********"); } } catch (IOException e) { e.printStackTrace(); } } @Override protected void onDestroy() { super.onDestroy(); //回收資料 if (camera != null) { camera.stopPreview(); camera.release(); camera = null; } if (bitmapCamera != null) { bitmapCamera.recycle();//回收bitmap空間 bitmapCamera = null; } } }
佈局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<FrameLayout
android:id="@+id/camera_preview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center_horizontal" />
<ImageView
android:id="@+id/img_camera"
android:layout_width="100dp"
android:layout_height="200dp"
android:layout_alignParentRight="true"
android:scaleType="centerCrop" />
<Button
android:id="@+id/btn_start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="start" />
</RelativeLayout>
自定義MySurfaceView
/**
* Created by jin on 2018/9/20.
*/
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private SurfaceHolder mHolder;
private Camera mCamera;
public MySurfaceView(Context context, Camera camera) {
super(context);
mCamera = camera;
mHolder = getHolder();
mHolder.addCallback(this);
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
try {
//攝像頭繫結view
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
if (mHolder.getSurface() == null){
// preview surface does not exist
return;
}
try {
mCamera.stopPreview();
} catch (Exception e){
// ignore: tried to stop a non-existent preview
}
try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (Exception e){
}
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
}
}
工具類CameraUtils,核心演算法
public final class CameraUtils {
private static final String TAG = "CameraUtils";
private static final int MIN_PREVIEW_PIXELS = 480 * 320; // normal screen
private static final double MAX_ASPECT_DISTORTION = 0.15;
private static final int MIN_FPS = 10;
private static final int MAX_FPS = 20;
private CameraUtils() {
}
/**
* set camera focus
*
* @param parameters
* @param autoFocus
* @param disableContinuous
* @param safeMode
*/
public static void setFocus(Camera.Parameters parameters,
boolean autoFocus,
boolean disableContinuous,
boolean safeMode) {
List<String> supportedFocusModes = parameters.getSupportedFocusModes();
String focusMode = null;
if (autoFocus) {
if (safeMode || disableContinuous) {
focusMode = findSettableValue("focus mode",
supportedFocusModes,
Camera.Parameters.FOCUS_MODE_AUTO);
} else {
focusMode = findSettableValue("focus mode",
supportedFocusModes,
Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO,
Camera.Parameters.FOCUS_MODE_AUTO);
}
}
// Maybe selected auto-focus but not available, so fall through here:
if (!safeMode && focusMode == null) {
focusMode = findSettableValue("focus mode",
supportedFocusModes,
Camera.Parameters.FOCUS_MODE_MACRO,
Camera.Parameters.FOCUS_MODE_EDOF);
}
if (focusMode != null) {
if (focusMode.equals(parameters.getFocusMode())) {// if camera already set focusMode equlas focusMode,no need to reset
} else {
parameters.setFocusMode(focusMode);
}
}
}
public static void setBestPreviewFPS(Camera.Parameters parameters) {
setBestPreviewFPS(parameters, MIN_FPS, MAX_FPS);
}
/**
* set camera fps(幀數)
*
* @param parameters
* @param minFPS
* @param maxFPS
*/
public static void setBestPreviewFPS(Camera.Parameters parameters, int minFPS, int maxFPS) {
List<int[]> supportedPreviewFpsRanges = parameters.getSupportedPreviewFpsRange();
if (supportedPreviewFpsRanges != null && !supportedPreviewFpsRanges.isEmpty()) {
int[] suitableFPSRange = null;
for (int[] fpsRange : supportedPreviewFpsRanges) {
int thisMin = fpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
int thisMax = fpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX];
if (thisMin >= minFPS * 1000 && thisMax <= maxFPS * 1000) {
suitableFPSRange = fpsRange;
break;
}
}
if (suitableFPSRange == null) {
} else {
int[] currentFpsRange = new int[2];
parameters.getPreviewFpsRange(currentFpsRange);
if (Arrays.equals(currentFpsRange, suitableFPSRange)) {
} else {
parameters.setPreviewFpsRange(suitableFPSRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
suitableFPSRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
}
}
}
}
/**
* set camera sceneMode
*
* @param parameters
* @param modes
*/
public static void setBarcodeSceneMode(Camera.Parameters parameters, String... modes) {
String sceneMode = findSettableValue("scene mode",
parameters.getSupportedSceneModes(),
modes);
if (sceneMode != null) {
parameters.setSceneMode(sceneMode);
}
}
/**
* find best previewSize value,on the basis of camera supported previewSize and screen size
*
* @param parameters
* @param screenResolution
* @return
*/
public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) {
List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
if (rawSupportedSizes == null) {
Log.w(TAG, "Device returned no supported preview sizes; using default");
Camera.Size defaultSize = parameters.getPreviewSize();
if (defaultSize == null) {
throw new IllegalStateException("Parameters contained no preview size!");
}
return new Point(defaultSize.width, defaultSize.height);
}
// Sort by size, descending
List<Camera.Size> supportedPreviewSizes = new ArrayList<>(rawSupportedSizes);
Collections.sort(supportedPreviewSizes, new Comparator<Camera.Size>() {
@Override
public int compare(Camera.Size a, Camera.Size b) {
int aPixels = a.height * a.width;
int bPixels = b.height * b.width;
if (bPixels < aPixels) {
return -1;
}
if (bPixels > aPixels) {
return 1;
}
return 0;
}
});
if (Log.isLoggable(TAG, Log.INFO)) {//檢查是否可以輸出日誌
StringBuilder previewSizesString = new StringBuilder();
for (Camera.Size supportedPreviewSize : supportedPreviewSizes) {
previewSizesString.append(supportedPreviewSize.width).append('x')
.append(supportedPreviewSize.height).append(' ');
}
Log.i(TAG, "Supported preview sizes: " + previewSizesString);
}
double screenAspectRatio;
if (screenResolution.x > screenResolution.y) {
screenAspectRatio = screenResolution.x / (double) screenResolution.y;//螢幕尺寸比例
} else {
screenAspectRatio = screenResolution.y / (double) screenResolution.x;//螢幕尺寸比例
}
// Remove sizes that are unsuitable
Iterator<Camera.Size> it = supportedPreviewSizes.iterator();
while (it.hasNext()) {
Camera.Size supportedPreviewSize = it.next();
int realWidth = supportedPreviewSize.width;
int realHeight = supportedPreviewSize.height;
if (realWidth * realHeight < MIN_PREVIEW_PIXELS) {//delete if less than minimum size
it.remove();
continue;
}
//camera preview width > height
boolean isCandidatePortrait = realWidth < realHeight;//width less than height
int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight;//ratio for camera
double distortion = Math.abs(aspectRatio - screenAspectRatio);//returan absolute value
if (distortion > MAX_ASPECT_DISTORTION) {//delete if distoraion greater than 0.15
it.remove();
continue;
}
if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {//serceen size equal to camera supportedPreviewSize
Point exactPoint = new Point(realWidth, realHeight);
Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint);
return exactPoint;
}
}
if (!supportedPreviewSizes.isEmpty()) {//default return first supportedPreviewSize,mean largest
Camera.Size largestPreview = supportedPreviewSizes.get(0);
Point largestSize = new Point(largestPreview.width, largestPreview.height);
Log.i(TAG, "Using largest suitable preview size: " + largestSize);
return largestSize;
}
// If there is nothing at all suitable, return current preview size
Camera.Size defaultPreview = parameters.getPreviewSize();
if (defaultPreview == null) {
throw new IllegalStateException("Parameters contained no preview size!");
}
Point defaultSize = new Point(defaultPreview.width, defaultPreview.height);
Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize);
return defaultSize;
}
/**
* find best pictureSize value,on the basis of camera supported pictureSize and screen size
*
* @param parameters
* @param screenResolution
* @return
*/
public static Point findBestPictureSizeValue(Camera.Parameters parameters, Point screenResolution) {
List<Camera.Size> rawSupportedSizes = parameters.getSupportedPictureSizes();
if (rawSupportedSizes == null) {
Log.w(TAG, "Device returned no supported preview sizes; using default");
Camera.Size defaultSize = parameters.getPictureSize();
if (defaultSize == null) {
throw new IllegalStateException("Parameters contained no preview size!");
}
return new Point(defaultSize.width, defaultSize.height);
}
// Sort by size, descending
List<Camera.Size> supportedPreviewSizes = new ArrayList<>(rawSupportedSizes);
Collections.sort(supportedPreviewSizes, new Comparator<Camera.Size>() {
@Override
public int compare(Camera.Size a, Camera.Size b) {
int aPixels = a.height * a.width;
int bPixels = b.height * b.width;
if (bPixels < aPixels) {
return -1;
}
if (bPixels > aPixels) {
return 1;
}
return 0;
}
});
if (Log.isLoggable(TAG, Log.INFO)) {//檢查是否可以輸出日誌
StringBuilder previewSizesString = new StringBuilder();
for (Camera.Size supportedPreviewSize : supportedPreviewSizes) {
previewSizesString.append(supportedPreviewSize.width).append('x')
.append(supportedPreviewSize.height).append(' ');
}
Log.i(TAG, "Supported picture sizes: " + previewSizesString);
}
double screenAspectRatio;
if (screenResolution.x > screenResolution.y) {
screenAspectRatio = screenResolution.x / (double) screenResolution.y;//螢幕尺寸比例
} else {
screenAspectRatio = screenResolution.y / (double) screenResolution.x;//螢幕尺寸比例
}
// Remove sizes that are unsuitable
Iterator<Camera.Size> it = supportedPreviewSizes.iterator();
while (it.hasNext()) {
Camera.Size supportedPreviewSize = it.next();
int realWidth = supportedPreviewSize.width;
int realHeight = supportedPreviewSize.height;
if (realWidth * realHeight < MIN_PREVIEW_PIXELS) {//delete if less than minimum size
it.remove();
continue;
}
//camera preview width > height
boolean isCandidatePortrait = realWidth < realHeight;//width less than height
int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight;//ratio for camera
double distortion = Math.abs(aspectRatio - screenAspectRatio);//returan absolute value
if (distortion > MAX_ASPECT_DISTORTION) {//delete if distoraion greater than 0.15
it.remove();
continue;
}
if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {//serceen size equal to camera supportedPreviewSize
Point exactPoint = new Point(realWidth, realHeight);
Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint);
return exactPoint;
}
}
if (!supportedPreviewSizes.isEmpty()) {//default return first supportedPreviewSize,mean largest
Camera.Size largestPreview = supportedPreviewSizes.get(0);
Point largestSize = new Point(largestPreview.width, largestPreview.height);
Log.i(TAG, "Using largest suitable preview size: " + largestSize);
return largestSize;
}
// If there is nothing at all suitable, return current preview size
Camera.Size defaultPreview = parameters.getPictureSize();
if (defaultPreview == null) {
throw new IllegalStateException("Parameters contained no preview size!");
}
Point defaultSize = new Point(defaultPreview.width, defaultPreview.height);
Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize);
return defaultSize;
}
/**
* find seetable value from supportedValues for desiredValues
*
* @param name
* @param supportedValues
* @param desiredValues
* @return
*/
private static String findSettableValue(String name,
Collection<String> supportedValues,
String... desiredValues) {
Log.i(TAG, "Requesting " + name + " value from among: " + Arrays.toString(desiredValues));
Log.i(TAG, "Supported " + name + " values: " + supportedValues);
if (supportedValues != null) {
for (String desiredValue : desiredValues) {
if (supportedValues.contains(desiredValue)) {
Log.i(TAG, "Can set " + name + " to: " + desiredValue);
return desiredValue;
}
}
}
Log.i(TAG, "No supported values match");
return null;
}
/**
* 根據相機預覽尺寸、控制元件可展示最大尺寸來計算控制元件的展示尺寸,防止影象變形
*
* @param previewSizeOnScreen
* @param maxSizeOnView
* @return
*/
public static Point calculateViewSize(Point previewSizeOnScreen, Point maxSizeOnView) {
Point point = new Point();
float ratioPreview = (float) previewSizeOnScreen.x / (float) previewSizeOnScreen.y;//相機預覽比率
float ratioMaxView = (float) maxSizeOnView.x / (float) maxSizeOnView.y;//控制元件比率
if (ratioPreview > ratioMaxView) {//x>y,以控制元件寬為標準,縮放高
point.x = maxSizeOnView.x;
point.y = (int) (maxSizeOnView.x / ratioPreview);
} else {
point.y = maxSizeOnView.y;
point.x = (int) (maxSizeOnView.y * ratioPreview);
}
return point;
}
}