示例:Teapot

Teapot 示例位于 NDK 安装根目录下的 samples/Teapot/ 目录中。此示例使用 OpenGL 库渲染标志性的犹他茶壶。特别值得注意的是,此示例展示了 ndk_helper 帮助程序类,即以原生应用形式实现游戏和类似应用所必需的一系列原生帮助程序函数。此类提供:

  • 用于处理某些 NDK 特有行为的抽象层 GLContext
  • 一些虽然有用但 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 级别(从级别 9 开始提供该框架类)。如需详细了解 NativeActivity 类,请参阅原生 Activity 和应用

APP_PLATFORM := android-9

下面这行代码会指示构建系统为所有受支持的架构构建代码。

APP_ABI := all

然后,该文件会告知构建系统要使用哪个 C++ 运行时支持库

APP_STL := stlport_static

Java 端实现

在 GitHub 上,TeapotNativeActivity 文件位于 teapots/classic-teapot/src/com/sample/teapotNDK repo 根目录下。它用于处理 Activity 生命周期事件,创建弹出式窗口以使用 ShowUI() 函数在屏幕上显示文本,以及使用 updateFPS() 函数动态更新帧速率。您可能会对以下代码感兴趣,因为该代码会将应用的 Activity 设置为全屏、沉浸模式且没有系统导航栏,以便整个屏幕可用于显示渲染的 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);
}

原生端实现

本部分将介绍 Teapot 应用通过 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 端,以更新界面显示。

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;