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

Em seguida, a função a seguir chama a parte em 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;