Android系女史

Android系女史

Android開発など雑多なプログラムの愚痴でもしています。

Amebaでブログを始めよう!
アプリ -> WebにViewを移動したはいいけどこのままだと戻れない。
なのでWebViewにしたときに上部に戻るボタンが配置されるようにして
ボタンを押したらアプリに戻るようにしてみた。

WebView+ボタンの表示とアプリに戻る機能の実装は以下の流れ。

1. LinearLayoutクラスにWebViewとButtonを追加
2. ButtonにはOnClickListenerを追加する
3. OnClickListener()内でアプリのViewに戻すようにする
3. setContentViewにはWebViewではなくLinearLayoutを渡す

LinearLayoutを使用したボタンの配置は「表示するURLの指定」が参考になりました。

自分のプログラムの実装ではアプリのViewクラスとWeb用のViewのクラスを分けたかったので

class WebPage extends LinearLayout implements OnClickListener {

}
というクラスを作成。
この中でWebViewを扱ったり、Layoutしたり、ボタンのクリックイベントを取得したりしました。

メインクラスのonCreate()
// Web用のViewの初期化
webPage = new WebPage(this);
// 重要:レイアウトの前に一旦ContentViewに登録
setContentView(webPage);
webPage.createLayout();


WebPageクラス
// layout用定数
private final int FP = ViewGroup.LayoutParams.FILL_PARENT;
private final int WC = ViewGroup.LayoutParams.WRAP_CONTENT;

// コンストラクタ
public WebPage(Activity main) {
    super(main);
    // アプリのViewを保存しておく
    mainView = main;

    // WebViewの作成
    webView = new WebView(main);
    webView.setWebViewClient(new WebViewClient());

    // ボタンの作成
    btnRetApp = new Button(main);
    btnRetApp.setText("Return");
    btnRetApp.setOnClickListener(this);
}

// レイアウト作成関数
public void createLayout() {
    this.setOrientation(LinearLayout.VERTICAL);
    LinearLayout upper = new LinearLayout(mainView);
    upeer.setOrientation(LinearLayout.HORIZONTAL);

// レイアウトに追加
    upeer.addView(btnRetApp, new LinearLayout.LayoutParams(WC, WC));
    this.addView(upeer, new LinearLayout.LayoutParams(FP, WC));
    this.addView(webView, new LinearLayout.LayoutParams(WC, FP));
}

public void onClick(View v) {
    if (v == btnRetApp) {
        // アプリのViewに戻る
        mainView.moveMainView();
    }
}
はまったのがLinearLayoutクラスにaddViewする前に
setContentViewでLinearLayoutクラスのViewをセットしておかないといけないということでした。
これをしておかないと落ちました。
さて、C++ -> Javaの呼び出しが出来たところで
JavaでWebページを開きます。

方法は2つ

1. アプリからブラウザを立ち上げてWebページを開く
2. アプリ内でWebページを開く

でやりたいことは後者。
前者の場合は


Uri uri = Uri.parse("http://google.co.jp);
Intent i = new Intent(Intent.ACTION_VIEW,uri);
startActivity(i);
で終了。

しかし、SetActivityはstaticメソッドから呼び出せないので
CallStaticVoidMethodではなくCallVoidMethodで呼び出さないといけない。
ついでにCallVoidMethodは引数にjclassではなくjobjectをとるので
Java -> C++の関数を呼び出すときに渡されるjobjectをグローバル変数として保存して使いまわし。
jobjectはNewObject関数で作成も出来るけど
元のJavaクラスとは違うインスタンスになる為なのか何なのか
startActivityするとエラーが出るのでこういう場合は使えない。

でもJNIEnvはグローバル変数として保存->使いまわしが出来ないっぽいのに
jobjectはできるのだろうか?
一応動いたけど。

後者の場合は
アプリ -> Web -> アプリと切替を行いたいので
プログラムの挿入箇所はOnCreate()とWebページに移動させる関数内の2箇所

OnCreate()
public void onCreate(Bundle savedInstanceState) {
  ...
  // WebViewの初期化
  webView = new WebView(this);
  webView.setWebViewClient(new WebViewClient());

  // アプリViewの初期化
  this.view = new SurfaceView(this);
  //this.view.setRenderer(new GLRenderer());
  this.view.getHolder().setType(SurfaceHolder.SURFACE_TYPE_GPU);
  view.getHolder().addCallback(this);
  setContentView(this.view);
  ...
}
Webページの遷移
public void moveWebView()
{
  // Viewの切替
  setContentView(webView);
  webView.loadUrl("http://www.google.co.jp/");
}
という感じのようです。
要はsetContentView()でViewの切替をする感じですかね。
eclipse上でc++コードをデバッグしたいという野望は現在中断。
このままだと進まないのでeclipse上でc++のコンパイルが出来たところで先に行くのです。

で、次はc++からJavaの関数の呼び出しです。

c++->Javaの呼び出しの基本はこんな感じ。

c++側
jclass jc = env->FindClass("com/java/package/ClassName");
if (jc == 0) {
  return false;
}

// メソッドの取得
jmethodID id = env->GetStaticMethodID(jc, "function", "()V");

//メソッドを実行。
env->CallStaticVoidMethod(jc, id);

java側
package com.java.package;

public class ClassName {
  public static void function() {
    // 何かする
  }
}

まずはFindClass関数でJavaクラスをゲット。
このときの"com/java/package"の部分は
パッケージ名でよい模様。
(詳しくは調べてない)

メソッドの取得はGetStaticMethodID。
Javaの関数がstaticじゃない場合はGetMethodIDとのこと。
第二引数が関数名。第三引数が引数と戻り値。
"()V"は引数なしのvoid型を返すという意味です。

で、実際にメソッドの呼び出しがCallStaticVoidMethod
Javaの関数がstaticじゃない場合は同じく"Static"の文字が不要。
そして返り値によって関数名が若干変わるらしい。
void型を返すからCallStaticVoidMethodだけど
これがint型を返すようなときはCallStaticIntMethodなんだとか。

このあたりは「Java/JNI - PukiWiki」がまとめられていて分かりやすかったです。
で、呼び出し方が分かったところでそもそもJNIEnv *envはどうやって取得しようという問題に当たります。

Java -> c -> JavaならJava->cで渡されたJNIEnvをそのまま利用すればよさそうですが
現在
Java -> c
OpenGLのコールバック -> c++ -> Java
なのでJNIEnvクラスを持ってないんです。
最初
Java -> cと呼び出されたときにJNIEnvクラスを保存しておこうかとも思ったのですが
JNIEnvは使いまわしができないようです。

じゃあどうすればいいんだと検索したら「【Android NDK】JNIEnv*の問題」が参考になりました。

要はJNIロード時に自動的に呼び出されるコールバック関数"jint JNI_OnLoad( JavaVM* vm, void* reserved )"の引数であるJavaVM*を保存しておいて
必要になったらこのJavaVMクラスのGetEnvメソッドを使用してJNIEnvを取得すればいいわけです。
これでようやくc++ -> Javaもできた。
さーてAndroidやるぞー(っ・ヮ・)つ□

と思った次の瞬間にはWebGLに浮気。
全然進まない。

さて、onDrawFrame()ではなくタイマー関数の中で描画をしようという試みは
実機でのみ画面のswapがされずに失敗していたのだが
あの後すぐにegl.eglMakeCurrentをしていないからというのに気づく。
……エミュレータだと動くのにっていうのが罠。

というわけで大まかなフレームワークが出来たところで
本格的にC++で書かれたコードをJNIを利用して読み込ませる作業を開始することにする。


その前に。
C++で書かれたコードのデバッグがprintfというのは流石にやりづらいので
eclipseでデバッグできるように奮闘。

cygwin使わなくても
eclipse上でビルド&デバッグする方法は
「Androidでネイティブデバッグ(実機でデバッグも) -その1-」参考にさせて貰いました。

ただこれだけだと何故かeclipse上では#include などなどが見つからないといわれるので
次は「EclipseによるAndroidNDK開発時のjni.hなどのUnresolved inclusionへの対処」のページを参考にパスを通す

cppが大量にある上に多段Makeしている所為で面倒だった。。。
jniフォルダに必要なc++やMakefileをドラッグ&ドロップしてリンク。
これで実行できるようになった。

しかし、c++コードのデバッグがこのままだと出来ない。
画面のちらつきが止まらない問題、
GLSurfaceView.RendererのonDrawFrame()が必ずバッファのスワップを行うらしいので
onDrawFrame()を呼ばないようにしました。

その代わりタイマーを作成して一定時間ごとにに描画関数を呼ぶことにし、
その描画関数の最後で
egl.eglSwapBuffers(eglDisplay, eglSurface);
とやらを呼んでみる。

結果 ---->

エミュレータでは動くけど実機で画面がスワップされないorz

何でだ。。。