العيّنة: إبريق شاي

يمكنك العثور على عينة إبريق الشاي ضمن الدليل samples/Teapot/، ضمن الدليل الجذري لتثبيت NDK. يستخدم هذا النموذج مكتبة OpenGL لعرض إبريق شاي في ولاية يوتا الشهير. على وجه الخصوص، يعرض هذا التطبيق فئة المساعدة ndk_helper، وهي مجموعة من وظائف المساعد المحلي المطلوبة لتشغيل الألعاب والتطبيقات المشابهة مثل التطبيقات الأصلية. ويوفّر هذا الصف ما يلي:

  • طبقة تجريد، GLContext، تعالج سلوكيات خاصة بـ NDK.
  • هي وظائف مساعدة مفيدة ولكنها غير متوفّرة في NDK، مثل رصد النقرات.
  • تطلب برامج لفّات JNI استخدام ميزات النظام الأساسي مثل تحميل الزخرفة.

ملف AndroidManifest.xml

بيان النشاط هنا ليس 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 أقل من 9، والذي قدّم هذه الفئة. للحصول على مزيد من المعلومات حول الصف NativeActivity، يُرجى الاطّلاع على الأنشطة والتطبيقات الأصلية.

APP_PLATFORM := android-9

يخبر السطر التالي نظام الإنشاء بالإنشاء لجميع البنى المتوافقة.

APP_ABI := all

بعد ذلك، يحدّد الملف لنظام الإنشاء مكتبة دعم وقت تشغيل C++ المطلوب استخدامها.

APP_STL := stlport_static

التنفيذ من جهة جافا

يقع الملف 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);
}

التنفيذ من جهة خارجية

يستكشف هذا القسم جزء تطبيق الشاي الذي تم تنفيذه في C++.

TeaبوتRenderer.h

تؤدي استدعاءات الدالة هذه العرض الفعلي لإبريق الشاي. ويُستخدم ndk_helper لحساب المصفوفة وتغيير موضع الكاميرا بناءً على المكان الذي ينقر عليه المستخدم.

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


ndk_helper::TapCamera* camera_;

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

يستعد التطبيق بعد ذلك لاستخدام أدوات الاستشعار في الجهاز، وذلك باستخدام واجهات برمجة التطبيقات الأصلية التي تم توفيرها في 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;