Prendre en charge les surfaces redimensionnables dans l'appli Appareil photo

1. Présentation

Dernière mise à jour:25 avril 2022

Pourquoi une surface redimensionnable ?

Auparavant, votre application pouvait avoir été utilisée dans la même fenêtre pendant tout le cycle de vie.

Mais avec la disponibilité de nouveaux facteurs de forme, tels que les appareils pliables, et les nouveaux modes d'affichage tels que le mode multifenêtre et le mode multi-écran, vous ne pouvez plus supposer que c'est le cas.

Découvrons en particulier quelques-unes des considérations les plus importantes à prendre en compte lors du développement d'applications ciblant les grands écrans et les appareils pliables:

  • Ne pas supposer que votre application sera disponible dans une fenêtre en mode Portrait : la demande d'orientation fixe est toujours acceptée en 12L, mais nous offrons maintenant la possibilité aux fabricants d'appareils d'ignorer la demande pour une orientation préférée.
  • Ne supposez pas de format ou de format fixe pour votre application. Même si vous définissez resizableActivity = "false" sur les grands écrans (>=600dp), votre application peut être utilisée en mode multifenêtre.
  • Ne supposez pas de relation fixe entre l'orientation de l'écran et l'appareil photo. Le document de définition de compatibilité Android spécifie qu'un capteur d'image d'appareil photo doit être orienté de sorte que la dimension longue de l'appareil photo soit alignée avec la dimension longue. À partir du niveau d'API 32, les clients de l'appareil photo qui interrogent les appareils pliables peuvent recevoir une valeur qui peut varier de manière dynamique en fonction de l'état de l'appareil ou de la ligne de flottaison.
  • Ne pas prendre en compte la taille de l'encart
  • Ne pas supposer que votre application dispose d'un accès exclusif à l'appareil photo Tant que l'application est en mode multifenêtre, d'autres applications peuvent avoir accès aux ressources partagées, comme l'appareil photo et le micro.

Il est temps de s'assurer que votre application photo fonctionne correctement dans tous les scénarios en apprenant à transformer la sortie de la caméra pour l'adapter aux surfaces redimensionnables et à utiliser les API proposées par Android pour gérer différents cas d'utilisation.

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez créer une application simple qui affiche l'aperçu de l'appareil photo. Dans un premier temps, vous allez créer une application qui permet de verrouiller l'orientation de l'appareil et déclare qu'elle n'est pas redimensionnable. Vous verrez qu'elle se comporte sur Android 12L.

Ensuite, mettez à jour le code source afin de vous assurer que l'aperçu s'affiche systématiquement dans tous les scénarios. Le résultat est une application d'appareil photo qui gère correctement les modifications de configuration et transforme automatiquement la surface pour qu'elle corresponde à l'aperçu.

1df0acf495b0a05a.png

Points abordés

  • Affichage des aperçus dans Camera2 sur les surfaces Android
  • Relation entre l'orientation, la rotation de l'écran et les proportions du capteur
  • Convertir une surface en fonction des proportions de l'aperçu de la caméra et de la rotation de l'écran

Prérequis

  • Une version récente d'Android Studio
  • Connaissances de base du développement d'applications Android
  • Connaissances de base des API Camera2
  • Un appareil ou un émulateur exécutant Android 12L

2. Configuration

Obtenir le code de départ

Pour comprendre le comportement sur Android 12L, vous commencerez par une application d'appareil photo qui verrouille l'orientation et se déclare comme non redimensionnable.

Si git est installé, vous pouvez simplement exécuter la commande ci-dessous. Pour vérifier si git est installé, saisissez git --version dans le terminal ou la ligne de commande, et vérifiez qu'il fonctionne correctement.

git clone https://github.com/googlecodelabs/android-camera2-preview.git

Si vous n'avez pas accès à git, cliquez sur le bouton ci-dessous pour télécharger l'ensemble du code de cet atelier de programmation : .

Ouvrir le premier module

Dans Android Studio, ouvrez le premier module situé sous /step1.

Android Studio vous invite à spécifier le chemin d'accès au SDK. Si vous rencontrez des problèmes, vous pouvez suivre les recommandations concernant la mise à jour de l'IDE et des SDK Tools.

302f1fb5070206c7.png

Si vous êtes invité à utiliser la dernière version de Gradle, continuez et mettez-la à jour.

Préparer l'appareil

À la date de publication de cet atelier de programmation, il y a un nombre limité d'appareils physiques pouvant exécuter Android 12L.

Vous trouverez la liste des appareils et les instructions pour installer la version 12L ici : https://developer.android.com/about/versions/12/12L/get.

Nous vous recommandons vivement d'utiliser un appareil physique pour tester les applications de l'appareil photo. Toutefois, si vous souhaitez utiliser un émulateur, assurez-vous d'en créer un avec un grand écran (par exemple, Pixel C) et avec l'API 32,

Préparer un sujet

Lorsque je travaille avec les caméras, j'aimerais avoir un sujet standard avec lequel je pourrais apprécier les différences de paramètres, d'orientation et de mise à l'échelle.

Pour cet atelier de programmation, je vais utiliser une version papier de cette image carrée.66e5d83317364e67.png

Dans tous les cas, la flèche ne pointe pas vers le haut ou le carré devient une autre figure géométrique . . . Quelque chose doit être corrigé !

3. Courir et observer

Placez l'appareil en mode Portrait et exécutez le code dans le module 1. Veillez à autoriser l'appli Camera2 Codelab à prendre des photos et à enregistrer des vidéos lorsque vous utilisez l'application. Comme vous pouvez le constater, l'aperçu s'affiche correctement et utilise l'espace de l'écran de manière efficace.

Faites maintenant pivoter l'appareil en mode paysage:

46f2d86b060dc3.a

C'est vraiment faux. Cliquez sur le bouton "Refresh" (Actualiser) en bas à droite.

B8fbd7a393cb6259.png

C'est un peu mieux, mais pas encore optimal.

Ce comportement correspond au mode de compatibilité d'Android 12L. Les applications qui permettent de verrouiller leur orientation en mode Portrait peuvent apparaître en format paysage lorsque l'appareil passe en mode Paysage et que la densité d'écran est supérieure à 600 dp.

Bien que ce mode préserve le format d'origine, il nuit également à l'expérience utilisateur, car la plupart de l'espace d'affichage n'est pas utilisé.

En outre, dans ce cas, l'aperçu pivote de manière incorrecte de 90 degrés.

Placez à nouveau l'appareil en mode Portrait et activez le mode Écran partagé.

Vous pouvez redimensionner la fenêtre en faisant glisser la séparation centrale.

Découvrez l'impact du redimensionnement sur l'aperçu de l'appareil photo. Elle est-elle déformée ? Les proportions restent-elles identiques ?

4. Solution rapide

Comme le mode de compatibilité n'est déclenché que pour les applications qui verrouillent l'orientation et qui ne peuvent pas être redimensionnés, vous pouvez être tenté de mettre à jour les indicateurs dans le fichier manifeste afin d'éviter qu'ils ne s'affichent.

Essayez dès maintenant:

step1/AndroidManifest.xml

<activity
    android:name=".CameraActivity"
    android:exported="true"
    android:resizeableActivity="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Maintenant, créez l'application et exécutez-la à nouveau en mode paysage. Ce type de message devrait s'afficher :

F5753af5a9e44d2f.png

La flèche ne pointe pas vers le haut, et cela n'est pas carré.

L'application n'a pas été conçue pour fonctionner en mode multifenêtre ou dans des orientations différentes. Par conséquent, le système ne devrait pas modifier la taille de la fenêtre, ce qui entraîne des problèmes.

5. Gérer les modifications de configuration

Commençons par indiquer au système que nous souhaitons gérer les modifications de configuration nous-mêmes. Ouvrez step1/AndroidManifest.xml et ajoutez les lignes suivantes:

step1/AndroidManifest.xml

<activity
    android:name=".CameraActivity"
    android:exported="true"
    android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
    android:resizeableActivity="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

À présent, vous devez également mettre à jour le champ step1/CameraActivity.kt pour recréer CameraCaptureSession chaque fois que la taille de la surface change.

Accédez à la ligne 232 et appelez la fonction createCaptureSession():

step1/CameraActivity.kt

override fun onSurfaceTextureSizeChanged(
    surface: SurfaceTexture,
    width: Int,
    height: Int
) {
    createCaptureSession()
}

Voici une mise en garde: la fonction onSurfaceTextureSizeChanged n'est pas appelée après une rotation de 180 degrés (la taille ne change pas). Elle ne déclenche pas non plus la commande onConfigurationChanged. Par conséquent, la seule option dont nous disposons consiste à instancier DisplayListener et à rechercher des rotations de 180 degrés. Comme l'appareil a quatre orientations (portrait, paysage, portrait inversé et paysage inversé) définies par les nombres entiers 0, 1, 2 et 3, nous devons vérifier la différence de rotation de 2.

Ajoutez le code suivant:

step1/CameraActivity.kt

/** [DisplayManager] to listen to display changes */
private val displayManager: DisplayManager by lazy {
    applicationContext.getSystemService(DISPLAY_SERVICE) as DisplayManager
}

/** Keeps track of display rotations */
private var displayRotation = 0

.
.
.

override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    displayManager.registerDisplayListener(displayListener, mainLooperHandler)
}

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    displayManager.unregisterDisplayListener(displayListener)
}

private val displayListener = object : DisplayManager.DisplayListener {
    override fun onDisplayAdded(displayId: Int) {}
    override fun onDisplayRemoved(displayId: Int) {}
    override fun onDisplayChanged(displayId: Int) {
        val difference = displayManager.getDisplay(displayId).rotation - displayRotation
        displayRotation = displayManager.getDisplay(displayId).rotation

        if (difference == 2 || difference == -2) {
            createCaptureSession()
        }
    }
}

Nous sommes sûrs que la session de capture est recréée dans tous les cas. Il est temps d'étudier la relation cachée entre les orientations de la caméra et les rotations d'écran.

6. Orientation du capteur et rotations de l'écran

L'orientation naturelle correspond à l'orientation dans laquelle les utilisateurs ont tendance à utiliser naturellement un appareil. Par exemple, l'orientation naturelle est probablement celle d'un ordinateur portable et d'un portrait pour un téléphone. Pour une tablette, il peut s'agir de l'un des deux.

Nous pouvons définir deux autres concepts à partir de cette définition.

1f9cf3248b95e533.png

Nous appelons l'orientation de la caméra : l'angle entre le capteur de la caméra et l'orientation naturelle de l'appareil. Cela dépend de la façon dont la caméra est montée sur l'appareil et du capteur devant être toujours aligné avec le long côté de l'écran ( CDD).

Étant donné qu'il peut être difficile de définir le côté long d'un appareil pliable, car il peut physiquement transformer sa géométrie, à partir de l'API 32, ce champ n'est plus statique, mais il peut être récupéré de façon dynamique à partir de l'objet CameraCharacteristics.

La rotation des appareils est un autre concept. Permet de mesurer la rotation physique de l'appareil par rapport à son orientation naturelle.

Comme nous ne souhaitons généralement gérer que quatre orientations différentes, nous ne pouvons considérer que les angles comme des multiples de 90 et obtenir ces informations en multipliant la valeur renvoyée par Display.getRotation() par 90.

Par défaut, l'élément TextureView est déjà compensé par l'orientation de l'appareil photo. Toutefois, il ne gère pas la rotation de l'écran, ce qui entraîne des rotations incorrectes.

Pour résoudre ce problème, il vous suffit de faire pivoter la surface cible. Mettez à jour la fonction CameraUtils.buildTargetTexture pour accepter le paramètre surfaceRotation: Int et appliquer la transformation à la surface:

step1/CameraUtils.kt

fun buildTargetTexture(
    containerView: TextureView,
    characteristics: CameraCharacteristics,
    surfaceRotation: Int
): SurfaceTexture? {

    val previewSize = findBestPreviewSize(Size(containerView.width, containerView.height), characteristics)

    val surfaceRotationDegrees = surfaceRotation * 90

    val halfWidth = containerView.width / 2f
    val halfHeight = containerView.height / 2f

    val matrix = Matrix()

    // Rotate to compensate display rotation
    matrix.postRotate(
        -surfaceRotationDegrees.toFloat(),
        halfWidth,
        halfHeight
    )

    containerView.setTransform(matrix)

    return containerView.surfaceTexture?.apply {
        setDefaultBufferSize(previewSize.width, previewSize.height)
    }
}

Vous pouvez ensuite l'appeler en modifiant la ligne 142 de CameraActivity comme suit:

step1/CameraActivity.kt

val transformedTexture = CameraUtils.buildTargetTexture(
textureView, characteristics,
displayManager.getDisplay(Display.DEFAULT_DISPLAY).rotation
)

Si vous exécutez l'application maintenant, un aperçu semblable à celui-ci s'affichera:

1566c3f9e5089a35.png

La flèche pointe désormais vers le haut, mais le conteneur ne correspond pas à un carré. Voyons comment résoudre ce problème à la dernière étape.

Mettre à l'échelle du viseur

La dernière étape consiste à ajuster la surface afin qu'elle corresponde aux proportions de la sortie de la caméra.

Le problème de l'étape précédente est dû au fait que, par défaut, la texture de TextureView adapte son contenu à la fenêtre entière. Cette fenêtre peut avoir un format différent de celui de l'aperçu de la caméra. Elle peut donc être étirée ou déformée.

Pour cela, nous pouvons procéder en deux étapes:

  • Calculer les facteurs de scaling que TextureView s'applique par défaut et inverser cette transformation
  • Calculez et appliquez le facteur de mise à l'échelle approprié (qui doit être identique pour les axes X et Y)

Pour calculer le facteur de mise à l'échelle approprié, nous devons prendre en compte la différence entre l'orientation de la caméra et la rotation de l'écran. Ouvrez step1/CameraUtils.kt et ajoutez la fonction suivante pour calculer la rotation relative entre l'orientation du capteur et la rotation de l'affichage:

step1/CameraUtils.kt

/**
 * Computes the relative rotation between the sensor orientation and the display rotation
 */
private fun computeRelativeRotation(
    characteristics: CameraCharacteristics,
    deviceOrientationDegrees: Int
): Int {
    val sensorOrientationDegrees =
        characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0

    // Reverse device orientation for front-facing cameras
    val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) ==
        CameraCharacteristics.LENS_FACING_FRONT
    ) 1 else -1

    return (sensorOrientationDegrees - (deviceOrientationDegrees * sign) + 360) % 360
}

Il est essentiel de savoir que la valeur renvoyée par computeRelativeRotation est importante, car cela nous permet de savoir si l'aperçu d'origine a été alterné avant d'être mis à l'échelle.

Par exemple, pour un téléphone dans son orientation naturelle, le résultat de la caméra est en forme de paysage et une rotation de 90 degrés avant d'être affichée à l'écran.

En revanche, si l'appareil est dans son orientation naturelle, la sortie vidéo s'affiche directement à l'écran sans rotation supplémentaire.

Consultez à nouveau les cas suivants:

4e3a61ea9796a916.png Dans le deuxième cas (milieu), l'axe des abscisses de la sortie de la caméra est affiché sur l'axe y de l'écran, et inversement, ce qui signifie que la largeur et la hauteur de la sortie de la caméra sont inversées lors de la transformation. Dans les autres cas, elles restent identiques, bien qu'une rotation soit encore obligatoire dans le troisième scénario.

Nous pouvons généraliser ces cas à l'aide de la formule:

val isRotationRequired =
        computeRelativeRotation(characteristics, surfaceRotationDegrees) % 180 != 0

À présent, nous pouvons mettre à jour la fonction pour mettre à l'échelle la surface:

step1/CameraUtils.kt

fun buildTargetTexture(
    containerView: TextureView,
    characteristics: CameraCharacteristics,
    surfaceRotation: Int
): SurfaceTexture? {

    val surfaceRotationDegrees = surfaceRotation * 90

    val previewSize = findBestPreviewSize(Size(containerView.width, containerView.height), characteristics)

    val isRotationRequired =
        computeRelativeRotation(characteristics, surfaceRotationDegrees) % 180 != 0

    /* Scale factor required to scale the preview to its original size on the x-axis */
    val scaleX =
        if (isRotationRequired) {
            containerView.width.toFloat() / previewSize.height
        } else {
            containerView.width.toFloat() / previewSize.width
        }
    /* Scale factor required to scale the preview to its original size on the y-axis */
    val scaleY =
        if (isRotationRequired) {
            containerView.height.toFloat() / previewSize.width
        } else {
            containerView.height.toFloat() / previewSize.height
        }

    /* Scale factor required to fit the preview to the TextureView size */
    val finalScale = max(scaleX, scaleY)
    val halfWidth = containerView.width / 2f
    val halfHeight = containerView.height / 2f

    val matrix = Matrix()

    if (isRotationRequired) {
        matrix.setScale(
            1 / scaleX * finalScale,
            1 / scaleY * finalScale,
            halfWidth,
            halfHeight
        )
    } else {
        matrix.setScale(
            containerView.height / containerView.width.toFloat() / scaleY * finalScale,
            containerView.width / containerView.height.toFloat() / scaleX * finalScale,
            halfWidth,
            halfHeight
        )
    }

    // Rotate to compensate display rotation
    matrix.postRotate(
        -surfaceRotationDegrees.toFloat(),
        halfWidth,
        halfHeight
    )

    containerView.setTransform(matrix)

    return containerView.surfaceTexture?.apply {
        setDefaultBufferSize(previewSize.width, previewSize.height)
    }
}

Créez l'application, exécutez-la et profitez de l'aperçu de votre caméra.

7. Félicitations

Félicitations !

Si vous êtes arrivé(e) jusqu'ici, vous devriez avoir appris:

  • Comportement des applications non optimisées sur Android 12L en mode de compatibilité
  • Gérer les modifications de configuration
  • Différences entre les concepts tels que l'orientation de l'appareil photo, la rotation de l'écran et l'orientation naturelle de l'appareil
  • Comportement par défaut de TextureView
  • Comment faire évoluer la surface pour afficher correctement l'aperçu de la caméra dans tous les cas !

Complément d'informations

Documents de référence