アクティビティにカメラプレビューを埋め込もうとしています。そして、それは縦向きだけです。問題は、プレビューが引き伸ばされることです。
最適なサイズを選んでみました。ただし、問題は、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
は、縦向きではなく横向き用です。
結果は次のとおりです。
私は1年前と同じ問題を抱えていました。さらに、前面カメラと背面カメラを扱う必要がありました。コードについてはあまり覚えていませんが、この回答を投稿する前に試してみましたが、それでも魅力のように機能しています。
コードを掘り下げて比較できることを願っています。何かが機能していれば、もっとコードを共有できます;)
/**
* 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>
FragmentActivityの下で使用されます(必要に応じて共有できます)
tl; drgetSupportedPreviewSizes()
とsetPreviewSize(int width, int height)
の両方で使用されるサイズは、元のカメラの向きです。そして通常は)自然な電話の向きや現在のディスプレイの向きとは異なります。
このため、getOptimalSize(int, int)
メソッドは、サイズが横になっているときにサイズをループし(そのため、サイズの1/ratio
を使用します)、サイズを選択せず、最後に間違った比率を選択します。メソッドの2番目のループに従った高さに基づいて、押しつぶされた画像になります。
どうやら、サポートされているサイズは常に自然な角度でカメラを参照しています(ドキュメントにはそれが記載されていませんが)。カメラの自然な角度は通常、電話の自然な角度と同じではありません。 CameraInfo.orientation
フィールドを使用してそれらの違いを確認できます。
これが真実であることを示唆するドキュメント(試してみる以外)は、謎を解くのと同じドキュメントです:Camera.Parameters.setPreviewSize(int width, int height)
:
幅と高さの側面は、カメラの向きに基づいています。つまり、プレビューサイズは、表示方向によって回転される前のサイズです。そのため、アプリケーションはプレビューサイズを設定する際に表示方向を考慮する必要があります。たとえば、カメラが480x320と320x480の両方のプレビューサイズをサポートしているとします。アプリケーションは3:2のプレビュー比を必要としています。表示方向が0または180に設定されている場合、プレビューサイズは480x320に設定する必要があります。表示方向が90または270に設定されている場合、プレビューサイズは320x480に設定する必要があります。画像サイズとサムネイルサイズを設定する際には、表示方向も考慮する必要があります。
( ドキュメントはこちら )
それからいくつかのことを学ぶことができます:
表示されるサイズは、ディスプレイや電話の向きに関係なく同じであるはずなので、そこに表示される値に問題はありません。 onMeasure()メソッドに最適なものを選択して、ビューを縦向きで測定するために、それらを横向きにする必要があります(プレビューで使用する画面とスペースに基づいて)。
理想的には、カメラの取り付け角度と現在の電話の角度に互換性がないことを確認した後で、それらを回します(1つの横向きと1つの縦向き)。
//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;
}
}
}
あなたと私の場合、isCameraOnSide()
はtrue
を返します-setPreviewOrientation(90)
の行からわかるように。より一般的な実装については、次の1つのベースのGoogleのCamera2Basicサンプルがあります。
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;
}
さらに重要なのは、Camera.Parameters.getPreviewSize()
メソッドを時計として、またはログで使用する場合、サイズの比率とは異なる比率に設定されていることがわかると思います。 setMearuseDimension(int, int)
メソッドによって選択されます。この比率の違いは、ストレッチ/スカッシュの原点です(写真では垂直に押しつぶされているように見えます。Thatポートレートビューのランドスケープ画像は押しつぶされるのではなく垂直に引き伸ばされるため、歪みがランドスケープ/ポートレートの混乱によるものではないことを示唆する場合もあります)。ビュー(この場合はSurfaceView)に適切なサイズを選択した後、ビューに使用したサイズと同じ比率(カメラに応じた幅)のサポートされているプレビューサイズでCamera.Parameters.setPreviewSize(int width, int height)
を呼び出す必要があります。 、現在の電話/ディスプレイの向きではありません。つまり、height
パラメータに入る可能性があります)。
たとえば、surfaceCreated
メソッドとsurfaceChanged
メソッドでこれを行うことができます(私のために働いた)。カメラのプレビューサイズを設定するときにプレビューがオンになっていないことを確認し、次の操作を行った後にプレビューを開始(または再起動)します。
//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();
私はしばらくの間カメラアプリケーションの開発に取り組んできましたが、考慮すべきことがたくさんありますが、シンプルにしましょう
CameraPreview
でonLayout()
をオーバーライドすることにより、アクティビティの中央に配置できます。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