Amostra: Teapot

A amostra Teapot está localizada no diretório samples/Teapot/, no diretório raiz da instalação do NDK. Essa amostra usa a biblioteca OpenGL para renderizar o icônico Utah teapot (link em inglês). Mais especificamente, ela demonstra o uso da classe auxiliar ndk_helper, uma coleção de funções auxiliares nativas necessárias para a implementação de jogos e apps parecidos, como aplicativos nativos. Essa classe fornece:

  • uma camada de abstração, GLContext, que gerencia certos comportamentos específicos do NDK;
  • funções auxiliares que são úteis, mas ausentes no NDK, como detecção de toque;
  • wrappers para chamadas de JNI para recursos de plataforma, como carregamento de textura.

AndroidManifest.xml

A declaração de atividade apresentada aqui não é NativeActivity, mas uma subclasse dela: TeapotNativeActivity.

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

O nome do arquivo do objeto compartilhado que o sistema de compilação cria é libTeapotNativeActivity.so. O sistema de compilação acrescenta o prefixo lib e a extensão .so. Nenhum desses dois faz parte do valor atribuído originalmente pelo manifesto ao android:value.

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

Application.mk

Apps que usam a classe de framework NativeActivity não podem especificar uma API do Android de nível anterior a 9, que introduziu essa classe. Para saber mais sobre a classe NativeActivity, consulte Apps e atividades nativos.

APP_PLATFORM := android-9

A próxima linha instrui o sistema de compilação a criar para todas as arquiteturas compatíveis.

APP_ABI := all

Em seguida, o arquivo instrui qual Biblioteca de Suporte de ambiente de execução C++ o sistema precisa usar.

APP_STL := stlport_static

Implementação em Java

O arquivo TeapotNativeActivity está localizado em teapots/classic-teapot/src/com/sample/teapot, no diretório raiz do repositório NDK no GitHub. Ele gerencia eventos de ciclo de vida da atividade, cria uma janela pop-up para exibir texto na tela com a função ShowUI() e atualiza dinamicamente o frame rate com a função updateFPS(). O código a seguir pode ser interessante para você, porque prepara a atividade do app para ser exibida em tela cheia, de forma imersiva e sem barras de navegação do sistema, de modo que toda a tela possa ser usada para exibir frames teapot 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);
}

Implementação nativa

Esta seção explora a parte do app Teapot implementado em C++.

TeapotRenderer.h

Essas chamadas de função executam a renderização real do teapot. Elas usam ndk_helper para cálculo de matriz e para reposicionar a câmera com base no local onde o usuário toca.

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

ndk_helper::TapCamera* camera_;

TeapotNativeActivity.cpp

As linhas a seguir incluem ndk_helper no arquivo de origem nativo e definem o nome da classe auxiliar.


#include "NDKHelper.h"

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

O primeiro uso da classe ndk_helper é para gerenciar o ciclo de vida relacionado a EGL, associando os estados de contexto de EGL (criados/perdidos) com eventos de ciclo de vida do Android. A classe ndk_helper permite que o app preserve as informações de contexto de modo que o sistema possa restaurar uma atividade destruída. Essa habilidade é útil, por exemplo, quando a máquina de destino é girada, causando a destruição de uma atividade e a restauração imediata na nova orientação, ou quando a tela de bloqueio aparece.

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

A seguir, o ndk_helper fornece controle por toque.

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

Também fornece controle de câmera (tronco de exibição do openGL).

ndk_helper::TapCamera tap_camera_;

Então, o app se prepara para usar os sensores do dispositivo, usando as APIs nativas fornecidas no NDK.

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

O app chama as seguintes funções em resposta a vários eventos do ciclo de vida do Android e mudanças no estado do contexto de EGL, usando diversas funcionalidades fornecidas por ndk_helper por meio da classe Engine.


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

Depois, a função a seguir chama o lado do Java para atualizar a exibição da 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;
}

Então, essa função chama o lado do Java para desenhar uma caixa de texto sobreposta à tela e renderizada no lado nativo, mostrando a contagem 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;
}

O app recebe o relógio do sistema e o fornece ao renderizador para animação do horário com base no relógio em tempo real. Essas informações são usadas, por exemplo, no momento do cálculo, quando a velocidade diminui em função do tempo.

renderer_.Update( monitor_.GetCurrentTime() );

O aplicativo agora inverte o frame renderizado para o buffer frontal para exibição por meio da função GLcontext::Swap(). Ele também gerencia possíveis erros que ocorreram durante o processo de inversão.

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

O programa transmite eventos de toque/movimento ao detector de gestos definido na classe ndk_helper. O detector de gestos rastreia gestos multitoque, como fazer um gesto de pinça e arrastar, e envia uma notificação quando é acionado por qualquer um desses eventos.

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

A classe ndk_helper também fornece acesso a uma biblioteca matemática de vetores (vecmath.h), que é usada aqui para transformar coordenadas de toque.

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

O método HandleCmd() gerencia comandos postados pela biblioteca android_native_app_glue. Para saber mais sobre o significado das mensagens, consulte os comentários nos arquivos de origem 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;
    }
}

A classe ndk_helper posta APP_CMD_INIT_WINDOW quando android_app_glue recebe um callback onNativeWindowCreated() do sistema. Os apps podem executar inicializações de janelas normalmente, como a inicialização de EGL. Eles fazem isso fora do ciclo de vida da atividade, uma vez que a atividade ainda não está 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;