範例:Teapot

Teapot 範例位於 NDK 安裝根目錄下的 samples/Teapot/ 目錄中。這個範例使用 OpenGL 程式庫來算繪經典的 Utah Teapot。特別值得注意的是,這個範例展示了 ndk_helper 輔助程式類別,即以原生應用程式形式實作遊戲和類似應用程式所需的一系列原生輔助函式。這個類別提供:

  • 抽象層 GLContext,用於處理特定的 NDK 專屬行為。
  • 一些實用但不存在於 NDK 的輔助函式 (例如輕觸偵測)。
  • 供 JNI 呼叫平台功能 (例如紋理載入) 的包裝函式。

AndroidManifest.xml

此處的活動聲明並非 NativeActivity 本身,而是它的一個子類別:TeapotNativeActivity

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

最終,建構系統建立的共用物件檔案名稱為 libTeapotNativeActivity.so。建構系統會新增 lib 前置字串和 .so 副檔名;但是,這兩者都不是資訊清單最初指派給 android:value 的值的一部分。

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

Application.mk

使用 NativeActivity 架構類別的應用程式不得指定 9 以下的 Android API 級別 (從級別 9 開始提供這個架構類別)。如要進一步瞭解 NativeActivity 類別,請參閱原生活動和應用程式

APP_PLATFORM := android-9

以下這行程式碼會指示建構系統針對所有支援的架構進行建構。

APP_ABI := all

接著,這個檔案會告知建構系統要使用的 C++ 執行階段支援程式庫

APP_STL := stlport_static

Java 端實作

在 GitHub 上,TeapotNativeActivity 檔案位於 NDK 存放區根目錄下的 teapots/classic-teapot/src/com/sample/teapot 中。它能處理活動生命週期事件,建立彈出式視窗以使用函式 ShowUI() 在畫面上顯示文字,以及使用函式 updateFPS() 動態更新畫面更新率。您可能會對以下程式碼感興趣,因為它可以將應用程式的活動設定為全螢幕、沉浸模式,而且不會顯示系統導覽列,以便讓整個螢幕都能顯示算繪的 teapot 畫面:

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

原生端實作

這一節將說明在 C++ 中實作 Teapot 應用程式。

TeapotRenderer.h

這些函式呼叫的作用是實際算繪 Teapot。使用 ndk_helper 進行矩陣計算,並根據使用者輕觸的位置重新調整相機位置。

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


ndk_helper::TapCamera* camera_;

TeapotNativeActivity.cpp

以下幾行程式碼會在原生來源檔案中加入 ndk_helper,以及定義輔助程式類別名稱。


#include "NDKHelper.h"

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

首次使用 ndk_helper 類別是為了處理 EGL 相關生命週期,將 EGL 情境狀態 (已建立/已丟失) 與 Android 生命週期事件建立關聯。ndk_helper 類別讓應用程式能夠保留情境資訊,以便系統還原刪除的活動。例如,當目標機器旋轉時 (使活動遭刪除後立即還原在新的螢幕方向上) 或是出現螢幕鎖定的情況時,這項功能便十分實用。

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

接著,ndk_helper 會提供觸控設定。

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

它還會提供相機控制設定 (openGL 視體)。

ndk_helper::TapCamera tap_camera_;

應用程式接下來會準備透過 NDK 提供的原生 API,來使用裝置的感應器。

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

應用程式會使用 ndk_helper 透過 Engine 類別提供的各項功能,來呼叫以下函式來回應各種 Android 生命週期事件和 EGL 情境狀態變更。


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

然後,以下函式會回呼至 Java 端以更新 UI 顯示。

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

接下來,這個函式會回呼至 Java 端,以便在原生端算繪的螢幕上疊加繪製一個文字方塊,並在其中顯示畫面數。

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

應用程式取得系統時鐘資料,並將資料提供給轉譯器,製作根據即時時鐘的時間動畫。舉例來說,這些資訊可用於計算速度隨時間推移而下降的動量。

renderer_.Update( monitor_.GetCurrentTime() );

應用程式現可透過 GLcontext::Swap() 函式將算繪後的畫面翻轉到前端緩衝區以便顯示,同時還能處理翻轉過程中可能發生的錯誤。

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

程式會將輕觸動作事件傳遞給 ndk_helper 類別中定義的手勢偵測器。手勢偵測器會追蹤多點輕觸手勢 (例如雙指撥動並拖曳),並在受這些事件觸發時傳送通知。

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

ndk_helper 類別也能存取向量數學程式庫 (vecmath.h)。在這個範例中,它將用於轉換輕觸座標。

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

HandleCmd() 方法會處理由 android_native_app_glue 程式庫發布的指令。如要進一步瞭解訊息的含義,請參閱 android_native_app_glue.h.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;
    }
}

android_app_glue 從系統收到 onNativeWindowCreated() 回呼時,ndk_helper 類別就會發布 APP_CMD_INIT_WINDOW。 應用程式可以正常執行視窗初始化,例如 EGL 初始化。應用程式會在活動生命週期之外執行這項作業,因為此時活動尚未準備就緒。

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

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