דוגמה: קנקן תה

דוגמת שתיית התה ממוקמת בספרייה samples/Teapot/, תחת NDK בתיקיית השורש של ההתקנה. בדוגמה הזו נעשה שימוש בספריית OpenGL כדי לעבד את יוטה קנקן תה. באופן ספציפי, מוצגת המחלקה המסייעת של ndk_helper, אוסף של פונקציות עזר מותאמות שנדרשות להטמעת משחקים שדומות לאלה של אפליקציות נייטיב. הקורס הזה מספק:

  • שכבת הפשטה, GLContext, שמטפלת בהתנהגויות מסוימות שספציפיות ל-NDK.
  • פונקציות מסייעות שימושיות אבל לא קיימות ב-NDK, כמו זיהוי הקשה.
  • רכיבי wrapper לקריאות JNI עבור תכונות פלטפורמה כמו טעינת טקסטורה.

AndroidManifest.xml

הצהרת הפעילות כאן היא לא NativeActivity עצמה, אבל תת-מחלקה שלו: TeapotNativeActivity.

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

בסופו של דבר, השם של קובץ האובייקט המשותף שמערכת ה-build יוצרת הוא libTeapotNativeActivity.so מערכת ה-build מוסיפה את הקידומת lib ואת .so סיומת; אף אחת מהאפשרויות האלה אינה חלק מהערך שהמניפסט מקצה לו במקור android:value.

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

Application.mk

אפליקציה שמשתמשת במחלקת framework של NativeActivity לא יכולה לציין רמת ה-API של Android נמוכה מ-9, שבה נוסף המחלקה הזאת. לקבלת מידע נוסף על כיתה אחת (NativeActivity), לעיון אפליקציות ופעילויות נייטיב.

APP_PLATFORM := android-9

השורה הבאה מנחה את מערכת ה-build לבנות לכל הארכיטקטורות הנתמכות.

APP_ABI := all

לאחר מכן הקובץ מורה למערכת ה-build ספריית תמיכה בסביבת זמן ריצה ב-C++.

APP_STL := stlport_static

הטמעה בצד Java

הקובץ TeapotNativeActivity נמצא ב-teapots/classic-teapot/src/com/sample/teapot, בספריית השורש של המאגר NDK ב-GitHub. השירות מטפל באירועי מחזור חיים של פעילות, יוצר חלון קופץ להצגת טקסט במסך עם הפונקציה ShowUI() ומעדכנים את קצב הפריימים באופן דינמי באמצעות הפונקציה updateFPS(). הקוד הבא עשוי לעניין אתכם בכך שהוא מכין את הפעילות של האפליקציה למסך מלא, סוחפת וללא סרגלי ניווט של המערכת, כך שניתן יהיה להשתמש בכל המסך להצגת הפריימים של קנקן תה:

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

הטמעה בצד המודעות

בקטע הזה מתואר החלק של אפליקציית Teabot שמוטמעת ב-C++.

Teaspot Renderer.h

הקריאות לפונקציה מבצעות את העיבוד בפועל של קנקן התה. הוא משתמש ndk_helper לחישוב המטריצה ולמיקום המצלמה מחדש בהתאם למקום שבו המשתמש מקיש.

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


ndk_helper::TapCamera* camera_;

TeabotNativeActivity.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 מאפשרת לאפליקציה לשמור על ההקשר כדי שהמערכת תוכל לשחזר פעילות שהושחתה. היכולת הזו שימושית לצורך לדוגמה, כשמתבצעת רוטציה של מכונת היעד (וכתוצאה מכך פעילות יושמד ואז ישוחזר מיד בכיוון החדש), או כשהמנעול מופיעה.

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

לאחר מכן, האפליקציה מתכוננת לשימוש בחיישני המכשיר באמצעות ממשקי ה-API המקוריים שסופקו ב-NDK.

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

הכיתה 'ndk_helper' מפרסמת את APP_CMD_INIT_WINDOW בתאריך android_app_glue מקבל מהמערכת קריאה חוזרת של onNativeWindowCreated(). בדרך כלל אפליקציות יכולות לבצע אתחולי חלונות, כמו 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;