茶壶示例位于 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;