Esempio: teiera

L'esempio di Teapot si trova nella directory samples/Teapot/, nella directory root dell'installazione NDK. In questo esempio viene utilizzata la libreria OpenGL per visualizzare l'iconica teiera di Utah. In particolare, illustra la classe helper ndk_helper, una raccolta di funzioni helper native necessarie per implementare giochi e applicazioni simili come applicazioni native. Questo corso offre:

  • Un livello di astrazione, GLContext, che gestisce alcuni comportamenti specifici di NDK.
  • Funzioni di supporto utili ma non presenti nell'NDK, come il rilevamento del tocco.
  • I wrapper per JNI richiedono funzionalità della piattaforma come il caricamento delle texture.

File AndroidManifest.xml

La dichiarazione dell'attività qui non è NativeActivity, ma una sottoclasse: TeapotNativeActivity.

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

Essenzialmente, il nome del file di oggetti condivisi creato dal sistema di compilazione è libTeapotNativeActivity.so. Il sistema di build aggiunge il prefisso lib e l'estensione .so; nessuno dei due fa parte del valore che il file manifest assegna originariamente a android:value.

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

Application.mk

Un'app che utilizza la classe framework NativeActivity non deve specificare un livello API Android inferiore a 9, che ha introdotto quella classe. Per ulteriori informazioni sulla classe NativeActivity, consulta Applicazioni e attività native.

APP_PLATFORM := android-9

La riga successiva indica al sistema di build di creare per tutte le architetture supportate.

APP_ABI := all

Il file indica al sistema di build la libreria di supporto per il runtime C++ da utilizzare.

APP_STL := stlport_static

Implementazione lato Java

Il file TeapotNativeActivity si trova in teapots/classic-teapot/src/com/sample/teapot, nella directory root del repository NDK su GitHub. Gestisce gli eventi del ciclo di vita delle attività, crea una finestra popup per mostrare il testo sullo schermo con la funzione ShowUI() e aggiorna la frequenza fotogrammi in modo dinamico con la funzione updateFPS(). Il seguente codice potrebbe interessarti in quanto prepara l'attività dell'app a schermo intero, immersiva e senza barre di navigazione del sistema, in modo che sia possibile utilizzare l'intero schermo per visualizzare i frame delle teapot visualizzati:

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

Implementazione lato nativo

Questa sezione esplora la parte dell'app Teapot implementata in C++.

TeapotRenderer.H

Queste chiamate di funzione eseguono il rendering effettivo della teiera. Utilizza ndk_helper per il calcolo matriciale e per riposizionare la videocamera in base a dove l'utente tocca.

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


ndk_helper::TapCamera* camera_;

TeapotNativeActivity.cpp

Le seguenti righe includono ndk_helper nel file di origine nativo e definiscono il nome della classe helper.


#include "NDKHelper.h"

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

Il primo utilizzo della classe ndk_helper è gestire il ciclo di vita relativo a EGL, associando gli stati del contesto EGL (creati/persi) agli eventi del ciclo di vita di Android. La classe ndk_helper consente all'applicazione di conservare le informazioni sul contesto in modo che il sistema possa ripristinare un'attività eliminata. Questa funzionalità è utile, ad esempio, quando la macchina di destinazione viene ruotata (quindi un'attività viene eliminata e poi ripristinata immediatamente nel nuovo orientamento) o quando viene visualizzata la schermata di blocco.

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

In seguito, ndk_helper offre il controllo touch.

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

Fornisce inoltre il controllo della fotocamera (openGL view frustum).

ndk_helper::TapCamera tap_camera_;

L'app si prepara quindi a usare i sensori del dispositivo usando le API native fornite nell'NDK.

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

L'app chiama le seguenti funzioni in risposta a vari eventi del ciclo di vita di Android e cambiamenti dello stato del contesto EGL, utilizzando varie funzionalità fornite da ndk_helper tramite la classe Engine.


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

Quindi, la seguente funzione richiama il lato Java per aggiornare la visualizzazione dell'interfaccia utente.

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

Successivamente, questa funzione richiama il lato Java per disegnare una casella di testo sovrapposta allo schermo visualizzato sul lato nativo e che mostra il numero di frame.

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'applicazione riceve l'orologio di sistema e lo fornisce al renderer per l'animazione basata sul tempo in base all'orologio in tempo reale. Queste informazioni vengono utilizzate, ad esempio, per calcolare l'impulso, in cui la velocità diminuisce in funzione del tempo.

renderer_.Update( monitor_.GetCurrentTime() );

L'applicazione ora capovolge il frame visualizzato sul buffer anteriore per mostrarlo tramite la funzione GLcontext::Swap(); gestisce anche i possibili errori che si sono verificati durante il processo di capovolgimento.

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

Il programma trasmette gli eventi di movimento tattile al rilevatore di gesti definito nella classe ndk_helper. Il rilevatore di gesti monitora i gesti multi-touch, ad esempio pizzica e trascina, e invia una notifica quando viene attivata da uno di questi eventi.

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 fornisce inoltre accesso a una libreria di matematica vettoriale (vecmath.h), che la utilizza per trasformare le coordinate del tocco.

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

Il metodo HandleCmd() gestisce i comandi pubblicati dalla libreria android_native_app_glue. Per ulteriori informazioni sul significato dei messaggi, consulta i commenti nei file di origine android_native_app_glue.h e .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;
    }
}

Il corso ndk_helper pubblica APP_CMD_INIT_WINDOW quando android_app_glue riceve un callback onNativeWindowCreated() dal sistema. Normalmente le applicazioni possono eseguire inizializzazioni di finestre, come l'inizializzazione EGL. Esegue questa operazione al di fuori del ciclo di vita dell'attività, poiché l'attività non è ancora pronta.

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

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