144 votes

Aperçu de la caméra Android étiré

J'ai travaillé sur la création de mon activité de caméra personnalisée sur Android, mais lorsque je tourne la caméra, le rapport hauteur/largeur de la vue surface est perturbé.

Dans mon onCreate pour l'activité, j'ai défini le framelayout qui contient la vue surface qui affiche les paramètres de la caméra.

//FrameLayout qui contiendra l'aperçu de la caméra
        FrameLayout previewHolder = (FrameLayout) findViewById(R.id.camerapreview);

        //Définition de la taille de l'aperçu de la caméra pour la meilleure taille d'aperçu
        Size optimalSize = null;
        camera = getCameraInstance();
        double aspectRatio = 0;
        if(camera != null){
            //Définition du rapport hauteur/largeur de la caméra
            Camera.Parameters parameters = camera.getParameters();
            List sizes = parameters.getSupportedPreviewSizes();
            optimalSize = CameraPreview.getOptimalPreviewSize(sizes, getResources().getDisplayMetrics().widthPixels, getResources().getDisplayMetrics().heightPixels);
            aspectRatio = (float)optimalSize.width/optimalSize.height;
        }

        if(optimalSize!= null){
            RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, (int)(getResources().getDisplayMetrics().widthPixels*aspectRatio));
            previewHolder.setLayoutParams(params);
            LayoutParams surfaceParams = new LayoutParams(LayoutParams.MATCH_PARENT, (int)(getResources().getDisplayMetrics().widthPixels*aspectRatio));
            cameraPreview.setLayoutParams(surfaceParams);

        }

        cameraPreview.setCamera(camera);

        //Ajout de l'aperçu au conteneur
        previewHolder.addView(cameraPreview);

Ensuite, dans la vue de surface, je définis les paramètres de la caméra à afficher

public void setCamera(Camera camera) {
        if (mCamera == camera) { return; }

        mCamera = camera;

        if (mCamera != null) {
            requestLayout();

            try {
                mCamera.setPreviewDisplay(mHolder);
            } catch (IOException e) {
                e.printStackTrace();
            }

            if(mCamera != null){
                //Définition du rapport hauteur/largeur de la caméra
                Camera.Parameters parameters = mCamera.getParameters();
                List sizes = parameters.getSupportedPreviewSizes();
                Size optimalSize = getOptimalPreviewSize(sizes, getResources().getDisplayMetrics().widthPixels, getResources().getDisplayMetrics().heightPixels);

                parameters.setPreviewSize(optimalSize.width, optimalSize.height);
                mCamera.setParameters(parameters);
            }

            /*
              Important : Appelez startPreview() pour commencer à mettre à jour la surface de prévisualisation. La prévisualisation doit 
              être démarrée avant de pouvoir prendre une photo.
              */
            mCamera.startPreview();
        }

    }

entrez la description de l'image ici

Vous pouvez voir que l'homme LEGO devient plus grand et plus mince lorsque le téléphone est tourné :

Comment puis-je m'assurer que le rapport hauteur/largeur de ma vue de caméra est correct ?

166voto

F1sher Points 827

Je suis en train d'utiliser cette méthode -> basée sur les API Demos pour obtenir ma taille de prévisualisation :

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

        if (sizes == null) return null;

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

        int targetHeight = h;

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

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

Comme vous pouvez le voir, vous devez entrer la largeur et la hauteur de votre écran. Cette méthode calculera le ratio de l'écran en fonction de ces valeurs, puis choisira le meilleur rapport qualité-prix de la liste des tailles de prévisualisation prises en charge pour vous parmi celles disponibles. Obtenez votre liste de tailles de prévisualisation prises en charge à l'endroit où l'objet Camera n'est pas nul en utilisant

mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();

Et puis dans onMeasure, vous pouvez obtenir votre taille de prévisualisation optimale de la manière suivante :

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
        final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
        setMeasuredDimension(width, height);

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

Et ensuite (dans mon code dans la méthode surfaceChanged, comme je l'ai dit, j'utilise la structure du code de CameraActivity des API Demos, vous pouvez la générer dans Eclipse) :

Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
mCamera.setParameters(parameters);
mCamera.startPreview();

Et un conseil pour vous, car j'ai presque fait la même application que vous. La bonne pratique pour une activité caméra est de masquer la barre d'état. Des applications comme Instagram le font. Cela réduit la valeur de la hauteur de votre écran et change la valeur de votre ratio. Il est possible d'obtenir des tailles de prévisualisation étranges sur certains appareils (votre SurfaceView sera légèrement coupée)


Et pour répondre à votre question, comment vérifier si votre ratio de prévisualisation est correct ? Ensuite, obtenez la hauteur et la largeur des paramètres que vous avez définis dans :

mCamera.setParameters(parameters);

le ratio que vous avez défini est égal à la hauteur/largeur. Si vous voulez que la caméra ait l'air bien sur votre écran, alors le ratio hauteur/largeur des paramètres que vous définissez pour la caméra doit être le même que le ratio hauteur (moins la barre d'état)/largeur de votre écran.

156voto

Hesam Points 6415

La solution de F1Sher est bonne mais parfois ne fonctionne pas. Notamment, lorsque votre SurfaceView ne couvre pas tout l'écran. Dans ce cas, vous devez remplacer la méthode onMeasure(). J'ai copié mon code ici pour votre référence.

Comme j'ai mesuré la SurfaceView en fonction de la largeur, j'ai un petit espace blanc à la fin de mon écran que j'ai rempli par design. Vous pouvez corriger ce problème en conservant la hauteur et en augmentant la largeur en la multipliant par un ratio. Cependant, cela va légèrement écraser la SurfaceView.

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {

    private static final String TAG = "CameraPreview";

    private Context mContext;
    private SurfaceHolder mHolder;
    private Camera mCamera;
    private List mSupportedPreviewSizes;
    private Camera.Size mPreviewSize;

    public CameraPreview(Context context, Camera camera) {
        super(context);
        mContext = context;
        mCamera = camera;

        // tailles de prévisualisation prises en charge
        mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();
        for(Camera.Size str: mSupportedPreviewSizes)
                Log.e(TAG, str.width + "/" + str.height);

        // Installe un SurfaceHolder.Callback pour être notifié lorsque la surface sous-jacente est créée et détruite.
        mHolder = getHolder();
        mHolder.addCallback(this);
        // paramètre déprécié, mais nécessaire sur les versions d'Android antérieures à la version 3.0
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    public void surfaceCreated(SurfaceHolder holder) {
        // vide. surfaceChanged s'occupera des choses
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // vide. Prenez soin de libérer l'aperçu de la caméra dans votre activité.
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        Log.e(TAG, "surfaceChanged => w=" + w + ", h=" + h);
        // Si votre aperçu peut changer ou pivoter, prenez soin de ces événements ici.
        // Assurez-vous d'arrêter l'aperçu avant de redimensionner ou de reformater.
        if (mHolder.getSurface() == null){
            // la surface de l'aperçu n'existe pas
            return;
        }

        // arrêtez l'aperçu avant d'apporter des modifications
        try {
            mCamera.stopPreview();
        } catch (Exception e){
            // ignorer : tentative d'arrêter un aperçu inexistant
        }

        // définir la taille de l'aperçu et apporter toutes les modifications de redimensionnement, rotation ou reformatage ici
        // démarrer l'aperçu avec les nouveaux paramètres
        try {
            Camera.Parameters parameters = mCamera.getParameters();
            parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
            mCamera.setParameters(parameters);
            mCamera.setDisplayOrientation(90);
            mCamera.setPreviewDisplay(mHolder);
            mCamera.startPreview();

        } catch (Exception e){
            Log.d(TAG, "Erreur lors du démarrage de l'aperçu de la caméra : " + e.getMessage());
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
        final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);

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

        if (mPreviewSize!=null) {
            float ratio;
            if(mPreviewSize.height >= mPreviewSize.width)
                ratio = (float) mPreviewSize.height / (float) mPreviewSize.width;
            else
                ratio = (float) mPreviewSize.width / (float) mPreviewSize.height;

            // Une de ces méthodes doit être utilisée, la deuxième méthode écrase légèrement l'aperçu
            setMeasuredDimension(width, (int) (width * ratio));
  //        setMeasuredDimension((int) (width * ratio), height);
        }
    }

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

        if (sizes == null)
            return null;

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

        int targetHeight = h;

        for (Camera.Size size : sizes) {
            double ratio = (double) size.height / size.width;
            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
                continue;

            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }

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

24voto

Yoosuf Points 463

REMARQUE : MA SOLUTION EST UNE CONTINUITÉ DE LA SOLUTION DE HESAM : https://stackoverflow.com/a/22758359/1718734

Ce que j'adresse : Hesam a dit qu'il y a un petit espace blanc qui peut apparaître sur certains téléphones, comme ceci :

REMARQUE : Bien que le ratio d'aspect soit correct, la caméra ne remplit pas tout l'écran.

Hesam a suggéré une deuxième solution, mais ça écrase l'aperçu. Et sur certains appareils, cela déforme fortement.

Alors comment résolvons-nous ce problème. C'est simple...en multipliant les ratios d'aspect jusqu'à remplir l'écran. J'ai remarqué que plusieurs applications populaires telles que Snapchat, WhatsApp, etc fonctionnent de la même manière.

Tout ce que vous avez à faire est d'ajouter ceci à la méthode onMeasure :

float camHeight = (int) (width * ratio);
    float newCamHeight;
    float newHeightRatio;

    if (camHeight < height) {
        newHeightRatio = (float) height / (float) mPreviewSize.height;
        newCamHeight = (newHeightRatio * camHeight);
        Log.e(TAG, camHeight + " " + height + " " + mPreviewSize.height + " " + newHeightRatio + " " + newCamHeight);
        setMeasuredDimension((int) (width * newHeightRatio), (int) newCamHeight);
        Log.e(TAG, mPreviewSize.width + " | " + mPreviewSize.height + " | ratio - " + ratio + " | H_ratio - " + newHeightRatio + " | A_width - " + (width * newHeightRatio) + " | A_height - " + newCamHeight);
    } else {
        newCamHeight = camHeight;
        setMeasuredDimension(width, (int) newCamHeight);
        Log.e(TAG, mPreviewSize.width + " | " + mPreviewSize.height + " | ratio - " + ratio + " | A_width - " + (width) + " | A_height - " + newCamHeight);
    }

Cela calculera la hauteur de l'écran et obtiendra le ratio de la hauteur de l'écran et la hauteur de mPreviewSize. Ensuite, il multipliera la largeur et la hauteur de la caméra par le nouveau ratio de hauteur et définira la dimension mesurée en conséquence.

entrer la description de l'image ici

Et la prochaine chose que vous savez, vous obtenez ceci :D

entrer la description de l'image ici

Cela fonctionne également bien avec la caméra frontale. Je crois que c'est la meilleure façon de procéder. Maintenant, la seule chose restante pour mon application est de sauvegarder l'aperçu lui-même en cliquant sur "Capture". Mais oui, c'est ça.

6voto

EstebanA Points 121

Bonjour, la méthode getOptimalPreview() qui se trouve ici n'a pas fonctionné pour moi, alors je veux partager ma version:

private Camera.Size getOptimalPreviewSize(List sizes, int w, int h) {

    if (sizes == null) return null;

    Camera.Size optimalSize = null;
    double ratio = (double)h / w;
    double minDiff = Double.MAX_VALUE;
    double newDiff;
    for (Camera.Size size : sizes) {
        newDiff = Math.abs((double)size.width / size.height - ratio);
        if (newDiff < minDiff) {
            optimalSize = size;
            minDiff = newDiff;
        }
    }
    return optimalSize;
}

2voto

Sarthak Mittal Points 3581

Juste pour rendre ce fil de discussion plus complet, j'ajoute ma version de réponse :

Ce que je voulais réaliser : La vue de surface ne doit pas être étirée et doit couvrir tout l'écran. De plus, il n'y avait qu'un mode paysage dans mon application.

Solution :

La solution est une extension extrêmement petite de la solution de F1sher :

\=> La première étape consiste à intégrer la solution de F1sher.

\=> Maintenant, il pourrait se produire un scénario dans la solution de F1sher où la vue de surface ne couvre pas tout l'écran. La solution consiste à rendre la vue de surface plus grande que les dimensions de l'écran pour qu'elle couvre tout l'écran, pour cela :

   size = getOptimalPreviewSize(mCamera.getParameters().getSupportedPreviewSizes(), screenWidth, screenHeight);

    Camera.Parameters parameters = mCamera.getParameters();
    parameters.setPreviewSize(size.width, size.height);

    mCamera.setParameters(parameters);      

    double screenRatio = (double) screenHeight / screenWidth;
    double previewRatio = (double) size.height / size.width;

    if (previewRatio > screenRatio)     /*si le ratio de l'aperçu est supérieur au ratio de l'écran, alors nous devrons recalculer la hauteur de la surface tout en maintenant la largeur de la surface égale à la largeur de l'écran*/
    {
        RelativeLayout.LayoutParams params1 = new RelativeLayout.LayoutParams(screenWidth, (int) (screenWidth * previewRatio));
        params1.addRule(RelativeLayout.CENTER_IN_PARENT);
        flPreview.setLayoutParams(params1);

        flPreview.setClipChildren(false);

        LayoutParams surfaceParams = new LayoutParams(screenWidth, (int) (screenWidth * previewRatio));
        surfaceParams.gravity = Gravity.CENTER;
        mPreview.setLayoutParams(surfaceParams);        
    }
    else     /*si le ratio de l'aperçu est inférieur au ratio de l'écran, alors nous devrons recalculer la largeur de la surface tout en maintenant la hauteur de la surface égale à la hauteur de l'écran*/
    {
        RelativeLayout.LayoutParams params1 = new RelativeLayout.LayoutParams((int) ((double) screenHeight / previewRatio), screenHeight);
        params1.addRule(RelativeLayout.CENTER_IN_PARENT);
        flPreview.setLayoutParams(params1);
        flPreview.setClipChildren(false);

        LayoutParams surfaceParams = new LayoutParams((int) ((double) screenHeight / previewRatio), screenHeight);
        surfaceParams.gravity = Gravity.CENTER;
        mPreview.setLayoutParams(surfaceParams);

    }       

    flPreview.addView(mPreview); 

  /*  Le layout le plus haut utilisé est le RelativeLayout, flPreview est le FrameLayout dans lequel la vue de surface est ajoutée, mPreview est une instance d'une classe qui étend SurfaceView  */

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X