1. 程式人生 > >Android Camera 自動適配多種螢幕,解決預覽照片拉伸和儲存的圖片拉伸

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;
    }
}