studio manejo intent custom camara app java android user-interface camera landscape-portrait

java - manejo - Android: cómo utilizar la cámara getSupportedPreviewSizes() para la orientación vertical



manejo de camara android studio (4)

Estoy tratando de incrustar una vista previa de la cámara en una actividad. Y es solo en orientación vertical. El problema es que la vista previa se estira.

He intentado elegir el tamaño óptimo. Pero el problema es que todos los tamaños de vista previa admitidos de getSupportedPreviewSizes() devuelven tamaños en orientación horizontal. Así que elegir el tamaño correcto de acuerdo con mi código no funcionará, supongo.

Mi diseño 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>

Aquí está mi clase de 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) { } }

Las siguientes imágenes son el resultado de los valores:

Specified resolution from measureSpecWidth/Height = `984x1335` Returned from getOptimalSize() = `1600x1200`.

Debido a que los tamaños de vista previa supportedPreviewSizes son para paisaje, no para retrato.

Aquí está el resultado:


He trabajado en el desarrollo de aplicaciones de cámara por un tiempo y hay muchas cosas que tomar en consideración, pero seamos sencillos.

  1. Intente que el tamaño de la vista de destino tenga la misma relación de aspecto que uno de los tamaños de vista previa admitidos populares (3: 2, 16: 9, 4: 3). Si no puede intentar elegir un tamaño de vista previa que tenga la menor diferencia en la relación de aspecto
  2. Después de elegir un tamaño de vista adecuado, puede centrarlo dentro de su actividad anulando onLayout() en su CameraPreview

Tuve el mismo problema que hace 1 año. Además tuve que lidiar con la cámara frontal y la trasera. No recuerdo mucho sobre el código, pero lo intenté antes de publicar esta respuesta y todavía funciona como un encanto.
Espero que puedas cavar y comparar con tu código. Puedo compartir más código si solo algo funciona;)

/** * 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 } }



Aquí está el XML, es simple (lo verás en la captura de pantalla). Lo único importante es el FrameLayout con id: capture_evidence_camera_preview

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

Se usa bajo FragmentActivity (puedo compartirlo si lo necesitas también)


tl; dr los tamaños utilizados tanto en getSupportedPreviewSizes() como en setPreviewSize(int width, int height) están en la orientación original de la cámara, lo que podría (y generalmente es) diferente a la orientación natural del teléfono y la orientación actual de la pantalla.

Debido a esto, el getOptimalSize(int, int) recorre los tamaños cuando están de su lado (y usan 1/ratio de ellos debido a eso), no seleccionando ninguno de ellos y eligiendo una proporción incorrecta al final, basado en La altura de acuerdo con el segundo bucle en el método, lo que resulta en una imagen aplastada.

Aparentemente, los tamaños admitidos siempre se refieren a la cámara en su ángulo natural (aunque la documentación no nos dice eso). El ángulo natural de la cámara normalmente no es el mismo que el ángulo natural del teléfono. Puede verificar la diferencia entre ellos usando el campo CameraInfo.orientation .

La documentación que sugiere que esto es cierto (además de probarlo) es la misma documentación que resuelve tu misterio también: Camera.Parameters.setPreviewSize(int width, int height) :

Los lados de anchura y altura se basan en la orientación de la cámara. Es decir, el tamaño de la vista previa es el tamaño antes de girarlo por la orientación de la pantalla. Por lo tanto, las aplicaciones deben considerar la orientación de la pantalla al configurar el tamaño de la vista previa. Por ejemplo, supongamos que la cámara admite tamaños de vista previa de 480x320 y 320x480. La aplicación quiere una relación de vista previa de 3: 2. Si la orientación de la pantalla se establece en 0 o 180, el tamaño de la vista previa se debe establecer en 480x320. Si la orientación de la pantalla se establece en 90 o 270, el tamaño de la vista previa se debe establecer en 320x480. La orientación de la pantalla también debe considerarse al configurar el tamaño de la imagen y el tamaño de la miniatura.

( Documentación aquí )

Podemos aprender un par de cosas de eso:

  1. Se supone que los tamaños que obtiene son los mismos, independientemente de la orientación de la pantalla / teléfono, por lo que no hay nada de malo en los valores que ve allí. Debes ponerlos de lado para elegir el mejor para el método onMeasure () para medir la vista en una orientación vertical (según la pantalla y el espacio que quieras que ocupe la vista previa).

    Lo ideal es girarlos después de confirmar el ángulo de montaje de la cámara y el ángulo del teléfono actual no son compatibles (un paisaje y un retrato).

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

    En su caso y en el mío, isCameraOnSide() devuelve true , como podemos ver en su línea de setPreviewOrientation(90) . Para una implementación más general, aquí hay una muestra de Google Camera2Basic basada en:

    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. Y, lo que es más importante, si utiliza el método Camera.Parameters.getPreviewSize() como un reloj o en un registro, creo que verá que se configura en una proporción diferente a la del tamaño elegido por setMearuseDimension(int, int) método. Esta diferencia en las proporciones es el origen del estiramiento / aplastamiento (se ve aplastado verticalmente en su imagen. Esto también puede ser un indicio de que la distorsión no se debe a una confusión de paisaje / retrato, ya que una imagen de paisaje en vista de retrato se extendería verticalmente en lugar de aplastado). Después de elegir el tamaño correcto para la vista (en este caso, SurfaceView), debe llamar a Camera.Parameters.setPreviewSize(int width, int height) con un tamaño de vista previa compatible que tenga la misma proporción que el tamaño que utilizó para la vista (nuevamente , ancho de acuerdo con la cámara, no con la orientación actual del teléfono / pantalla. Esto significa que podría ir al parámetro de height )

    Por ejemplo, podría hacerlo en métodos de surfaceCreated y surfaceChanged (funcionó para mí). Asegúrese de que la vista previa no esté activada cuando configure el tamaño de vista previa de la cámara y que la inicie (o la reinicie) después de hacerlo:

    //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();


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