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