web-dev-qa-db-fra.com

Comment les rappels SurfaceHolder sont-ils liés au cycle de vie des activités?

J'ai essayé d'implémenter une application qui nécessite un aperçu de la caméra sur une surface. Selon moi, les cycles de vie d'activité et de surface se composent des états suivants:

  1. Lors du premier lancement de mon activité: onResume()->onSurfaceCreated()->onSurfaceChanged()
  2. Lorsque je quitte mon activité: onPause()->onSurfaceDestroyed()

Dans ce schéma, je peux faire des appels correspondants comme ouvrir/libérer la caméra et démarrer/arrêter l'aperçu dans onPause/onResume Et onSurfaceCreated()/onSurfaceDestroyed().

Cela fonctionne bien, sauf si je verrouille l'écran. Lorsque je lance l'application, puis verrouille l'écran et le déverrouille plus tard, je vois:

onPause() - et rien d'autre après que l'écran est verrouillé - puis onResume() après déverrouillage - et aucun rappel de surface par la suite. En fait, onResume() est appelée après avoir appuyé sur le bouton d'alimentation et l'écran est allumé, mais l'écran de verrouillage est toujours actif, donc, c'est avant que l'activité ne devienne encore visible.

Avec ce schéma, j'obtiens un écran noir après le déverrouillage et aucun rappel de surface n'est appelé.

Voici un fragment de code qui n'implique pas de travail réel avec la caméra, mais les rappels SurfaceHolder. Le problème ci-dessus est reproduit même avec ce code sur mon téléphone (les rappels sont appelés dans un ordre normal lorsque vous appuyez sur le bouton "Retour", mais manquent lorsque vous verrouillez l'écran):

class Preview extends SurfaceView implements SurfaceHolder.Callback {

    private static final String tag= "Preview";

    public Preview(Context context) {
        super(context);
        Log.d(tag, "Preview()");
        SurfaceHolder holder = getHolder();
        holder.addCallback(this);
        holder.setType(SurfaceHolder.SURFACE_TYPE_Push_BUFFERS);
    }

    public void surfaceCreated(SurfaceHolder holder) {
        Log.d(tag, "surfaceCreated");
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.d(tag, "surfaceDestroyed");
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        Log.d(tag, "surfaceChanged");
    }
}

Avez-vous des idées sur la raison pour laquelle la surface n'est pas détruite après la pause de l'activité? De plus, comment gérez-vous le cycle de vie de la caméra dans de tels cas?

67
krugloid

Modifier: si le targetSDK est supérieur à 10, mettre l'application en veille appels onPause etonStop. Source

J'ai regardé le cycle de vie de l'activité et de la SurfaceView dans une petite application d'appareil photo sur mon téléphone Gingerbread. Vous avez tout à fait raison; la surface n'est pas détruite lorsque le bouton d'alimentation est enfoncé pour mettre le téléphone en veille. Lorsque le téléphone se met en veille, l'activité fait onPause. (Et ne fait pas onStop.) Il fait onResume lorsque le téléphone se réveille, et, comme vous le faites remarquer, il le fait pendant que l'écran de verrouillage est toujours visible et accepte l'entrée, ce qui est un peu bizarre. Lorsque je rend l'activité invisible en appuyant sur le bouton Accueil, l'activité fait à la fois onPause et onStop. Quelque chose provoque un rappel à surfaceDestroyed dans ce cas entre la fin de onPause et le début de onStop. Ce n'est pas très évident, mais cela semble très cohérent.

Lorsque le bouton d'alimentation est enfoncé pour mettre le téléphone en veille, à moins que quelque chose ne soit explicitement fait pour l'arrêter, l'appareil photo continue de fonctionner! Si je demande à la caméra d'effectuer un rappel par image pour chaque image d'aperçu, avec un Log.d () là-dedans, les instructions du journal continuent de s'afficher pendant que le téléphone fait semblant de dormir. Je pense que c'est Very Sneaky.

Autre confusion, les rappels à surfaceCreated et surfaceChanged se produisent aprèsonResume dans l'activité, si la surface est en cours de création.

En règle générale, je gère la caméra dans la classe qui implémente les rappels SurfaceHolder.

class Preview extends SurfaceView implements SurfaceHolder.Callback {
    private boolean previewIsRunning;
    private Camera camera;

    public void surfaceCreated(SurfaceHolder holder) {
        camera = Camera.open();
        // ...
        // but do not start the preview here!
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // set preview size etc here ... then
        myStartPreview();
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        myStopPreview();
        camera.release();
        camera = null;
    }

   // safe call to start the preview
   // if this is called in onResume, the surface might not have been created yet
   // so check that the camera has been set up too.
   public void myStartPreview() {
       if (!previewIsRunning && (camera != null)) {
           camera.startPreview();
           previewIsRunning = true;
       }
   }

   // same for stopping the preview
   public void myStopPreview() {
       if (previewIsRunning && (camera != null)) {
           camera.stopPreview();
           previewIsRunning = false;
       }
   }
}

puis dans l'activité:

@Override public void onResume() {
    preview.myStartPreview();  // restart preview after awake from phone sleeping
    super.onResume();
}
@Override public void onPause() {
    preview.myStopPreview();  // stop preview in case phone is going to sleep
    super.onPause();
}

et cela semble bien fonctionner pour moi. Les événements de rotation entraînent la destruction et la recréation de l'activité, ce qui entraîne également la destruction et la recréation de SurfaceView.

56
emrys57

Une autre solution simple qui fonctionne bien - pour changer la visibilité de la surface d'aperçu.

private SurfaceView preview;

l'aperçu est init dans la méthode onCreate. Dans onResume ensemble de méthodes View.VISIBLE pour la surface d'aperçu:

@Override
public void onResume() {
    preview.setVisibility(View.VISIBLE);
    super.onResume();
}

et respectivement dans onPause définir la visibilité View.GONE:

@Override
public void onPause() {
    super.onPause();
    preview.setVisibility(View.GONE);
    stopPreviewAndFreeCamera(); //stop and release camera
}
20
validcat

Grâce aux deux réponses précédentes, j'ai réussi à faire fonctionner l'aperçu de mon appareil photo tout en revenant de l'arrière-plan ou de l'écran de verrouillage.

Comme @ e7fendy l'a mentionné, le rappel de SurfaceView ne sera pas appelé pendant le verrouillage de l'écran car la vue de surface est toujours visible pour le système.

Par conséquent, comme @validcat l'a conseillé, appeler preview.setVisibility(View.VISIBLE); et preview.setVisibility(View.GONE); dans respectivement onPause () et onResume () forcera la vue de surface à se relayer et l'appellera des rappels.

D'ici là, la solution de @ emrys57 plus ces deux appels de méthode de visibilité ci-dessus feront que l'aperçu de votre caméra fonctionne clairement :)

Je ne peux donc donner que +1 à chacun de vous comme vous le méritiez tous;)

2
ptitvinou

SurfaceHolder.Callback est lié à sa surface.

L'activité est-elle à l'écran? Si c'est le cas, il n'y aura pas SurfaceHolder.Callback, car la Surface est toujours à l'écran.

Pour contrôler une SurfaceView, vous pouvez la gérer uniquement dans onPause/onResume. Pour SurfaceHolder.Callback, vous pouvez l'utiliser si la Surface est modifiée (créée, dimensionnée et détruite), comme initialiser openGL lorsque surfaceCreated, et détruire openGL lorsque surfaceDestroyed, etc.

1
e7fendy