Mẫu: Ấm trà

Mẫu Ấm trà nằm trong thư mục samples/Teapot/, trong thư mục gốc của chế độ cài đặt NDK. Mẫu này sử dụng thư viện OpenGL để kết xuất ấm trà Utah nổi tiếng. Cụ thể, mẫu này giới thiệu một lớp trợ giúp ndk_helper, một tập hợp các chức năng trợ giúp gốc cần thiết để triển khai trò chơi và các ứng dụng tương tự dưới dạng ứng dụng gốc. Lớp này cung cấp:

  • Một lớp trừu tượng, GLContext, xử lý một số hành vi dành riêng cho NDK.
  • Các chức năng trợ giúp hữu ích nhưng không có trong NDK, chẳng hạn như chức năng phát hiện thao tác nhấn.
  • Trình bao bọc cho lệnh gọi JNI cho các tính năng nền tảng như tải kết cấu.

AndroidManifest.xml

Phần khai báo hoạt động ở đây không phải là chính NativeActivity, mà là một lớp con trong đó: TeapotNativeActivity.

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

Cuối cùng, tên của tệp đối tượng dùng chung mà hệ thống xây dựng tạo ra là libTeapotNativeActivity.so. Hệ thống xây dựng sẽ thêm tiền tố lib và đuôi .so; cả hai đều không thuộc giá trị mà tệp kê khai gán cho android:value lúc ban đầu.

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

Application.mk

Một ứng dụng sử dụng lớp khung NativeActivity không được chỉ định cấp độ Android API thấp hơn 9, tức là cấp đã đưa ra lớp đó. Để biết thêm thông tin về lớp NativeActivity, hãy xem Hoạt động và ứng dụng gốc.

APP_PLATFORM := android-9

Dòng tiếp theo yêu cầu hệ thống xây dựng tạo cho tất cả cấu trúc được hỗ trợ.

APP_ABI := all

Tiếp theo, tệp này sẽ cho hệ thống xây dựng biết nên sử dụng thư viện hỗ trợ thời gian chạy C++ nào.

APP_STL := stlport_static

Triển khai phía Java

Tệp TeapotNativeActivity nằm trong teapots/classic-teapot/src/com/sample/teapot, thuộc thư mục gốc kho lưu trữ NDK trên GitHub. Tệp này xử lý các sự kiện trong vòng đời hoạt động, tạo cửa sổ bật lên để hiển thị văn bản trên màn hình bằng hàm ShowUI() và linh động cập nhật tốc độ khung hình bằng hàm updateFPS(). Bạn có thể sẽ quan tâm đến mã sau đây vì mã này chuẩn bị cho Hoạt động của ứng dụng ở chế độ toàn màn hình, mang tính sống động và không có các thanh điều hướng hệ thống. Nhờ đó, toàn bộ màn hình có thể được dùng để hiển thị khung ấm trà được kết xuất:

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

Triển khai phía gốc

Phần này khám phá phần ứng dụng Ấm trà (Teapot) được triển khai trong C++.

TeapotRenderer.h

Các lệnh gọi hàm này thực hiện hoạt động kết xuất ấm trà trên thực tế. Phương thức này sử dụng ndk_helper để tính toán ma trận và định vị lại máy ảnh dựa trên vị trí người dùng nhấn vào.

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

ndk_helper::TapCamera* camera_;

TeapotNativeActivity.cpp

Các dòng sau bao gồm ndk_helper trong tệp nguồn gốc và xác định tên lớp trợ giúp.


#include "NDKHelper.h"

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

Lần đầu tiên sử dụng lớp ndk_helper là để xử lý vòng đời liên quan đến EGL, liên kết các trạng thái ngữ cảnh EGL (đã tạo/bị mất) với các sự kiện trong vòng đời của Android. Lớp ndk_helper cho phép ứng dụng lưu giữ thông tin về ngữ cảnh để hệ thống có thể khôi phục hoạt động đã bị huỷ bỏ. Ví dụ: Khả năng này rất hữu ích, chẳng hạn như khi máy mục tiêu bị xoay (khiến một hoạt động bị phá huỷ, rồi sau đó được khôi phục ngay theo hướng mới) hoặc khi màn hình khoá xuất hiện.

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

Tiếp theo, ndk_helper cung cấp tính năng điều khiển cảm ứng.

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

Tính năng này cũng cung cấp chức năng điều khiển máy ảnh (hình chóp cụt quan sát OpenGL).

ndk_helper::TapCamera tap_camera_;

Sau đó, ứng dụng chuẩn bị sử dụng các cảm biến của thiết bị bằng các API gốc được cung cấp trong NDK.

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

Ứng dụng này gọi các hàm sau để phản hồi nhiều sự kiện trong vòng đời của Android và các thay đổi về trạng thái ngữ cảnh EGL, bằng cách sử dụng nhiều chức năng do ndk_helper cung cấp thông qua lớp Engine.


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

Sau đó, hàm sau thực hiện lệnh gọi lại phía Java để cập nhật màn hình giao diện người dùng.

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

Tiếp theo, hàm này thực hiện lệnh gọi lại phía Java để vẽ một hộp văn bản được xếp chồng trên màn hình hiển thị ở phía gốc và hiển thị số khung.

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

Ứng dụng nhận được đồng hồ hệ thống và cung cấp cho trình kết xuất để tạo ảnh động theo thời gian dựa trên đồng hồ thời gian thực. Ví dụ: Bạn có thể sử dụng thông tin này để tính toán động lượng, trong đó tốc độ giảm dần như một hàm thời gian.

renderer_.Update( monitor_.GetCurrentTime() );

Lúc này, ứng dụng sẽ lật khung đã kết xuất vào vùng đệm trước để hiển thị thông qua chức năng GLcontext::Swap(); cũng xử lý các lỗi có thể xảy ra trong quá trình lật.

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

Chương trình này sẽ truyền các sự kiện chuyển động nhấn tới trình phát hiện cử chỉ được xác định trong lớp ndk_helper. Trình phát hiện cử chỉ theo dõi các cử chỉ cảm ứng đa điểm, chẳng hạn như chụm và kéo, đồng thời gửi thông báo khi được kích hoạt bởi bất kỳ sự kiện nào trong số này.

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

Lớp ndk_helper cũng cung cấp quyền truy cập vào thư viện toán vectơ (vecmath.h), sử dụng thư viện đó tại đây để biến đổi toạ độ điểm nhấn.

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

Phương thức HandleCmd() xử lý các lệnh đăng từ thư viện android_native_app_glue. Để biết thêm thông tin về ý nghĩa của các thông báo, hãy tham khảo nhận xét trong tệp nguồn 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;
    }
}

Lớp ndk_helper sẽ đăng APP_CMD_INIT_WINDOW khi android_app_glue nhận được lệnh gọi lại onNativeWindowCreated() từ hệ thống. Các ứng dụng có thể thực hiện khởi chạy cửa sổ như bình thường, chẳng hạn như khởi chạy EGL. Các ứng dụng làm điều này bên ngoài vòng đời hoạt động, vì hoạt động chưa sẵn sàng.

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

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