Android NDKのnative-activityサンプルってC言語で書かれているので分かりづらいですよね、ということで、C++に書き直してみました。
Win32のメッセージループまわりをクラス化したことがある人なら馴染みのある形だと思います。
まずは表示まわりと加速度センサーをログ出力する部分を除いた純粋なアプリケーションクラスだけの例です。
#include <android/log.h> #include <android_native_app_glue.h> #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "nativeactivitytest", __VA_ARGS__)) #define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "nativeactivitytest", __VA_ARGS__)) #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "nativeactivitytest", __VA_ARGS__)) class ThisApp { android_app *app_; struct SavedState { }; SavedState state_; public: explicit ThisApp(android_app *app) : app_(app) { app->userData = this; app->onAppCmd = handleCmdStatic; app->onInputEvent = handleInputStatic; } ThisApp(const ThisApp &) = delete; ThisApp &operator=(const ThisApp &) = delete; ~ThisApp() { app_->userData = nullptr; app_->onAppCmd = nullptr; app_->onInputEvent = nullptr; } private: static void handleCmdStatic(android_app *app, int32_t cmd) { reinterpret_cast<ThisApp *>(app->userData)->handleCmd(cmd); } static int32_t handleInputStatic(android_app *app, AInputEvent *event) { return reinterpret_cast<ThisApp *>(app->userData)->handleInput(event); } void handleCmd(int32_t cmd) { switch(cmd){ // Window case APP_CMD_INIT_WINDOW: LOGI("handleCmd(APP_CMD_INIT_WINDOW)"); break; case APP_CMD_TERM_WINDOW: LOGI("handleCmd(APP_CMD_TERM_WINDOW)"); break; case APP_CMD_WINDOW_RESIZED: LOGI("handleCmd(APP_CMD_WINDOW_RESIZED)"); break; case APP_CMD_WINDOW_REDRAW_NEEDED: LOGI("handleCmd(APP_CMD_WINDOW_REDRAW_NEEDED)"); break; case APP_CMD_CONTENT_RECT_CHANGED: LOGI("handleCmd(APP_CMD_CONTENT_RECT_CHANGED)"); break; case APP_CMD_GAINED_FOCUS: LOGI("handleCmd(APP_CMD_GAINED_FOCUS)"); break; case APP_CMD_LOST_FOCUS: LOGI("handleCmd(APP_CMD_LOST_FOCUS)"); break; // Activity State case APP_CMD_START: LOGI("handleCmd(APP_CMD_START)"); break; case APP_CMD_RESUME: LOGI("handleCmd(APP_CMD_RESUME)"); break; case APP_CMD_PAUSE: LOGI("handleCmd(APP_CMD_PAUSE)"); break; case APP_CMD_STOP: LOGI("handleCmd(APP_CMD_STOP)"); break; case APP_CMD_DESTROY: LOGI("handleCmd(APP_CMD_DESTROY)"); break; case APP_CMD_SAVE_STATE: LOGI("handleCmd(APP_CMD_SAVE_STATE)"); backupState(); break; } } int32_t handleInput(AInputEvent *event) { LOGI("handleInput(type=%d, deviceId=%d, source=%d)", AInputEvent_getType(event), AInputEvent_getDeviceId(event), AInputEvent_getSource(event)); switch(AInputEvent_getType(event)){ case AINPUT_EVENT_TYPE_KEY: LOGI("handleKeyEvent(action=%d, flags=0x%x, keycode=%d, scancode=%d, meta=%d, repeatcount=%d)", AKeyEvent_getAction(event), AKeyEvent_getFlags(event), AKeyEvent_getKeyCode(event), AKeyEvent_getScanCode(event), AKeyEvent_getMetaState(event), AKeyEvent_getRepeatCount(event)); //return 1; break; case AINPUT_EVENT_TYPE_MOTION: LOGI("handleMotionEvent(action=%d, flags=0x%x, meta=%d, buttonstate=%d, edgeflag=%d, p0x=%f, p0y=%f", AMotionEvent_getAction(event), AMotionEvent_getFlags(event), AMotionEvent_getMetaState(event), AMotionEvent_getButtonState(event), AMotionEvent_getEdgeFlags(event), AMotionEvent_getX(event, 0), AMotionEvent_getY(event, 0)); //size_t AMotionEvent_getPointerCount //d AMotionEvent_getPointerId //d AMotionEvent_getToolType //f AMotionEvent_getRawX //...etc(see:<ndkdir>/platforms/android-*/arch-*/usr/include/android/input.h) //return 1; break; } return 0; } void backupState() { LOGI("backupState"); app_->savedStateSize = sizeof(SavedState); app_->savedState = malloc(sizeof(SavedState)); *reinterpret_cast<SavedState *>(app_->savedState) = state_; } void restoreState() { LOGI("restoreState"); if(app_->savedState && app_->savedStateSize == sizeof(SavedState)){ state_ = *reinterpret_cast<const SavedState *>(app_->savedState); } } int getNextEventDelayTimeMilli() { // 遅くとも一定時間後までに processInEventLoop() が呼ばれるようにするには、 // ここでミリ秒を返す。 // それより早い時間で呼ばれることもある。 return -1; } void processInEventLoop() { // なんらかのイベントが処理された後または、 // getNextEventDelayTimeMilli()が返した時間経過したときに呼ばれる。 } int looperIdNext_ = LOOPER_ID_USER; int getNewLooperId() { return looperIdNext_++; } void processUserLooperId(int looperId) { LOGI("processUserLooperId looperId=%d", looperId); // ユーザー定義のLooper ID(LOOPER_ID_USER以上の値)を持つイベントが発生したときに呼ばれる。 } void destroyApplication() { LOGI("destroyApplication"); } public: void run() { // Restore app state restoreState(); // event loop for(;;){ int ident; int events; android_poll_source* source; while((ident = ALooper_pollAll(getNextEventDelayTimeMilli(), NULL, &events, (void**)&source)) >= 0){ // Process this event. if(ident >= 0 && ident < LOOPER_ID_USER){ // Dispatch LOOPER_ID_MAIN, LOOPER_ID_INPUT if (source != NULL) { source->process(app_, source); } } else if(ident >= LOOPER_ID_USER){ processUserLooperId(ident); } // Check if we are exiting. if(app_->destroyRequested != 0){ destroyApplication(); return; } } processInEventLoop(); } } }; void android_main(android_app *state) { app_dummy(); ThisApp thisApp(state); thisApp.run(); }
- アクティビティの状態変更やタッチ入力など、イベントの発生時にログを出力するようにしました。どのタイミングでどの関数が呼ばれるのかを把握することが大切です。
- アクティビティの状態を保存・復元するコードのひな形も一応入ってます。複雑なアプリの場合はちゃんとしたシリアライズの仕組みを入れるべきでしょう。
- イベントループのタイムアウトを決定する仕組みのひな形が入ってます。このあたりを使うと独自のタイマークラスなんかが作れるんじゃないかと思います。
- ユーザー定義のLooper IDという概念がandroid_native_app_glueにあるのですが、それを処理する仕組みも入ってます。native-activityサンプルでは加速度センサーイベントの処理にユーザー定義のIDを使っています。でも、加速度センサーの場合はコールバックを使っても実装できるので、必ずしもユーザー定義のIDを使わなくても良い気がします。
加速度センサー
native-activityサンプルのように加速度センサーの値をログへ出力するには、上のソースを次のように改変します。元のサンプルではLOOPER_ID_USERを使用していましたが、調べたところASensorManager_createEventQueueのコールバック引数を使えば使わずに済みそうだったのでそのようにしてみました。
#include <android/sensor.h> //..略... class SensorMonitor { ASensorManager *sensorManager_ = nullptr; const ASensor *accelerometerSensor_ = nullptr; ASensorEventQueue *sensorEventQueue_ = nullptr; public: SensorMonitor(){} SensorMonitor(const SensorMonitor &) = delete; SensorMonitor &operator=(const SensorMonitor &) = delete; ~SensorMonitor() { destroy(); } void init(android_app *app) { if(!sensorEventQueue_){ sensorManager_ = ASensorManager_getInstance(); accelerometerSensor_ = ASensorManager_getDefaultSensor(sensorManager_, ASENSOR_TYPE_ACCELEROMETER); sensorEventQueue_ = ASensorManager_createEventQueue(sensorManager_, app->looper, ALOOPER_POLL_CALLBACK, handleSensorStatic, this); } } void destroy() { stop(); if(sensorEventQueue_){ ASensorManager_destroyEventQueue(sensorManager_, sensorEventQueue_); sensorEventQueue_ = nullptr; } accelerometerSensor_ = nullptr; } void start() { LOGI("Sensor start"); if(accelerometerSensor_){ ASensorEventQueue_enableSensor(sensorEventQueue_, accelerometerSensor_); ASensorEventQueue_setEventRate(sensorEventQueue_, accelerometerSensor_, (1000L/60)*1000); } } void stop() { LOGI("Sensor stop"); if(accelerometerSensor_){ ASensorEventQueue_disableSensor(sensorEventQueue_, accelerometerSensor_); } } private: static int handleSensorStatic(int fd, int events, void *data) { reinterpret_cast<SensorMonitor *>(data)->handleSensor(); return 1; } void handleSensor() { LOGI("Sensor process"); if(accelerometerSensor_){ ASensorEvent event; while(ASensorEventQueue_getEvents(sensorEventQueue_, &event, 1) > 0){ LOGI("accelerometer: x=%f y=%f z=%f", event.acceleration.x, event.acceleration.y, event.acceleration.z); } } } }; class ThisApp { //...略... SensorMonitor sensor_; public: explicit ThisApp(android_app *app) : app_(app) { //...略... sensor_.init(app); } //...略... void handleCmd(int32_t cmd) { //...略... case APP_CMD_GAINED_FOCUS: LOGI("handleCmd(APP_CMD_GAINED_FOCUS)"); // When our app gains focus, we start monitoring the accelerometer, etc. sensor_.start(); break; case APP_CMD_LOST_FOCUS: LOGI("handleCmd(APP_CMD_LOST_FOCUS)"); // When our app loses focus, we stop monitoring the accelerometer, etc. // This is to avoid consuming battery while not being used. sensor_.stop(); //...略... } //...略...
OpenGL ES
OpenGL ES2.0を使って画面に三角形を表示するには次のように改変します。
#include <memory> #include <string> #include <EGL/egl.h> #include <GLES2/gl2.h> ...略... // OpenGL Utilities template<typename FunGetIv, typename FunGetLog> std::string getGLLogStr(GLuint obj, FunGetIv funGetIv, FunGetLog funGetLog) { GLint len = 0; funGetIv(obj, GL_INFO_LOG_LENGTH, &len); if(len > 1){ std::unique_ptr<char[]> infoLog(new char[len]); funGetLog(obj, len, nullptr, infoLog.get()); return std::string(infoLog.get()); } else{ return std::string(); } } std::string getGLShaderLogInfo(GLuint obj) { return getGLLogStr(obj, glGetShaderiv, glGetShaderInfoLog); } std::string getGLProgramLogInfo(GLuint obj) { return getGLLogStr(obj, glGetProgramiv, glGetProgramInfoLog); } GLuint loadShader(GLenum type, const char *shaderSrc) { GLuint shader = glCreateShader(type); if(shader == 0){ return 0; } glShaderSource(shader, 1, &shaderSrc, nullptr); glCompileShader(shader); GLint compiled = 0; glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); if(!compiled){ LOGI("glCompileShader failed.\n Error:(%s)\n", getGLShaderLogInfo(shader).c_str()); glDeleteShader(shader); return 0; } return shader; } /** * OpenGLES 2.0の初期化からフレームレンダリングまでをカバーしたフレームワークです。 * * このクラスを継承して必要な仮想関数をオーバーライドしてください。 * リソースの読み込み、解放を行うloadResources()、termResources()やinitializeContextState()、フレームのレンダリングを行うdrawFrame()をオーバーライドしてください。 * * そしてネイティブウィンドウ(ANativeWindow *)が有効になった時点でinitWindow()を、無効になった時点でtermWindow()を呼び出すようにします。 * ウィンドウへの描画が必要になった時点でpresentFrame()を呼び出してください。 */ class GLEnvironment { EGLDisplay display_; EGLSurface surface_; //window surface EGLConfig config_; //frame buffer configuration EGLint screenWidth_; EGLint screenHeight_; EGLContext context_; public: GLEnvironment() : display_(EGL_NO_DISPLAY), surface_(EGL_NO_SURFACE), config_(nullptr), screenWidth_(), screenHeight_(), context_(EGL_NO_CONTEXT) {} ~GLEnvironment() { destroy(); } GLEnvironment(const GLEnvironment &) = delete; GLEnvironment &operator=(const GLEnvironment &) = delete; bool create(ANativeWindow *window) { return initContext(window); } void destroy() { disconnectDisplay(); } bool recreate(ANativeWindow *window) { destroy(); return create(window); } // // Attach/Detach ANativeWindow // static const bool preserveEGLContextOnPause = true; bool initWindow(ANativeWindow *window) { LOGI("initWindow"); if(preserveEGLContextOnPause && hasDisplay() && !hasSurface() && hasContext() && isContextInitialized()){ // Resume (Try create window surface by same config) LOGI("Trying to resume"); if(createSurface(window, config_) && makeContextCurrent(window)){ LOGI("resume succeeded"); return true; } LOGI("resume failed"); } // Recreate all objects. return recreate(window); } void termWindow() { if(!preserveEGLContextOnPause){ destroyContext(); } destroySurface(); } // // Presentation // bool presentFrame(ANativeWindow *window) { if(!isContextInitialized()){ LOGE("Context not initialized"); return false; } drawFrame(); if(!swap()){ recreate(window); drawFrame(); if(!swap()){ LOGE("Swap failed twice"); return false; } } return true; } private: bool swap() { if(hasSurface()){ LOGI("swap"); if(!eglSwapBuffers(display_, surface_)){ LOGI("eglSwapBuffers failed"); return false; } } return true; } virtual void drawFrame(){} // // Display // private: bool hasDisplay() const {return display_ != EGL_NO_DISPLAY;} bool connectDisplay() { if(display_ == EGL_NO_DISPLAY){ // Get current display. const EGLDisplay uninitializedDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); if(uninitializedDisplay == EGL_NO_DISPLAY){ // Do not terminate display. LOGE("eglGetDisplay failed"); return false; } if(!eglInitialize(uninitializedDisplay, 0, 0)){ LOGE("eglInitialize failed"); return false; } display_ = uninitializedDisplay; } return true; } void disconnectDisplay() { destroyContext(); destroySurface(); if(display_ != EGL_NO_DISPLAY){ LOGI("eglTerminate"); eglTerminate(display_); display_ = EGL_NO_DISPLAY; } } // // Config // private: static EGLConfig chooseConfig(EGLDisplay display) { struct DesiredConfig { int depth; DesiredConfig(int depth):depth(depth){} }; const DesiredConfig desiredConfigs[] = {DesiredConfig(24), DesiredConfig(16)}; const DesiredConfig * const dcEnd = desiredConfigs + sizeof(desiredConfigs)/sizeof(desiredConfigs[0]); const DesiredConfig *dc; EGLConfig config = nullptr; for(dc = desiredConfigs; dc != dcEnd; ++dc){ const EGLint desiredAttribs[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //Request opengl ES2.0 EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_RED_SIZE, 8, EGL_DEPTH_SIZE, dc->depth, EGL_NONE }; EGLint numConfigs; if(!eglChooseConfig(display, desiredAttribs, &config, 1, &numConfigs)){ LOGE("eglChooseConfig failed"); return nullptr; } if(numConfigs == 1){ break; } } if(config == nullptr){ LOGE("no EGL config"); return nullptr; } LOGI("match config depth=%d", dc->depth); return config; } // // Window Surface // private: bool hasSurface() const { return surface_ != EGL_NO_SURFACE;} bool createSurface(ANativeWindow *window, EGLConfig desiredConfig = nullptr) { if(!connectDisplay()){ return false; } if(surface_ == EGL_NO_SURFACE){ const EGLConfig config = desiredConfig ? desiredConfig : chooseConfig(display_); if(!config){ LOGE("chooseConfig failed"); return false; } // ANativeWindow_setBuffersGeometry EGLint format; eglGetConfigAttrib(display_, config, EGL_NATIVE_VISUAL_ID, &format); ANativeWindow_setBuffersGeometry(window, 0, 0, format); // Create surface. surface_ = eglCreateWindowSurface(display_, config, window, nullptr); if(surface_ == EGL_NO_SURFACE){ LOGE("eglCreateWindowSurface failed. error=%d", eglGetError()); return false; } config_ = config; eglQuerySurface(display_, surface_, EGL_WIDTH, &screenWidth_); eglQuerySurface(display_, surface_, EGL_HEIGHT, &screenHeight_); LOGI("screen size %d x %d", screenWidth_, screenHeight_); } return true; } void destroySurface() { //detachCurrentContext(); // 後続のunloadResourcesのために今はdetachしないことにする。 if(surface_ != EGL_NO_SURFACE){ LOGI("eglDestroySurface(window surface)"); eglDestroySurface(display_, surface_); surface_ = EGL_NO_SURFACE; } } public: int getScreenWidth() const {return screenWidth_;} int getScreenHeight() const {return screenHeight_;} // // Context // private: bool hasContext() const{ return context_ != EGL_NO_CONTEXT;} bool createContext(ANativeWindow *window) { if(!createSurface(window)){ //Needs display_ & config_ return false; } if(context_ == EGL_NO_CONTEXT){ const EGLint attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, //Request opengl ES2.0 EGL_NONE }; context_ = eglCreateContext(display_, config_, nullptr, attribs); if(context_ == EGL_NO_CONTEXT){ LOGE("eglCreateContext failed"); return false; } } return true; } void destroyContext() { termContext(); if(context_ != EGL_NO_CONTEXT){ LOGI("eglMakeCurrent null"); eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); LOGI("eglDestroyContext"); eglDestroyContext(display_, context_); context_ = EGL_NO_CONTEXT; } } // Current Context bool makeContextCurrent(ANativeWindow *window) { if(!createSurface(window)){ return false; } if(!createContext(window)){ return false; } LOGI("eglMakeCurrent"); if(!eglMakeCurrent(display_, surface_, surface_, context_)){ LOGE( "eglMakeCurrent failed" ); return false; } return true; } void detachCurrentContext() { if(display_ != EGL_NO_DISPLAY){ LOGI("detachCurrentContext"); eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); } } bool isContextCurrent() { return context_ != EGL_NO_CONTEXT && eglGetCurrentContext() == context_; } // Context Initialization bool contextInitialized_ = false; protected: bool isContextInitialized() const {return contextInitialized_;} private: bool initContext(ANativeWindow *window) { if(!makeContextCurrent(window)){ return false; } if(!contextInitialized_){ if(!loadResources()){ return false; } initializeContextState(); contextInitialized_ = true; } return true; } void termContext() { if(contextInitialized_){ unloadResources(); contextInitialized_ = false; } } virtual bool loadResources(){return true;} virtual void unloadResources(){} virtual void initializeContextState(){} }; class ThisAppGraphics : public GLEnvironment { GLuint vertexShader_; GLuint fragmentShader_; GLuint programObject_; public: ThisAppGraphics() : GLEnvironment(), vertexShader_(0), fragmentShader_(0), programObject_(0) { } private: virtual bool loadResources() override { LOGI("loadResources"); const char vShaderStr[] = "attribute vec4 vPosition;" "void main(){" " gl_Position = vPosition;" "}"; const char fShaderStr[] = "precision mediump float;" "void main(){" " gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);" "}"; vertexShader_ = loadShader(GL_VERTEX_SHADER, vShaderStr); fragmentShader_ = loadShader(GL_FRAGMENT_SHADER, fShaderStr); programObject_ = glCreateProgram(); glAttachShader(programObject_, vertexShader_); glAttachShader(programObject_, fragmentShader_); glBindAttribLocation(programObject_, 0, "vPosition"); glLinkProgram(programObject_); GLint linked = 0; glGetProgramiv(programObject_, GL_LINK_STATUS, &linked); if(!linked){ LOGI("glLinkProgram failed.\n Error:(%s)\n", getGLProgramLogInfo(programObject_).c_str()); return false; } return true; } virtual void unloadResources() override { LOGI("unloadResources"); if(programObject_){ glDeleteProgram(programObject_); programObject_ = 0; } if(vertexShader_){ glDeleteShader(vertexShader_); vertexShader_ = 0; } if(fragmentShader_){ glDeleteShader(fragmentShader_); fragmentShader_ = 0; } } virtual void initializeContextState() override { glEnable(GL_CULL_FACE); glDisable(GL_DEPTH_TEST); glViewport(0, 0, getScreenWidth(), getScreenHeight()); } virtual void drawFrame() override { LOGI("drawFrame"); GLfloat vertices[] = { 0.0f, 0.5f, 0.0f, -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f }; glClearColor(0, 0, 1, 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(programObject_); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vertices); glEnableVertexAttribArray(0); glDrawArrays(GL_TRIANGLES, 0, 3); } }; class ThisApp { ...略... ThisAppGraphics gl_; ...略... void handleCmd(int32_t cmd) { switch(cmd){ case APP_CMD_INIT_WINDOW: LOGI("handleCmd(APP_CMD_INIT_WINDOW)"); // The window is being shown, get it ready. if(app_->window){ gl_.initWindow(app_->window); gl_.presentFrame(app_->window); } break; case APP_CMD_TERM_WINDOW: LOGI("handleCmd(APP_CMD_TERM_WINDOW)"); // The window is being hidden or closed, clean it up. gl_.termWindow(); break; ...略...
他のプロジェクトでも使い回せるようなコードはGLEnvironmentクラスに収め、このプロジェクトに固有な描画コードはThisAppGraphicsクラスに収めてみました。ThisAppクラスからはapp_->windowが有効になったときにinitWindow()やpresentFrame()を、app_->windowが無効になったときにtermWindow()を呼び出すだけです。
気になった点をいくつか。
- エミュレータだとPauseしてResumeすると画面が真っ暗になります。コンテキストがロストしているのかもしれませんが特にエラーが起きている様子も無いので原因がよく分かりません。Teapotサンプルも同じ現象が起きます。上ソースのpreserveEGLContextOnPauseをfalseにすれば(EGLContextをPause時に破棄してResume時に作り直せば)解消します。
- preserveEGLContextOnPauseをfalseにするとポーズ・レジュームのたびにリソースを作り直します。opengl/java/android/opengl/GLSurfaceView.java - platform/frameworks/base - Git at Google を見ると、Q3Dimension MSM7500のときは必ずコンテキストを作り直すようにする等のコードが入っているので、そのような判定も追加すべきかもしれません。
- 画面を回転させるとNativeActivityは終了して再起動します。これはそういうものっぽいです。嫌ならNativeActivityを使わずにJavaで作ったActivityから呼び出す形で何とかするしかないかも。(2015/08/05追記: APIレベルによるみたい? KOBE GDG: 回転時にActivityを破棄させない方法 )
- ndk_helperのGLContextクラスを使おうかなと思ったのですが、ソースを追っていて気になるところがあった(EGLオブジェクトを破棄せずに作り直しているパスがある気がする)ので自分で実装しました。