Exemple : Teapot

L'exemple Teapot se trouve dans le répertoire samples/Teapot/, sous le répertoire racine de l'installation du NDK. Cet exemple utilise la bibliothèque OpenGL pour afficher la célèbre théière de l'Utah. Il présente en particulier la classe d'assistance ndk_helper, un ensemble de fonctions d'assistance natives requises pour implémenter des jeux et des applications similaires en tant qu'applications natives. Cette classe fournit les éléments suivants :

  • Une couche d'abstraction, GLContext, qui gère certains comportements spécifiques au NDK
  • Des fonctions d'assistance utiles qui ne se trouvent pas dans le NDK, telles que la détection des gestes tactiles
  • Des wrappers pour les appels JNI pour les fonctionnalités de plate-forme telles que le chargement de texture

AndroidManifest.xml

La déclaration d'activité ici n'est pas NativeActivity, mais une sous-classe de celle-ci : TeapotNativeActivity.

<activity android:name="com.sample.teapot.TeapotNativeActivity"
        android:label="@string/app_name"
        android:configChanges="orientation|keyboardHidden">

Enfin, le nom du fichier d'objet partagé créé par le système de compilation est libTeapotNativeActivity.so. Le système de compilation ajoute le préfixe lib et l'extension .so. Ni l'un ni l'autre ne font partie de la valeur initialement attribuée au fichier android:value par le fichier manifeste.

<meta-data android:name="android.app.lib_name"
        android:value="TeapotNativeActivity" />

Application.mk

Une application qui utilise la classe de framework NativeActivity ne doit pas spécifier un niveau d'API Android inférieur à 9. Il s'agit du niveau à partir duquel cette classe a été ajoutée. Pour en savoir plus sur la classe NativeActivity, consultez la section Activités et applications natives.

APP_PLATFORM := android-9

La ligne suivante indique au système de compilation de prendre en compte toutes les architectures compatibles lors de la compilation.

APP_ABI := all

Puis, le fichier indique au système de compilation la bibliothèque sous-jacente d'exécution C++ à utiliser.

APP_STL := stlport_static

Implémentation côté Java

Le fichier TeapotNativeActivity se trouve dans teapots/classic-teapot/src/com/sample/teapot, sous le répertoire racine du dépôt NDK sur GitHub. Il gère les événements de cycle de vie des activités, crée une fenêtre pop-up pour afficher le texte à l'écran avec la fonction ShowUI() et met à jour la fréquence d'images de manière dynamique avec la fonction updateFPS(). Le code suivant peut vous intéresser, car il prépare l'activité de l'application à être en mode plein écran, immersive et sans barres de navigation système. De cette manière, l'intégralité de l'écran peut être utilisée pour afficher les images de théière affichées :

Kotlin

fun setImmersiveSticky() {
    window.decorView.systemUiVisibility = (
            View.SYSTEM_UI_FLAG_FULLSCREEN
                    or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                    or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                    or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            )
}

Java

void setImmersiveSticky() {
    View decorView = getWindow().getDecorView();
    decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN
            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}

Implémentation côté natif

Cette section explore la partie de l'application Teapot implémentée en langage C++.

TeapotRenderer.h

Ces appels de fonction effectuent le rendu réel de la théière. ndk_helper est utilisé pour le calcul de la matrice et pour repositionner l'appareil photo en fonction de l'endroit où l'utilisateur appuie.

ndk_helper::Mat4 mat_projection_;
ndk_helper::Mat4 mat_view_;
ndk_helper::Mat4 mat_model_;


ndk_helper::TapCamera* camera_;

TeapotNativeActivity.cpp

Les lignes suivantes incluent ndk_helper dans le fichier source natif et définissent le nom de la classe d'assistance.


#include "NDKHelper.h"

//-------------------------------------------------------------------------
//Preprocessor
//-------------------------------------------------------------------------
#define HELPER_CLASS_NAME "com/sample/helper/NDKHelper" //Class name of helper
function

La première utilisation de la classe ndk_helper consiste à gérer le cycle de vie lié aux EGL, en associant les états de contexte EGL (création/perte) aux événements de cycle de vie Android. La classe ndk_helper permet à l'application de conserver les informations contextuelles afin que le système puisse restaurer une activité détruite. Cette fonctionnalité est utile, par exemple, lors de la rotation de la machine cible (ce qui entraîne l'élimination d'une activité, puis sa restauration immédiate dans la nouvelle orientation) ou lorsque l'écran de verrouillage apparaît.

ndk_helper::GLContext* gl_context_; // handles EGL-related lifecycle.

Puis, ndk_helper propose des commandes tactiles.

ndk_helper::DoubletapDetector doubletap_detector_;
ndk_helper::PinchDetector pinch_detector_;
ndk_helper::DragDetector drag_detector_;
ndk_helper::PerfMonitor monitor_;

Il fournit également des commandes pour contrôler l'appareil photo (vue OpenGL).

ndk_helper::TapCamera tap_camera_;

L'application se prépare ensuite à utiliser les capteurs de l'appareil à l'aide des API natives fournies dans le NDK.

ASensorManager* sensor_manager_;
const ASensor* accelerometer_sensor_;
ASensorEventQueue* sensor_event_queue_;

L'application appelle les fonctions suivantes en réponse à divers événements de cycle de vie Android et modifications d'état du contexte EGL, à l'aide de diverses fonctionnalités fournies par ndk_helper via la classe Engine.


void LoadResources();
void UnloadResources();
void DrawFrame();
void TermDisplay();
void TrimMemory();
bool IsReady();

Ensuite, la fonction suivante rappelle le côté Java pour mettre à jour l'affichage de l'interface utilisateur.

void Engine::ShowUI()
{
    JNIEnv *jni;
    app_->activity->vm->AttachCurrentThread( &jni, NULL );


    //Default class retrieval
    jclass clazz = jni->GetObjectClass( app_->activity->clazz );
    jmethodID methodID = jni->GetMethodID( clazz, "showUI", "()V" );
    jni->CallVoidMethod( app_->activity->clazz, methodID );


    app_->activity->vm->DetachCurrentThread();
    return;
}

Elle rappelle également le côté Java pour dessiner une zone de texte superposée sur l'écran affiché du côté natif, qui indique le nombre de frames.

void Engine::UpdateFPS( float fFPS )
{
    JNIEnv *jni;
    app_->activity->vm->AttachCurrentThread( &jni, NULL );


    //Default class retrieval
    jclass clazz = jni->GetObjectClass( app_->activity->clazz );
    jmethodID methodID = jni->GetMethodID( clazz, "updateFPS", "(F)V" );
    jni->CallVoidMethod( app_->activity->clazz, methodID, fFPS );


    app_->activity->vm->DetachCurrentThread();
    return;
}

L'application récupère l'horloge système et la fournit au moteur de rendu pour une animation temporelle basée sur l'horloge en temps réel. Ces informations sont utilisées, entre autres, dans le calcul du momentum, lorsque la vitesse diminue en fonction du temps.

renderer_.Update( monitor_.GetCurrentTime() );

L'application inverse le frame généré pour le mettre en mémoire tampon avant de l'afficher via la fonction GLcontext::Swap(). Elle gère également les erreurs qui se sont produites lors de l'inversion.

if( EGL_SUCCESS != gl_context_->Swap() )  // swaps
buffer.

Le programme transmet les événements de détection de mouvement tactile au détecteur de gestes défini dans la classe ndk_helper. Le détecteur de gestes suit les gestes multipoint (pincer et faire glisser, par exemple) et envoie une notification lorsqu'il est déclenché par l'un de ces événements.

if( AInputEvent_getType( event ) == AINPUT_EVENT_TYPE_MOTION )
{
    ndk_helper::GESTURE_STATE doubleTapState =
        eng->doubletap_detector_.Detect( event );
    ndk_helper::GESTURE_STATE dragState = eng->drag_detector_.Detect( event );
    ndk_helper::GESTURE_STATE pinchState = eng->pinch_detector_.Detect( event );

    //Double tap detector has a priority over other detectors
    if( doubleTapState == ndk_helper::GESTURE_STATE_ACTION )
    {
        //Detect double tap
        eng->tap_camera_.Reset( true );
    }
    else
    {
        //Handle drag state
        if( dragState & ndk_helper::GESTURE_STATE_START )
        {
             //Otherwise, start dragging
             ndk_helper::Vec2 v;
             eng->drag_detector_.GetPointer( v );
             eng->TransformPosition( v );
             eng->tap_camera_.BeginDrag( v );
        }
        // ...else other possible drag states...

        //Handle pinch state
        if( pinchState & ndk_helper::GESTURE_STATE_START )
        {
            //Start new pinch
            ndk_helper::Vec2 v1;
            ndk_helper::Vec2 v2;
            eng->pinch_detector_.GetPointers( v1, v2 );
            eng->TransformPosition( v1 );
            eng->TransformPosition( v2 );
            eng->tap_camera_.BeginPinch( v1, v2 );
        }
        // ...else other possible pinch states...
    }
    return 1;
}

La classe ndk_helper permet également d'accéder à une bibliothèque de mathématiques vectorielles (vecmath.h) utilisée ici pour transformer des coordonnées tactiles.

void Engine::TransformPosition( ndk_helper::Vec2& vec )
{
    vec = ndk_helper::Vec2( 2.0f, 2.0f ) * vec
            / ndk_helper::Vec2( gl_context_->GetScreenWidth(),
            gl_context_->GetScreenHeight() ) - ndk_helper::Vec2( 1.f, 1.f );
}

La méthode HandleCmd() gère les commandes publiées à partir de la bibliothèque android_native_app_glue. Pour en savoir plus sur la signification des messages, consultez les commentaires dans les fichiers sources android_native_app_glue.h et .c.

void Engine::HandleCmd( struct android_app* app,
        int32_t cmd )
{
    Engine* eng = (Engine*) app->userData;
    switch( cmd )
    {
    case APP_CMD_SAVE_STATE:
        break;
    case APP_CMD_INIT_WINDOW:
        // The window is being shown, get it ready.
        if( app->window != NULL )
        {
            eng->InitDisplay();
            eng->DrawFrame();
        }
        break;
    case APP_CMD_TERM_WINDOW:
        // The window is being hidden or closed, clean it up.
        eng->TermDisplay();
        eng->has_focus_ = false;
        break;
    case APP_CMD_STOP:
        break;
    case APP_CMD_GAINED_FOCUS:
        eng->ResumeSensors();
        //Start animation
        eng->has_focus_ = true;
        break;
    case APP_CMD_LOST_FOCUS:
        eng->SuspendSensors();
        // Also stop animating.
        eng->has_focus_ = false;
        eng->DrawFrame();
        break;
    case APP_CMD_LOW_MEMORY:
        //Free up GL resources
        eng->TrimMemory();
        break;
    }
}

La classe ndk_helper publie APP_CMD_INIT_WINDOW quand android_app_glue reçoit un rappel onNativeWindowCreated() du système. Les applications peuvent normalement effectuer l'initialisation des fenêtres, telles que l'initialisation EGL. Elles procèdent en dehors du cycle de vie de l'activité, car celle-ci n'est pas encore prête.

//Init helper functions
ndk_helper::JNIHelper::Init( state->activity, HELPER_CLASS_NAME );

state->userData = &g_engine;
state->onAppCmd = Engine::HandleCmd;
state->onInputEvent = Engine::HandleInput;