2013-07-24

Create Android Live Wallpaper with Cocos2d-x

底下敘述使用 Cocos2d-x 製作 Android Live Wallpaper 的方法。(建構環境 : OSX ML,cocos2d-2.1rc0-x-2.1.3,Android NDK r8e,Android SDK r22。執行環境 : Sony Xperia TX with Android 4.1.2)

A. 程式架構 :
Cocos2d-x 本身提供一個便利的方法建立 Android Project ( 執行 Cocos2d-x 裡的 create-android-project.sh 即可 ),從自動建立的 Android Project 大概可以了解 App 模式之 Cocos2d-x 與 Android 作業系統間的關係,目前 Cocos2d-x 的程式架構在 Android 環境下,單一 Process 只能同時擁有及處理單一 GL Context 及 GLSurface,若要直接把 Cocos2dxRenderer 及 Cocos2dxGLSurfaceView 整合至 WallpaperService.Engine 內會衍生不少問題。因此這裡實作把它們整合在 WallpaperService。各別 Engine 在進行繪圖時 ( onDrawFrame ),只實作貼圖 ( Texture ) 的動作 ( 使用 Android 系統提供的 GL Function ),其中圖片的來源則是向 WallpaperService 的 Cocos2dxRenderer 取得 ( 此處的 Cocos2dxRenderer 是以 off-screen rendering 方式產生貼圖 )。

在不同的 GL Context 之間分享貼圖的方法,這裡選擇使用 EGL_KHR_gl_texture_2D_image Extension 的功能來實作,可避免因為使用 glReadPixles 及 glTexImage2D 造成效能低落的問題。

B. 實作程式 :

1. 建立 Android Project
從 cocos2d-x 網站下載檔案 cocos2d-2.1rc0-x-2.1.3.zip ,將壓縮檔解開並編輯檔案 create-android-project.sh,找到 :
# set environment paramters
NDK_ROOT_LOCAL="/home/laschweinski/android/android-ndk-r5"
ANDROID_SDK_ROOT_LOCAL="/home/laschweinski/android/android-sdk-linux_86"
將它們修改成自己電腦裡 NDK 及 Android SDK 的路徑,例如 :
# set environment paramters
NDK_ROOT_LOCAL="/Users/macaronics/Downloads/android-ndk-r8e"
ANDROID_SDK_ROOT_LOCAL="/Users/macaronics/android-sdks"
完成之後執行這個 sh 檔案,執行畫面如下 :
執行 create-android-project.sh

輸入 package path (com.macaronics.cclw) 之後,畫面會出現目前 Android SDK 已安裝的 API (依照各電腦情況不同),此範例選擇的是 id:1 (API Level 10),最後輸入專案名稱 (cclw),就完成了。

打開 eclipse 並匯入 cclw 這個專案,完成之後會發現幾個錯誤回報,那是因為找不到 cocos2dx package,此時匯入 cocos2dx 專案即可解決 ( cocos2dx 專案的路徑為 cocos2d-2.1rc0-x-2.1.3/cocos2dx/platform/android/java )。

匯入 cclw 及 cocos2dx 專案

本專案建立時選擇之 API Level 為 10,因此匯入完成之後檢查 AndroidManifest.xml 檔案,將 :
<uses-sdk android:minSdkVersion="8"/>
改為
<uses-sdk android:minSdkVersion="10"/>
到目前為止已經可以正常編譯並執行 cclw 專案,首先在資料夾 cocos2d-2.1rc0-x-2.1.3/cclw/proj.android 底下執行 build_native.sh,完成之後會看到專案裡的 lib 資料夾多出檔案 libgame.so,接著在 eclipse 點選 cclw 專案,並於功能表上選擇 Run --> Run As --> Android Application 即可實際編譯執行。此外也可直接在 Terminal 裡輸入 :
ant release -Dsdk.dir=/Users/macaronics/android-sdks/
編譯 apk 檔案 (/Users/macaronics/android-sdks/ 為 Android SDK 之路徑)。

2. 新增 PixelBuffer
如圖所示,在 cclw/src/com/macaronics/cclw 資料夾底下增加檔案 PixelBuffer.java :
新增檔案 PixelBuffer.java

PixelBuffer 的作用是向系統要求建立 GL Context 與 Surface Buffer ,它和 GLSurfaceView 的功能幾乎一樣,只是它所建立的 Surface Buffer 是 PBuffer (作為 off-screen rendering 使用),與 GLSurfaceView 建立的 Window Surface Buffer 不同。此段程式碼參考的來源在此,但 EGL Config Chooser 的部分參考 GLSurfaceView 做了一些修改,程式碼內容如下 :
public class PixelBuffer
{
    final static String TAG = "PixelBuffer";
    final static boolean LIST_CONFIGS = true;

    GLSurfaceView.Renderer mRenderer; // borrow this interface
    int mWidth, mHeight;
    Bitmap mBitmap;
        
    EGL10 mEGL; 
    EGLDisplay mEGLDisplay;
    EGLConfig[] mEGLConfigs;
    EGLConfig mEGLConfig;
    EGLContext mEGLContext;
    EGLSurface mEGLSurface;
    GL10 mGL;

    EGLConfigChooser mEGLConfigChooser;
    int mEGLContextClientVersion;
    
    String mThreadOwner;
    private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    private static int EGL_LARGEST_PBUFFER        = 0x3058;
    private static int EGL_OPENGL_ES2_BIT          = 4;


    public PixelBuffer(int width, int height) {
        mWidth = width;
        mHeight = height;
        mEGLContextClientVersion =2;
                
        int[] version = new int[2];
        int[] attribList = new int[] {
            EGL_WIDTH,              mWidth,
            EGL_HEIGHT,             mHeight,
            EGL_LARGEST_PBUFFER,    1,
            EGL_NONE
        };

        int [] context_attribList =new int[] {
            EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, 
            EGL_NONE
        };
                
        // No error checking performed, minimum required code to elucidate logic
        mEGL = (EGL10) EGLContext.getEGL();
        mEGLDisplay = mEGL.eglGetDisplay(EGL_DEFAULT_DISPLAY);
        mEGL.eglInitialize(mEGLDisplay, version);
        Log.i("cclw", "eglInitialized, version ="+version[0]+"."+version[1]);
        Log.i("cclw", "EGL Extension : "+mEGL.eglQueryString(mEGLDisplay, EGL_EXTENSIONS));

        mEGLConfigChooser =new SimpleEGLConfigChooser(8, 8, 8, 0, true);
        mEGLConfig =mEGLConfigChooser.chooseConfig(mEGL, mEGLDisplay);
        if (mEGLConfig ==null)
        {
            mEGLConfigChooser =new SimpleEGLConfigChooser(5, 8, 5, 0, true);
            mEGLConfig =mEGLConfigChooser.chooseConfig(mEGL, mEGLDisplay);
        }
        
        mEGLContext = mEGL.eglCreateContext(mEGLDisplay, mEGLConfig, EGL_NO_CONTEXT, context_attribList);
        mEGLSurface = mEGL.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, attribList);
        mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
        mGL = (GL10) mEGLContext.getGL();
        
        // Record thread owner of OpenGL context
        mThreadOwner = Thread.currentThread().getName();
        Log.i("cclw", "PixelBuffer created. mThreadOwner ="+mThreadOwner);

    }

    public void dispose()
    {
        if (mEGLDisplay !=EGL_NO_DISPLAY)
        {
            mEGL.eglMakeCurrent(mEGLDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
            if (mEGLContext !=EGL_NO_CONTEXT)
                mEGL.eglDestroyContext(mEGLDisplay, mEGLContext);

            if (mEGLSurface !=EGL_NO_SURFACE)
                mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface);
            mEGL.eglTerminate(mEGLDisplay);
        }
    }
    
    public void setRenderer(GLSurfaceView.Renderer renderer) {
        mRenderer = renderer;
        
        // Does this thread own the OpenGL context?
        if (!Thread.currentThread().getName().equals(mThreadOwner)) {
            Log.e(TAG, "setRenderer: This thread does not own the OpenGL context.");
            return;
        }
        
        // Call the renderer initialization routines
        mRenderer.onSurfaceCreated(mGL, mEGLConfig);
        mRenderer.onSurfaceChanged(mGL, mWidth, mHeight);
    }

    public void drawFrame()
    {
        makeCurrent();
        mRenderer.onDrawFrame(mGL);
    }

    public void makeCurrent()
    {
        // Do we have a renderer?
        if (mRenderer == null) {
            Log.e(TAG, "getBitmap: Renderer was not set.");
            return;
        }
        
        // Does this thread own the OpenGL context?
        if (!Thread.currentThread().getName().equals(mThreadOwner)) {
            Log.e(TAG, "getBitmap: This thread does not own the OpenGL context.");
            return;
        }
                
        // Call the renderer draw routine
        mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
    } 

    public interface EGLConfigChooser {
        EGLConfig chooseConfig(EGL10 egl, EGLDisplay display);
    }

    private abstract class BaseConfigChooser implements EGLConfigChooser {
        public BaseConfigChooser(int[] configSpec) {
            mConfigSpec =configSpec;
        }

        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
            int[] num_config = new int[1];
            if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config)) {
                throw new IllegalArgumentException("eglChooseConfig failed");
            }

            int numConfigs = num_config[0];

            if (numConfigs <= 0) {
                throw new IllegalArgumentException("No configs match configSpec");
            }

            EGLConfig[] configs = new EGLConfig[numConfigs];
            if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config)) {
                throw new IllegalArgumentException("eglChooseConfig#2 failed");
            }

            //list configs
            listConfig(configs);

            EGLConfig config = chooseConfig(egl, display, configs);
            if (config == null) {
                throw new IllegalArgumentException("No config chosen");
            }
            return config;
        }

        abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs);
        protected int[] mConfigSpec;
    }

    private class ComponentSizeChooser extends BaseConfigChooser {
        public ComponentSizeChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) {
            super(new int[] {
                    EGL10.EGL_RENDERABLE_TYPE,  EGL_OPENGL_ES2_BIT,
                    EGL10.EGL_SURFACE_TYPE,     EGL_WINDOW_BIT | EGL_PBUFFER_BIT,

                    EGL10.EGL_RED_SIZE,         redSize,
                    EGL10.EGL_GREEN_SIZE,       greenSize,
                    EGL10.EGL_BLUE_SIZE,        blueSize,
                    EGL10.EGL_ALPHA_SIZE,       alphaSize,
                    EGL10.EGL_DEPTH_SIZE,       depthSize,
                    EGL10.EGL_STENCIL_SIZE,     stencilSize,

                    EGL10.EGL_NONE});

            mValue = new int[1];
            mRedSize = redSize;
            mGreenSize = greenSize;
            mBlueSize = blueSize;
            mAlphaSize = alphaSize;
            mDepthSize = depthSize;
            mStencilSize = stencilSize;
        }

        @Override
        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display,
                EGLConfig[] configs) {
            for (EGLConfig config : configs) {
                int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0);
                int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0);
                if ((d >= mDepthSize) && (s >= mStencilSize)) {
                    int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0);
                    int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0);
                    int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0);
                    int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0);
                    if ((r == mRedSize) && (g == mGreenSize) && (b == mBlueSize) && (a == mAlphaSize)) {
                        return config;
                    }
                }
            }
            return null;
        }

        private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) {

            if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) {
                return mValue[0];
            }
            return defaultValue;
        }

        private int[] mValue;
        // Subclasses can adjust these values:
        protected int mRedSize;
        protected int mGreenSize;
        protected int mBlueSize;
        protected int mAlphaSize;
        protected int mDepthSize;
        protected int mStencilSize;
        }

    private class SimpleEGLConfigChooser extends ComponentSizeChooser {
        public SimpleEGLConfigChooser(int red, int green, int blue, int alpha, boolean withDepthBuffer) {
            super(red, green, blue, alpha, withDepthBuffer ? 16 : 0, 0);
        }
    }    
    
    private void listConfig(EGLConfig[] tmpConfig) {
        Log.i("cclw", "Config List {");

        for (EGLConfig config : tmpConfig) {
            int d, s, r, g, b, a;
                    
            // Expand on this logic to dump other attributes        
            d = getConfigAttrib(config, EGL_DEPTH_SIZE);
            s = getConfigAttrib(config, EGL_STENCIL_SIZE);
            r = getConfigAttrib(config, EGL_RED_SIZE);
            g = getConfigAttrib(config, EGL_GREEN_SIZE);
            b = getConfigAttrib(config, EGL_BLUE_SIZE);
            a = getConfigAttrib(config, EGL_ALPHA_SIZE);
            Log.i("cclw", "    <d,s,r,g,b,a> = <" + d + "," + s + "," +  r + "," + g + "," + b + "," + a + ">");
        }

        Log.i("cclw", "}");
    }
        
    private int getConfigAttrib(EGLConfig config, int attribute) {
        int[] value = new int[1];
        return mEGL.eglGetConfigAttrib(mEGLDisplay, config,
                        attribute, value)? value[0] : 0;
    }
}
3. EGL_KHR_gl_texture_2D_image Extension 的部分 :
WallpaperService.Engine 命令 Cocos2d-x 以 off-screen rendering 的方式將圖案畫在 PBuffer 裡,然後將 PBuffer 裡的資料複製到自己的 GL Context,此處複製 PBuffer 資料的步驟使用 EGL_KHR_gl_texture_2D_image Extension 以 C 語言來實作,讓 WallpaperService.Engine 以 JNI 的方式呼叫。實作時,直接將函數寫在檔案 cclw/jni/hellocpp/main.cpp 內,底下列出修改後的結果 :
#include "AppDelegate.h"
#include "platform/android/jni/JniHelper.h"
#include <jni.h>
#include <android/log.h>

#include "cocos2d.h"
#include "HelloWorldScene.h"

#define EGL_EGLEXT_PROTOTYPES

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2ext.h>

#define  LOG_TAG    "main"
#define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)

using namespace cocos2d;

extern "C"
{

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JniHelper::setJavaVM(vm);

    return JNI_VERSION_1_4;
}

void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv*  env, jobject thiz, jint w, jint h)
{
    if (!CCDirector::sharedDirector()->getOpenGLView())
    {
        CCEGLView *view = CCEGLView::sharedOpenGLView();
        view->setFrameSize(w, h);

        AppDelegate *pAppDelegate = new AppDelegate();
        CCApplication::sharedApplication()->run();
    }
    /*
    else
    {
        ccDrawInit();
        ccGLInvalidateStateCache();
        
        CCShaderCache::sharedShaderCache()->reloadDefaultShaders();
        CCTextureCache::reloadAllTextures();
        CCNotificationCenter::sharedNotificationCenter()->postNotification(EVNET_COME_TO_FOREGROUND, NULL);
        CCDirector::sharedDirector()->setGLDefaultValues(); 
    }
    */
}

EGLImageKHR eglImage =NULL;
EGLDisplay eglDisplay =NULL;

void Java_com_macaronics_cclw_cclwservice_nativeAttachEGLImageKHR(JNIEnv*  env, jobject thiz)
{
    if (eglImage !=NULL)
    {
        glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage);
    }
}

void Java_com_macaronics_cclw_cclwservice_nativeDestroyEGLImageKHR(JNIEnv*  env, jobject thiz)
{
    if (eglImage !=NULL && eglDisplay !=NULL)
    {
        bool ret =eglDestroyImageKHR(eglDisplay, eglImage);
        LOGD("cclw, nativeDestroyEGLImageKHR ret val =%d", ret);

        eglImage =NULL;
        eglDisplay =NULL;
    }
}

void Java_com_macaronics_cclw_cclwservice_nativeRenderToTexture(JNIEnv*  env, jobject thiz)
{
    int textureID =HelloWorld::renderToTexture();

    if (eglImage ==NULL)
    {
        LOGD("cclw, nativeRenderToTexture - Generate EGLImageKHR ...");
        EGLint imageAttributes[] = {
            EGL_GL_TEXTURE_LEVEL_KHR,   0, // mip map level to reference
            EGL_IMAGE_PRESERVED_KHR,    EGL_FALSE,
            EGL_NONE,                   EGL_NONE
        };

        eglDisplay =eglGetCurrentDisplay();
        EGLContext eglContext =eglGetCurrentContext();
        LOGD("cclw, eglDisplay=%d, eglContext=%d, textureID=%d", eglDisplay, eglContext, textureID);

        eglImage =
            eglCreateImageKHR(
                eglDisplay,
                eglContext,
                EGL_GL_TEXTURE_2D_KHR,
                reinterpret_cast<EGLClientBuffer>(textureID),
                imageAttributes
            );
    }
}

}
其中值得注意的是,並不是所有的 GPU 都支援 EGL_GL_TEXTURE_2D_KHR (例如: Adreno 205 GPU 不支援),此外,函數 :
HelloWorld::renderToTexture();
利用 Cocos2d-x 提供的 CCRenderTexture 物件將圖案畫在 Texture 裡,實作內容如下 (檔案 HelloWorldScene.cpp) :
CCRenderTexture* g_RT =NULL;
int HelloWorld::renderToTexture()
{
    if (g_RT==NULL)
    {
        g_RT =CCRenderTexture::create(720, 1184, kCCTexture2DPixelFormat_RGBA8888);
        g_RT->retain();
        g_RT->setPosition(ccp(720 / 2, 1184 / 2));
        CCLOG("cclw, create RenderTexture (id=%d)...", g_RT->getSprite()->getTexture()->getName());
    }

    g_RT->beginWithClear(0.0f, 0.0f, 0.0f, 1.0f);
    CCDirector::sharedDirector()->mainLoop();
    g_RT->end();

    return g_RT->getSprite()->getTexture()->getName();
}
其中的 720 是畫面寬度,1184 是畫面高度。

4. WallpaperService 的部分 :
新增檔案 cclwservice.java 於資料夾 cclw/src/com/macaronics/cclw 。檔案的程式碼如底下所示,其中 WallpaperService 主要分為 Engine、Renderer 及 Service (負責創造 Engine Instance 、 Cocos2d-x Renderer 執行緒及收拾殘局) :
public class cclwservice extends WallpaperService {
 
    public final static long NANOSECONDSPERSECOND = 1000000000L;
    public final static long NANOSECONDSPERMICROSECOND = 1000000;
 
    public static native void nativeAttachEGLImageKHR();
    public static native void nativeDestroyEGLImageKHR();
    public static native void nativeRenderToTexture();

    //----------------------
    // LIBRARY
    //
        static {
            System.loadLibrary("game");
        }

    //----------------------
    // VARIABLES
    //
        public static cclwservice inst =null;

        public static PixelBuffer pb =null;
        public static Handler ph =null;
        public static HandlerThread mThread;

        private Cocos2dxRenderer pc2r =null;

    //----------------------
    // MAIN FUNCTION
    //
        @Override
        public Engine onCreateEngine() {

            Log.i("cclw", "cclw - onCreateEngine");
            Engine retEng =null;
            retEng =new GLEngine();

            return retEng;
        }

        @Override
        public void onCreate(){
            Log.i("cclw", "cclw - onCreate");
            super.onCreate();
            inst =this;

            mThread =new HandlerThread("Rendering Thread");
            mThread.start();
            ph =new Handler(mThread.getLooper());
            ph.post(new Runnable(){
                @Override
                public void run(){
                    Log.i("cclw", "cclw - calling Cocos2dxHelper.init...");
                    Cocos2dxHelper.init(cclwservice.inst, null);

                    //prepare offscreen buffer
                    Log.i("cclw", "cclw - prepare offscreen buffer...");
                    if (pb ==null)
                        pb =new PixelBuffer(720, 1184);

                    Log.i("cclw", "cclw - create native renderer...");

                    pc2r =new Cocos2dxRenderer();
                    pc2r.setScreenWidthAndHeight(720, 1184);
                    pb.setRenderer(pc2r);
                }
            });
                    
        }

        @Override
        public void onDestroy(){
            Log.i("cclw", "cclw - onDestroy");
            super.onDestroy();

            //dispose handler
            ph =null;

            //dispose thread
            Log.i("cclw", "cclw - dispose Rendering Thread...");
            mThread.quit();

            cclwservice.nativeDestroyEGLImageKHR();

            Log.i("cclw", "cclw - dispose PixelBuffer");
            if (pb !=null){
                pb.dispose();
            }
            pb =null;

        }

        synchronized static public void renderToTexture(){
            pb.makeCurrent();
            cclwservice.nativeRenderToTexture();
        }

}
函數 renderToTexture 執行時,會透過 JNI 呼叫 cclw/jni/hellocpp/main.cpp 檔案裡的函數 Java_com_macaronics_cclw_cclwservice_nativeRenderToTexture 讓 Cocos2d-x 將圖案畫在 PixelBuffer 所建立之 PBuffer 的 Texture 裡,並將此 Texture 設定為 KHR_image。

底下為 Engine 部分的程式 ( Engine 負責建立自己的 Renderer、接收系統訊息,及乎如同一般的 Activity ) :
//----------------------
// GL Engine
//
private class GLEngine extends Engine
{

    //----------------------
    // VARIABLES
    //
        private WallpaperGLSurfaceView glSurfaceView;
        private boolean rendererHasBeenSet;

        private WallpaperGLRenderer glRenderer;
         
    //----------------------
    // FUNCTION
    //                 
        @Override
        public Bundle onCommand(String action, int x, int y, int z, Bundle extras, boolean resultRequested) {
            Log.i("cclw", "onCommand");
            return super.onCommand(action, x, y, z, extras, resultRequested);
        }

        @Override
        public void onCreate(SurfaceHolder surfaceHolder) {
            super.onCreate(surfaceHolder);

            setTouchEventsEnabled(true);

            Log.i("cclw", "onCreate");
            glSurfaceView = new WallpaperGLSurfaceView(cclwservice.this);

            setEGLContextClientVersion(2);
            glRenderer =new WallpaperGLRenderer();
            setRenderer(glRenderer);

        }

        @Override
        public void onDestroy() {
            super.onDestroy();

            glRenderer.onDestroy();
            glRenderer =null;

            Log.i("cclw", "onDestroy");
            glSurfaceView.onDestroy();
        }

        @Override
        public void onVisibilityChanged(boolean visible) {
            super.onVisibilityChanged(visible);
         
            Log.i("cclw", "onVisibilityChanged, visible ="+visible);
            if (rendererHasBeenSet) {
                if (visible) {
                    Log.i("cclw", "calling onResume...");
                    glSurfaceView.onResume();
                } else {
                    Log.i("cclw", "calling onPause...");
                    glSurfaceView.onPause();
                }
            }
        }

        @Override
        public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset)
        {
            super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset);
            //Log.i("cclw", "onOffsetsChanged, xOffset="+xOffset+", yOffset="+yOffset+", xOffsetStep="+xOffsetStep+", yOffsetStep="+yOffsetStep+", xPixelOffset="+xPixelOffset+", yPixelOffset="+yPixelOffset);
        }

        protected void setRenderer(Renderer renderer){
            glSurfaceView.setRenderer(renderer);
            rendererHasBeenSet =true;
        }

        protected void setEGLContextClientVersion(int version) {
              glSurfaceView.setEGLContextClientVersion(version);
        }


    //------------------------
    // Custom SurfaceView for WallpaperService.engine
    //
        private class WallpaperGLSurfaceView extends GLSurfaceView {
         
            WallpaperGLSurfaceView(Context context) {
                super(context);
            }
         
            @Override
            public SurfaceHolder getHolder() {
                return getSurfaceHolder();
            }
        }

}
值得注意的是 WallpaperGLSurfaceView 類別裡的函數 getHolder,是透過 Engine 類別的函數 getSurfaceHolder 來取得 SurfaceHolder。此外,Engine 所建立的 Renderer 與 Cocos2d-x Renderer 無關,它擁有自己的 GL Context 及 Window Surface Buffer。

WallpaperGLRenderer 的部分 : (底下的程式如同一般的 GL 程式,使用 OpenGL ES 2.0 來顯示一張 Texture,只是這個 Texture 的資料是 PixelBuffer 的 PBuffer 的資料 )
//----------------------
// RENDERER
//
private class WallpaperGLRenderer implements GLSurfaceView.Renderer{

    private String TAG ="cclw";
    
    int[] mTextureNameWorkspace =new int[] {0};

    private final float[] mTriangleVerticesData = { -1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f };
    private final float[] mTriangleNormalData ={0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f};
    private final float[] mTriangleTexCoordData ={0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f };
    private final float[] mTriangleTexCoordData2 ={1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f };

    private FloatBuffer mTriangleVertices;
    private FloatBuffer mTriangleNormal;
    private FloatBuffer mTriangleTexCoord;
    private FloatBuffer mTriangleTexCoord2;

    private final String mVertexShader = "attribute vec4 a_position;   \n"+
        "attribute vec3 a_normal;     \n"+
        "attribute vec2 a_texCoord;   \n"+
        "varying vec2 v_texCoord;     \n"+
        "varying vec3 v_normal;       \n"+
        "void main()                  \n"+
        "{                            \n"+
        "   gl_Position =a_position;  \n"+
        "   v_normal = a_normal;      \n"+
        "   v_texCoord = a_texCoord;  \n"+
        "}                            \n";

    private final String mFragmentShader = "precision mediump float; \n"+
                "varying vec2 v_texCoord;                            \n"+
                "varying vec3 v_normal;                              \n"+
                "uniform sampler2D s_texture;                        \n"+
                "void main()                                         \n"+
                "{                                                   \n"+
                "  gl_FragColor = texture2D( s_texture, v_texCoord );\n"+
                "}                                                   \n";
    
    private int mProgram;
    private int mvPositionHandle;
    private int mvNormalHandle;
    private int mvTexCoordHandle;
    private int mvSamplerHandle;

    private long mLastTickInNanoSeconds;
    public long aniInterval;

    public int mOrientation =0;

    public boolean onDestroyCalled =false;

    public WallpaperGLRenderer() {
        mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleVertices.put(mTriangleVerticesData).position(0);

        mTriangleNormal = ByteBuffer.allocateDirect(mTriangleNormalData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleNormal.put(mTriangleNormalData).position(0);

        mTriangleTexCoord = ByteBuffer.allocateDirect(mTriangleTexCoordData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleTexCoord.put(mTriangleTexCoordData).position(0);

        mTriangleTexCoord2 =ByteBuffer.allocateDirect(mTriangleTexCoordData2.length *4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleTexCoord2.put(mTriangleTexCoordData2).position(0);

        this.mLastTickInNanoSeconds =System.nanoTime();
        aniInterval =(long) (1.0 / 30 * NANOSECONDSPERSECOND);
    }

    public void onDestroy(){
        Log.i("cclw", "onDestroy called...");
        onDestroyCalled =true;
    }
    
    protected class WorkerRunnable implements Runnable{
        private final CountDownLatch doneSignal;
        WorkerRunnable(CountDownLatch doneSignal){
            this.doneSignal =doneSignal;
        }

        public void run(){
            try{
                cclwservice.renderToTexture();
                this.doneSignal.countDown();
            }catch(Exception ex){
                Log.i("cclw", "Error : Runnable return exception : "+ex);

            }                    
        }
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        try{
                final long nowInNanoSeconds = System.nanoTime();
                final long interval = nowInNanoSeconds - this.mLastTickInNanoSeconds;

                //------------------------
                // FETCH DATA
                //
                    EGL10 mEGL = (EGL10) EGLContext.getEGL();
                    EGLSurface mEGLSurface =mEGL.eglGetCurrentSurface(EGL10.EGL_DRAW);
                    EGLDisplay mEGLDisplay =mEGL.eglGetCurrentDisplay();
                    EGLContext mEGLContext =mEGL.eglGetCurrentContext();

                    CountDownLatch doneSignal =new CountDownLatch(1);
                    cclwservice.ph.post(new WorkerRunnable(doneSignal));

                    doneSignal.await();

                //------------------------
                // SETUP BASIC ENVIRONMENT
                //
                    mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
                    if (onDestroyCalled==true)
                    {
                        Log.i("cclw", "onDestroyCalled==true, ignore drawing...");
                        return;
                    }

                //------------------------
                // UPDATE TEXTURE
                //
                    if (mTextureNameWorkspace[0]==0){                                                            
                        //Load texture
                        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
                        gl.glGenTextures(1, mTextureNameWorkspace, 0);
                        Log.i("cclw", "mTextureNameWorkspace[0]="+mTextureNameWorkspace[0]);

                    }

                    gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureNameWorkspace[0]);

                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);

                    cclwservice.nativeAttachEGLImageKHR();

                    gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);


                //------------------------
                // RENDER SCENE
                //
                    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
                    GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
                    GLES20.glUseProgram(mProgram);

                    GLES20.glVertexAttribPointer(mvPositionHandle, 3, GLES20.GL_FLOAT, false, 0, mTriangleVertices);
                    GLES20.glVertexAttribPointer(mvNormalHandle, 3, GLES20.GL_FLOAT, false, 0, mTriangleNormal);

                    if (mOrientation ==1)
                        GLES20.glVertexAttribPointer(mvTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTriangleTexCoord);
                    else if(mOrientation ==0)
                        GLES20.glVertexAttribPointer(mvTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTriangleTexCoord2);

                    GLES20.glEnableVertexAttribArray(mvPositionHandle);
                    GLES20.glEnableVertexAttribArray(mvNormalHandle);
                    GLES20.glEnableVertexAttribArray(mvTexCoordHandle);

                    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
                    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureNameWorkspace[0]);
                    GLES20.glUniform1i(mvSamplerHandle, 0);

                    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);

                //------------------------
                // FPS Limitation
                //
                    final long val =(aniInterval - interval) / NANOSECONDSPERMICROSECOND;
                    if (val >0)
                    {
                        try{
                            Thread.sleep(val);
                        }catch(final Exception e){}
                    }

                    this.mLastTickInNanoSeconds = nowInNanoSeconds;

        }
        catch(Exception e){

        }
    }

    @Override
    public void onSurfaceChanged(GL10 arg0, int arg1, int arg2) { //arg1:width, arg2:height
        // TODO Auto-generated method stub
        Log.i("cclw", "WallpaperGLRenderer::onSurfaceChanged, arg1="+arg1+", arg2="+arg2);

        float myRatio =1184.0f/720.0f;
        mOrientation =0;
        if (arg2>=arg1)
        {
            myRatio =720.0f/1184.0f;
            mOrientation =1;
        }

        int targetHeight =arg2;
        int targetWidth =(int)((float)arg2*myRatio);

        if (targetWidth >arg1)
        {
            targetWidth =arg1;
            targetHeight =(int)((float)arg1/myRatio);
        }

        Log.i("cclw", "WallpaperGLRenderer::onSurfaceChanged, fit targetWidth="+targetWidth+", targetHeight="+targetHeight);
        GLES20.glViewport((int)((arg1-targetWidth)*0.5f), (int)((arg2-targetHeight)*0.5f), targetWidth, targetHeight);
    }

    @Override
    public void onSurfaceCreated(GL10 arg0, EGLConfig arg1) {
        // TODO Auto-generated method stub
        Log.i("cclw", "WallpaperGLRenderer::onSurfaceCreated");

        mProgram =createProgram(mVertexShader, mFragmentShader);
        if (mProgram ==0)
            return;

        mvPositionHandle =GLES20.glGetAttribLocation(mProgram, "a_position");
        if (mvPositionHandle ==-1)
            return;
        
        mvNormalHandle =GLES20.glGetAttribLocation(mProgram, "a_normal");
        mvTexCoordHandle =GLES20.glGetAttribLocation(mProgram, "a_texCoord");
        mvSamplerHandle =GLES20.glGetUniformLocation(mProgram, "s_texture");

        GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
    }

    ...

其中值得注意的是函數 onDrawFrame 被系統呼叫之後,會產生一個實體 WorkerRunnable ,讓負責 cocos2d-x 繪圖的執行緒去執行,目的是讓 cocos2d-x 更新 PBuffer 裡的資料,更新完成之後,onDrawFrame 才繼續執行貼圖的動作。

5. 設定 AndroidManifest.xml
為 WallpaperService 新增 service 標籤,如底下所示 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.macaronics.cclw"
      android:versionCode="1"
      android:versionName="1.0">

    <uses-sdk android:minSdkVersion="10"/>
    <uses-feature android:glEsVersion="0x00020000" />

    <application android:label="@string/app_name"
        android:icon="@drawable/icon">

        <activity android:name=".cclw"
                  android:label="@string/app_name"
                  android:screenOrientation="landscape"
                  android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
                  android:process="com.macaronics.cclw.cclw"
                  android:configChanges="orientation">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <service
            android:name=".cclwservice"
            android:label="@string/app_name"
            android:screenOrientation="portrait"
            android:permission="android.permission.BIND_WALLPAPER"
            android:process="com.macaronics.cclw.cclwservice">
            <intent-filter>
                <action android:name="android.service.wallpaper.WallpaperService" />
            </intent-filter>
            <meta-data android:name="android.service.wallpaper" android:resource="@xml/cclw_res" />
        </service>
                
    </application>
    <supports-screens android:largeScreens="true"
                      android:smallScreens="true"
                      android:anyDensity="true"
                      android:normalScreens="true"/>
</manifest> 
其中的 xml/cclw_res.xml 表示 Live Wallpaper 選單列表時所顯示之資訊,需要額外手動建立,步驟是首先建立資料夾 xml 於 cclw/res,然後建立檔案 cclw_res.xml,並設定內容如下 :
<?xml version="1.0" encoding="utf-8"?>
<wallpaper 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:thumbnail="@drawable/icon" 
/>

6. 設定 jni/Android.mk :
此時若執行 native_build.sh 會顯示一些錯誤,那是因為尚未設定連結 EGL Library,將 jni/Android.mk 修改如下即可正常編譯 :
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := game_shared

LOCAL_MODULE_FILENAME := libgame

LOCAL_SRC_FILES := hellocpp/main.cpp \
                   ../../Classes/AppDelegate.cpp \
                   ../../Classes/HelloWorldScene.cpp
                   
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes                   

LOCAL_LDLIBS := -lEGL

LOCAL_WHOLE_STATIC_LIBRARIES := cocos2dx_static cocosdenshion_static cocos_extension_static
            
include $(BUILD_SHARED_LIBRARY)

$(call import-module,CocosDenshion/android) \
$(call import-module,cocos2dx) \
$(call import-module,extensions)

7. 執行結果 :
動態桌布選單多出了 cocos2d-x 建立的 cclw


將 cclw 設定成動態桌布

完整範例程式碼可以到 Git 下載: http://github.com/phardera/cocos2dx_android_livewallpaper