示例:茶壶

茶壶示例位于 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 框架类的应用不得指定低于 9 的 Android API 级别,因为该级别引入了该类。有关 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++ 实现的 Teapot 应用的部分内容。

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

ndk_helper 类在 android_app_glue 从系统接收 onNativeWindowCreated() 回调时发布 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;