Android - 如何在竖屏方向使用相机的getSupportedPreviewSizes()函数

17

我正在尝试在一个活动中嵌入摄像头预览。并且它只能在纵向方向上显示。问题是预览会被拉伸。

我尝试选择最佳大小。但问题是从getSupportedPreviewSizes()返回的所有支持的预览尺寸都是横向方向的。因此,根据我的代码选择正确的尺寸可能行不通。

我的布局XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_take_attendance"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:orientation="vertical"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.lab.rafael.smartattendance.TakeAttendanceActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/take_attendance_label"
        android:id="@+id/take_attendance_label"
        android:layout_marginBottom="@dimen/activity_vertical_margin"/>

    <!-- camera preview container -->
    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="@color/red"
        android:id="@+id/take_attendance_scan_qr_frame"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="@string/take_attendance_manual_text"
            />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/take_attendance_manual_button"
            android:id="@+id/take_attendance_manual_button"/>
    </LinearLayout>
</LinearLayout>

这是我的CameraPreview类:

package com.lab.rafael.smartattendance.camera;

import android.content.Context;
import android.hardware.Camera;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
import java.util.List;

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
    private Camera mCamera = null;
    private SurfaceHolder mHolder = null;
    private Camera.Size optimalSize = null;

    public CameraPreview(Context context, Camera camera)
    {
        super(context);
        mCamera = camera;
        mHolder = getHolder();
        mHolder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            Camera.Parameters params = mCamera.getParameters();
            List<String> focusModes = params.getSupportedFocusModes();
            mCamera.setDisplayOrientation(90);
            mCamera.setPreviewDisplay(holder);

            if(focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
                params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
            }

            if(optimalSize != null) {
                params.setPreviewSize(optimalSize.width, optimalSize.height);
            }

            mCamera.setParameters(params);

            mCamera.startPreview();
        } catch (IOException e)
        {
            Log.e("created_error", e.getMessage());
        }

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        if(mHolder.getSurface() == null) {
            return;
        }

        try {
            mCamera.stopPreview();
        } catch (Exception e) {
            Log.e("changed_error", e.getMessage());
        }

        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e){
            Log.e("error", e.getMessage());
        }
    }

    @Override
    public void onMeasure(int measureWidthSpec, int measureHeightSpec) {
        optimalSize = getOptimalSize(MeasureSpec.getSize(measureWidthSpec), MeasureSpec.getSize(measureHeightSpec));
        setMeasuredDimension(optimalSize.width, optimalSize.height);
    }

    protected Camera.Size getOptimalSize(int width, int height) {
        List<Camera.Size> supportedSizes = mCamera.getParameters().getSupportedPreviewSizes();
        double targetRatio = (double) width / height,
                optimalRatio = 0.0,
                acceptableRatioMargin = 0.1,
                minDiff = Double.MAX_VALUE;


        for(Camera.Size size : supportedSizes) {
            optimalRatio = (double) size.width / size.height;
            if(Math.abs(optimalRatio - targetRatio) < acceptableRatioMargin) {
                if(Math.abs(height - size.height) < minDiff) {
                    minDiff = Math.abs(height - size.height);
                    optimalSize = size;
                }
            }
        }

        if(optimalSize == null) {
            for(Camera.Size size : supportedSizes) {
                if(Math.abs(height - size.height) <= minDiff) {
                    minDiff = Math.abs(height - size.height);
                    optimalSize = size;
                }
            }
        }

        return optimalSize;
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
    }
}
以下图片是由以下数值生成的:
Specified resolution from measureSpecWidth/Height = `984x1335`

Returned from getOptimalSize() = `1600x1200`.

因为提供的supportedPreviewSizes是针对横向而不是纵向的。

这是结果:

输入图像描述

4个回答

9

我大约一年前遇到了与你相同的问题。此外,我还需要处理前置和后置摄像头。我不太记得代码的细节,但在发布这个答案之前,我已经试过它,而且它仍然像魔法一样运行。
希望你能查找并与你的代码进行比较。如果你需要,我可以分享更多的代码;)

/**
 * A simple wrapper around a Camera and a SurfaceView that renders a centered preview of the Camera
 * to the surface. We need to center the SurfaceView because not all devices have cameras that
 * support preview sizes at the same aspect ratio as the device's display.
 */
public class Preview extends ViewGroup implements SurfaceHolder.Callback {

    SurfaceView mSurfaceView;
    SurfaceHolder mHolder;
    Camera.Size mPreviewSize;
    List<Camera.Size> mSupportedPreviewSizes;
    Camera mCamera;
    private Context context;
    private int mCameraId;
    public boolean use_front_camera;

    public Preview(Context context, int cameraId) {
        super(context);

        this.context = context;
        mCameraId = cameraId;
        use_front_camera = true;

        mSurfaceView = new SurfaceView(context);
        addView(mSurfaceView);

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = mSurfaceView.getHolder();
        mHolder.addCallback(this);
    }

    public void setCamera(Camera camera) {
        mCamera = camera;
        if (mCamera != null) {
            mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();
            requestLayout();
        }
    }

    public void switchCamera(Camera camera) {
        setCamera(camera);
        try {
            camera.setPreviewDisplay(mHolder);
        } catch (IOException exception) {
            android.util.Log.e(IdelityConstants.DEBUG_IDELITY_KEY_LOG, "IOException caused by setPreviewDisplay()", exception);
        }
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
        requestLayout();

        camera.setParameters(parameters);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // We purposely disregard child measurements because act as a
        // wrapper to a SurfaceView that centers the camera preview instead
        // of stretching it.

        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        //MUST CALL THIS
        setMeasuredDimension(width, height);

        if (mSupportedPreviewSizes != null) {
            mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, width, height);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed && getChildCount() > 0) {
            final View child = getChildAt(0);

            final int width = r - l;
            final int height = b - t;

            int previewWidth = width;
            int previewHeight = height;
            if (mPreviewSize != null) {
                /**
                 * Como el calculo se hace con la cámara en modo landscape y luego toca
                 * girar la cámara para que se vea bien, se pasan los valores cambiados.
                 */
                previewWidth = mPreviewSize.height;
                previewHeight = mPreviewSize.width;
            }

            // Center the child SurfaceView within the parent.
            if (width * previewHeight < height * previewWidth) {
                final int scaledChildWidth = previewWidth * height / previewHeight;
                child.layout((width - scaledChildWidth) / 2, 0,
                    (width + scaledChildWidth) / 2, height);
            } else {
                final int scaledChildHeight = previewHeight * width / previewWidth;
                child.layout(0, (height - scaledChildHeight) / 2,
                    width, (height + scaledChildHeight) / 2);
            }
        }
    }

    public void surfaceCreated(SurfaceHolder holder) {
        // The Surface has been created, acquire the camera and tell it where
        // to draw.
        try {
            if (mCamera != null) {
                mCamera.setPreviewDisplay(holder);
            }
        } catch (IOException exception) {
            android.util.Log.e(IdelityConstants.DEBUG_IDELITY_KEY_LOG, "IOException caused by setPreviewDisplay()", exception);
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface will be destroyed when we return, so stop the preview.
    //        if (mCamera != null) {
    //            mCamera.stopPreview();
    //        }
    }


    private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
        final double ASPECT_TOLERANCE = 0.1;
        double targetRatio = (double) w / h;
        if (sizes == null) return null;

        Camera.Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;

        int targetHeight = h;

        // Try to find an size match aspect ratio and size
        for (Camera.Size size : sizes) {
            double ratio = (double) size.width / size.height;
            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }

        // Cannot find the one match the aspect ratio, ignore the requirement
        if (optimalSize == null) {
            minDiff = Double.MAX_VALUE;
            for (Camera.Size size : sizes) {
                if (Math.abs(size.height - targetHeight) < minDiff) {
                    optimalSize = size;
                    minDiff = Math.abs(size.height - targetHeight);
                }
            }
        }
        return optimalSize;
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // Now that the size is known, set up the camera parameters and begin    
        // the preview.

        if (mCamera == null)
            return;

        Camera.Parameters parameters = mCamera.getParameters();
        parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
        parameters.setJpegQuality(100);
        parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);


        List<Camera.Size> sizes = parameters.getSupportedPictureSizes();
        Camera.Size size = sizes.get(0);
        for(int i=0;i<sizes.size();i++)
        {
            if(sizes.get(i).width > size.width)
                size = sizes.get(i);
        }
        parameters.setPictureSize(size.width, size.height);


        requestLayout();


        mCamera.setParameters(parameters);
            mCamera.setDisplayOrientation(getCameraDisplayOrientation((FragmentActivity)context, mCameraId));
        mCamera.startPreview();
    }


    public static int getCameraDisplayOrientation(FragmentActivity activity, int cameraId) {
        Camera.CameraInfo info = new Camera.CameraInfo();

        Camera.getCameraInfo(cameraId, info);
        int rotation = activity.getWindowManager().getDefaultDisplay().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;
        }


        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror
        }
        else {  // back-facing
            result = (info.orientation - degrees + 360) % 360;
        }

        return result;
    }


    /** A safe way to get an instance of the Camera object. */
    public static Camera getCameraInstance(int cameraIndex){
        Camera c = null;
        try {
            c = Camera.open(cameraIndex); // attempt to get a Camera instance
        }
        catch (Exception e){
            // Camera is not available (in use or does not exist)
            android.util.Log.e(IdelityConstants.ERROR_IDELITY_KEY_LOG, "Camera is not available: " + e.getMessage());
        }
        return c; // returns null if camera is unavailable
    }
}



这里是XML代码,它很简单(在截图中可以看到)。唯一重要的是具有ID为capture_evidence_camera_preview的FrameLayout。

<RelativeLayout
    android:layout_width="fill_parent"
    android:layout_height="0dp"
    android:id="@+id/capture_evidence_linearLayout_camera"
    android:layout_weight="3"
    android:layout_gravity="center_horizontal">


    <FrameLayout
        android:id="@+id/capture_evidence_camera_preview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"/>

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/capture_evidence_default_text_number_evidence"
        android:id="@+id/capture_evidence_textView_value_typed"
        android:textSize="50sp"
        android:textColor="@color/idelity_blanco"
        android:gravity="center_horizontal"
        android:paddingLeft="5dp"
        android:paddingRight="5dp"
        android:background="#d2000000"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:paddingTop="8dp"
        android:paddingBottom="8dp" />
</RelativeLayout>


<RelativeLayout
    android:layout_width="fill_parent"
    android:layout_height="0dp"
    android:layout_weight="1">

    <net.idelity.idelitymobile.ui.helpers.IdelityButton
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:text="@string/button_back"
        android:id="@+id/capture_evidence_button_cancel"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:background="@drawable/button_gray"
        android:textColor="@color/idelity_blanco"
        android:textSize="20sp"
        android:paddingLeft="40dp"
        android:paddingRight="40dp"
        android:textStyle="bold" />

    <net.idelity.idelitymobile.ui.helpers.IdelityButton
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:id="@+id/capture_evidence_button_capture_evidence"
        android:layout_alignParentBottom="true"
        android:layout_toRightOf="@+id/capture_evidence_button_cancel"
        android:layout_toEndOf="@+id/capture_evidence_button_cancel"
        android:background="@drawable/take_photo_button_camera"
        android:textSize="25sp"
        android:textColor="@color/idelity_blanco"
        android:textStyle="bold"
        android:text="@string/capture_evidence_button_capture_evidence"
        android:paddingBottom="10dp" />
</RelativeLayout>

XML预览

这是在FragmentActivity下使用的(如果需要,我可以分享给你)。


谢谢,不知道你能否分享这个活动的XML文件? - Rafael Adel
当然,给我2个小时(驾车)。 - MiguelHincapieC
告诉我你是否已经理解这个与编程有关的内容,如果你需要其他任何帮助,请告诉我。 - MiguelHincapieC

4

简而言之:getSupportedPreviewSizes()setPreviewSize(int width, int height)中使用的尺寸是相机原始方向的尺寸,这可能(并通常)与手机自然方向和当前显示方向不同。

因此,当它们处于侧面时(并且由于这个原因使用了1/ratio),getOptimalSize(int, int)方法循环遍历这些尺寸,没有选择任何尺寸,并在方法中第二次循环后基于高度选择了错误的比例,导致图像变形。


显然,支持的尺寸总是指相机在其自然角度下的大小(尽管文档没有告诉我们)。相机的自然角度通常与手机的自然角度不同。您可以使用CameraInfo.orientation字段检查它们之间的差异。
除了试用之外,暗示这一点的文档也解决了您的疑惑:Camera.Parameters.setPreviewSize(int width, int height)
“宽度和高度的边是基于相机方向的。也就是说,预览大小是在显示方向旋转之前的大小。因此,应用程序在设置预览大小时需要考虑显示方向。例如,假设相机支持480x320和320x480两种预览尺寸。应用程序想要3:2的预览比例。如果将显示方向设置为0或180,则应将预览大小设置为480x320。如果将显示方向设置为90或270,则应将预览大小设置为320x480。在设置图片大小和缩略图大小时,还应考虑显示方向。”
此处的文档
我们可以从中学到几件事情:
  1. The sizes you get are supposed to be the same no matter what the display/phone orientation is, so there is nothing wrong with the values you see there. You should turn them on their side in order to pick the best one for the onMeasure() method to measure the view in a portrait orientation (based on the screen and space you want the preview to occupy).

    Ideally - turn them after you confirmed the camera's mounting angle and the current phone's angle are not compatible (one landscape and one portrait).

    //in getOptimalSize(int width, int height)
             //isCameraOnSide() is a new method you should implement
             //return true iff the camera is mounted on the side compared to 
             //the phone's natural orientation.
    double targetRatio = (isCameraOnSide()) ?  (double) height / width 
                            : (double) width / height,
            optimalRatio = 0.0,
            acceptableRatioMargin = 0.1,
            minDiff = Double.MAX_VALUE;
    
    
    for(Camera.Size size : supportedSizes) {
        optimalRatio = (double) size.width / size.height;
        if(Math.abs(optimalRatio - targetRatio) < acceptableRatioMargin) {
            if(Math.abs(height - size.height) < minDiff) {
                minDiff = Math.abs(height - size.height);
                optimalSize = size;
            }
        }
    }
    

    In your and my cases isCameraOnSide() returns true - as we can see from your line of setPreviewOrientation(90). For a more general implementation, here's one based google's Camera2Basic sample:

    private boolean isCameraOnSide(){
        int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        //Inquire the sensor's orientation relative to the natural phone's orientation
        android.hardware.Camera.CameraInfo info =
            new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(0, info); //Back-facing camera
        int sensorOrientation = info.orientation;
    
        boolean swappedDimensions = false;
        switch (displayRotation) {
            case Surface.ROTATION_0:
            case Surface.ROTATION_180:
                if (sensorOrientation == 90 || sensorOrientation == 270) {
                    swappedDimensions = true;
                }
                break;
            case Surface.ROTATION_90:
            case Surface.ROTATION_270:
                if (sensorOrientation == 0 || sensorOrientation == 180) {
                    swappedDimensions = true;
                }
                break;
            default:
                Log.e(TAG, "Display rotation is invalid: " + displayRotation);
        }
    
        return swappedDimensions;
    }
    
  2. And more importantly: If you use the Camera.Parameters.getPreviewSize() method as a watch or in a Log I think you will see that it is set to a different ratio than the one of the size picked by the setMearuseDimension(int, int) method. This difference in ratios is the origin of the stretch/squash (it looks squashed vertically in your picture. That can also be a hint that the distortion is not from a landscape/portrait confusion, as a landscape picture in portrait view would be stretched vertically rather than squashed). After choosing the right size for the view (in this case SurfaceView), you should call Camera.Parameters.setPreviewSize(int width, int height) with a supported preview size that has the same ratio as the size you used for the view (again, width according to the camera, not the current phone/display orientation. That means it might go into the height parameter).

    For example, you could do that in surfaceCreated and surfaceChanged methods (worked for me). Make sure the preview is not on when you set the camera's preview size and start it (or re-start it) after you do:

        //inside surfaceCreated(SurfaceHolder holder)
        Camera.Parameters params = mCamera.getParameters(); 
        Camera.Size prevSize = getOptimalSize(getWidth(), getHeight()); 
             //prevSize should be still in the camera's orientation. In your and my cases - landscape
        params.setPreviewSize(prevSize.width, prevSize.height);
        mCamera.setParameters(params);
    
        mCamera.setPreviewDisplay(holder);
        mCamera.startPreview();
    

0

我已经从事相机应用程序的开发一段时间了,有很多要考虑的事情,但让我们保持简单。

  1. 尝试将目标视图大小设置为与受支持的预览大小之一具有相同的宽高比(3:2、16:9、4:3)。如果无法实现,请尝试选择一个宽高比差异最小的预览大小。
  2. 选择适当的视图大小后,您可以通过在CameraPreview上覆盖onLayout()来将其居中放置在活动中。

0
public static Camera.Size determineBestPreviewSize(Camera.Parameters parameters) {
    List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();
    return determineBestSize(sizes);
}

public static Camera.Size determineBestPictureSize(Camera.Parameters parameters) {
    List<Camera.Size> sizes = parameters.getSupportedPictureSizes();
    return determineBestSize(sizes);
}

protected static Camera.Size determineBestSize(List<Camera.Size> sizes) {
    Camera.Size bestSize = null;
    long used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
    long availableMemory = Runtime.getRuntime().maxMemory() - used;
    for (Camera.Size currentSize : sizes) {
        int newArea = currentSize.width * currentSize.height;
        long neededMemory = newArea * 4 * 4; // newArea * 4 Bytes/pixel * 4 needed copies of the bitmap (for safety :) )
        boolean isDesiredRatio = (currentSize.width / 4) == (currentSize.height / 3);
        boolean isBetterSize = (bestSize == null || currentSize.width > bestSize.width);
        boolean isSafe = neededMemory < availableMemory;
        if (isDesiredRatio && isBetterSize && isSafe) {
            bestSize = currentSize;
        }
    }
    if (bestSize == null) {
        return sizes.get(0);
    }
    return

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接