Ejemplo: Teapot

El ejemplo de Teapot se encuentra en el directorio samples/Teapot/, específicamente dentro del directorio raíz de la instalación del NDK. Este ejemplo usa la biblioteca OpenGL para renderizar la icónica tetera de Utah. En concreto, muestra la clase auxiliar ndk_helper, una colección de funciones auxiliares nativas necesarias para implementar juegos y aplicaciones similares como aplicaciones nativas. Esta clase proporciona lo siguiente:

  • Una capa de abstracción, GLContext, que controla ciertos comportamientos específicos del NDK
  • Funciones auxiliares que son útiles pero no están presentes en el NDK, como la detección de toque.
  • Contenedores para llamadas de JNI, para solicitar funciones de la plataforma, como carga de textura.

AndroidManifest.xml

Aquí, la declaración de la actividad no es NativeActivity, sino una subclase de esta: TeapotNativeActivity.

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

En última instancia, el nombre del archivo de objeto compartido creado por el sistema de compilación es libTeapotNativeActivity.so. El sistema de compilación agrega el prefijo lib y la extensión .so; ninguno forma parte del valor que el manifiesto asigna originalmente a android:value.

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

Application.mk

Una app que usa la clase de marco de trabajo NativeActivity no debe especificar un nivel de API de Android inferior a 9, que es el nivel que introdujo esa clase. Para obtener más información sobre la clase NativeActivity, consulta Aplicaciones y actividades nativas.

APP_PLATFORM := android-9

La línea que sigue indica al sistema de compilación que realice compilaciones para todas las arquitecturas admitidas.

APP_ABI := all

A continuación, el archivo indica al sistema de compilación la biblioteca de compatibilidad en tiempo de ejecución C++ que debe usar.

APP_STL := stlport_static

Implementación en Java

El archivo TeapotNativeActivity se encuentra en teapots/classic-teapot/src/com/sample/teapot, específicamente en el directorio raíz de repositorio del NDK en GitHub. Este archivo controla eventos de ciclo de vida de la actividad, crea una ventana emergente para mostrar texto en pantalla con la función ShowUI() y actualiza la velocidad de fotogramas de forma dinámica con la función updateFPS(). Es posible que te interese el siguiente código, ya que prepara la actividad de la app para pantalla completa, inmersiva y sin barras de navegación de sistema. De esa manera, se puede usar toda la pantalla a fin de mostrar marcos de tetera renderizados:

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

Implementación en el lado nativo

En esta sección se analiza la parte de la app Teapot implementada en C++.

TeapotRenderer.h

Estas llamadas a funciones realizan la renderización efectiva de la tetera. Utiliza ndk_helper para el cálculo de la matriz y para reubicar la cámara en función del punto que presione el usuario.

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


ndk_helper::TapCamera* camera_;

TeapotNativeActivity.cpp

Las líneas que figuran a continuación incluyen la clase ndk_helper en el archivo de origen nativo, y definen el nombre de la clase auxiliar.

#include "NDKHelper.h"

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

La primera aplicación de la clase ndk_helper consiste en controlar el ciclo de vida relacionado con EGL asociando los estados contextuales de EGL (creado/perdido) con eventos de ciclo de vida de Android. La clase ndk_helper también permite que la aplicación conserve información contextual de modo que el sistema pueda restablecer una actividad destruida. Esta capacidad es útil, por ejemplo, cuando se rota la máquina de destino (lo cual provoca la destrucción de una actividad, que se restaura inmediatamente en la nueva orientación de la pantalla) o cuando aparece la pantalla de bloqueo.

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

A continuación, ndk_helper proporciona controles de tacto.

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

También proporciona control de la cámara (frustum de la vista de openGL).

ndk_helper::TapCamera tap_camera_;

Luego, la app se prepara para usar los sensores del dispositivo mediante las API nativas que se proporcionan en el NDK.

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

La app llama a las siguientes funciones en respuesta a varios eventos de ciclo de vida de Android y a los cambios de estado del contexto de EGL, y utiliza distintas funcionalidades proporcionadas por ndk_helper a través de la clase Engine.

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

Luego, la siguiente función vuelve a llamar a Java para actualizar la pantalla de la IU.

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

A continuación, esta función vuelve a llamar al código Java para trazar un cuadro de texto superpuesto en la pantalla renderizada del lado nativo, y muestra el recuento de fotogramas.

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

La aplicación obtiene el reloj del sistema y se lo proporciona al procesador para la animación basada en tiempo en función del reloj en tiempo real. Esta información se usa, por ejemplo, para calcular el impulso en el cual la velocidad disminuye como una función del tiempo.

renderer_.Update( monitor_.GetCurrentTime() );

La aplicación ahora gira el marco renderizado al búfer frontal para mostrarlo a través de la función GLcontext::Swap(). Además, controla posibles errores que se produjeron mientras se giraba.

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

El programa pasa eventos de movimiento-toque al detector de gestos definido en la clase ndk_helper. El detector de gestos realiza un seguimiento de los gestos multitáctiles, como pellizcar y arrastrar, y envía una notificación cuando alguno de estos eventos los activa.

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 clase ndk_helper también proporciona acceso a una biblioteca vector-math (vecmath.h), y la usa aquí para transformar coordenadas táctiles.

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

El método HandleCmd() controla comandos publicados desde la biblioteca android_native_app_glue. Para obtener más información sobre el significado de los mensajes, consulta los comentarios en los archivos de origen android_native_app_glue.h y .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 clase ndk_helper publica APP_CMD_INIT_WINDOW cuando android_app_glue recibe una devolución de llamada onNativeWindowCreated() del sistema. Por lo general, las aplicaciones pueden realizar inicializaciones de ventanas, como la inicialización de EGL. Hacen esto fuera del ciclo de vida de la actividad, ya que esta aún no está lista.

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

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