示例:茶壶

茶壶示例位于 NDK 安装根目录下的 samples/Teapot/ 目录中。此示例使用 OpenGL 库渲染标志性的犹他茶壶。特别是,它展示了 ndk_helper 辅助类,这是一组实现游戏和类似本机应用所需的本机辅助函数。此类提供

  • 一个抽象层 GLContext,用于处理某些 NDK 特定行为。
  • 一些实用但在 NDK 中不存在的辅助函数,例如轻触检测。
  • 用于平台功能的 JNI 调用包装器,例如纹理加载。

AndroidManifest.xml

这里的 activity 声明不是 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 框架类的应用指定的 Android API level 不能低于 9,因为该类是在 API level 9 中引入的。有关 NativeActivity 类的更多信息,请参阅原生 Activity 和应用

APP_PLATFORM := android-9

下一行告诉构建系统为所有支持的架构构建。

APP_ABI := all

接下来,该文件告诉构建系统要使用哪个C++ 运行时支持库

APP_STL := stlport_static

Java 侧实现

TeapotNativeActivity 文件位于 GitHub 上NDK 仓库根目录下的 teapots/classic-teapot/src/com/sample/teapot 中。它处理 activity 生命周期事件,使用 ShowUI() 函数创建弹出窗口以在屏幕上显示文本,并使用 updateFPS() 函数动态更新帧率。以下代码可能会引起您的兴趣,因为它会准备应用的 Activity 以使其全屏、沉浸,并且没有系统导航栏,以便整个屏幕可用于显示渲染的茶壶帧

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++ 实现的部分。

TeapotRenderer.h

这些函数调用执行茶壶的实际渲染。它使用 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 类使应用能够保留上下文信息,以便系统可以恢复被销毁的 activity。例如,当目标设备旋转(导致 activity 被销毁,然后立即在新方向上恢复)或出现锁屏时,此功能非常有用。

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

应用调用以下函数来响应各种 Android 生命周期事件和 EGL 上下文状态更改,使用 ndk_helper 通过 Engine 类提供的各种功能。

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 初始化。由于 activity 尚未准备好,它们会在 activity 生命周期之外执行此操作。

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

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