(cache) Webブラウザビューチュートリアル

Webブラウザビューチュートリアル

AndroidではWebkitベースのブラウザ機能をWebViewとういうビュークラスからアクセスできる。これを使えば、htmlファイルを表示したり、Webサイトを閲覧するアプリケーションが簡単に作れる。ここでは段階を追ってWebViewの使い方を見てみる。

単純なWebアクセス

まずは一番単純なブラウザを作ってみよう。必要となるものはURLを入力するエリア、URLへ移動を指示するボタン、URLで指定されたWebサイトを表示するエリアとなる。その前に一つしておかなければならないことがある。インターネットへのアクセスは権限が必要なため、マニフェストファイルに宣言がしておく。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.suddenAngerSystem"
      android:versionCode="1"
      android:versionName="1.0">
	<uses-permission android:name="android.permission.INTERNET"></uses-permission>
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".WebAccess"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-sdk android:minSdkVersion="3" />
</manifest>
	

次にレイアウトファイルを作成する。WebViewはそのままWebViewタグとして表現される。WebViewタグの属性は他の要素と変わらないため、特筆すべき事はない。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
	<LinearLayout android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content">
		<EditText android:text="EditText01" android:id="@+id/UriText" android:layout_width="wrap_content" android:layout_height="wrap_content"></EditText>
		<Button android:text="Go" android:id="@+id/GoButton" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
	</LinearLayout>
	<WebView android:id="@+id/web" android:layout_width="fill_parent" android:layout_height="fill_parent" />
</LinearLayout>
	

次にgoボタンを押したら入力されたURLを読みとって、そのURLのWEBサイトを表示するソースを作成しよう。ポイントはWebViewクラスのloadUrlメソッドとrequestFocusメソッドとなる。まず、loadUriメソッドは引数で指定されたURLの読みとりを開始する。(開始するのであって、関数から戻ってきても読み込みが完了しているとは限らない。読み込みの完了を待ち合わせるためには別の処理が必要となる)また、requestFocusメソッドはそのインスタンスにフォーカスを合わせる。(Goボタンを押した際にGoボタンがアクティブだと、ユーザーが間違ってボタンを押して連続してWebサイトを読みにいってしまう可能性があるため)

package com.suddenAngerSystem;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.EditText;

public class WebAccess extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        final Button button = (Button)findViewById(R.id.GoButton);
        final OnClickListener listener = new OnClickListener() {
        	@Override
        	public void onClick(View v) {
        		final EditText edit = (EditText)findViewById(R.id.UriText);
        		final String uriString = edit.getText().toString();
        		final WebView web = (WebView)findViewById(R.id.web);
        		web.loadUrl(uriString);
        		web.requestFocus();
        	}
        };
        button.setOnClickListener(listener);
    }
}
	

簡単なWebブラウザができあがったが、実はまだまだ未完成である。リンクを選択してみると分かるが、リンクを選択するとこのアプリケーションの中でリンク先を読みにいくのではなく、図のように標準のwebブラウザを起動してリンク先を読みにいってしまう。まずはアプリケーション内で閉じた動作とするように修正してみる。

少し賢いWebアクセス

WebViewで起こるイベントに対してsetWebViewClientメソッドを使ってWebViewに対してWebViewClientクラスのインスタンスを登録する事により、特定のイベントをフックすることができる。フックしたイベントについて通常通りの動作をさせることもできるし、イベントを捨ててしまうこともできる。以下にエラーハンドリング以外のフックできるイベントを示す。また、もちろんキー入力やタッチパット入力は通常のViewクラスと同様に扱える。

注:WebViewClientは抽象クラスでもインターフェースでもないため、デフォルトの動作のままでも実は使用できる。(デフォルト動作についてはAndroid公式サイトにも記載がないので試してみるしかない。)

アプリケーション内で閉じた動作とさせるにはこのうちのリソース読み込みイベントをフックすれば良さそうに見えるが、単純にフックを登録すればデフォルトの動作が上書きされるようで、標準のwebブラウザは起動されなくなる。(つまり、以下の様にデフォルトのWebViewClientを登録するだけで良い。)

public class WebAccess extends Activity {
	private WebView web_;
	/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        web_ = (WebView)findViewById(R.id.web);
        final Button button = (Button)findViewById(R.id.GoButton);
        final OnClickListener listener = new OnClickListener() {
        	@Override
        	public void onClick(View v) {
        		final EditText edit = (EditText)findViewById(R.id.UriText);
        		final String uriString = edit.getText().toString();
        		web_.loadUrl(uriString);
        		web_.requestFocus();
        	}
        };
        button.setOnClickListener(listener);

        web_.setWebViewClient(new WebViewClient());
    }
}
	

しかし、これだけではつまらない。ロード中に名言を表示してユーザーの待ち感覚を緩和して見よう。WebViewClientのロード開始はonPageStartedで検知できて、onPageFinishedで検知できる。つまりonPageStartedで名言を表示し、onPageFinishedで名言を消せば良い。

class CustomWebViewClient extends WebViewClient {
	private Dialog dialog_;
	public CustomWebViewClient() {
		super();
		dialog_ = null;
	}
	//ページ読み込み開始時の動作
	@Override
	public void onPageStarted(WebView view, String url, Bitmap favicon) {
		//ダイアログを作成して表示
		dialog_ = new Dialog(view.getContext());
		dialog_.setTitle("待て、しかして希望せよ");
		dialog_.show();
	}
	//ページ読み込み終了時の動作
	@Override
	public void onPageFinished(WebView view, String url) {
		//ダイアログを削除
		dialog_.dismiss();
		dialog_ = null;
	}
}
	

先ほどのWebViewClientはブラウザの描画領域を対象としたイベントをフックするクラスだったが、ブラウザの縁部分(タイトルバーやプログレスバーなど)向けのイベントをフックするWebChromeClientも提供されている。setWebChromeClientでWebViewClientと同様に登録することができ、以下のイベントをフックできる。(こちらについてはほとんどWebViewClientと同様に扱えるため、自分で使ってみて欲しい。)

Webアクセスの小技

Android標準のブラウザには拡大用ボタンなどがある。それはどうやって使うのだろうか?WebViewではいろいろな標準的なデコレーションのon/off設定ができる。やり方としてはgetSettingsメソッドを使って設定アクセス用のWebSettingsクラスのインスタンスを取得し、WebSettingsクラスのメソッドを使用すればよい。デフォルトの拡大用ボタンを表示させるにはsetBuiltInZoomControlsメソッドを使えばよい。

AndroidアプリケーションとWebサイトの連携(Webサイト→Androidアプリケーション)

Android向けアプリケーションを提供している開発社は自社のwebサイトでそのアプリケーションのサポートや、アプリケーションと連携した動作をさせたい場合がある。(例えば、自社のwebサイトでストレージを提供するなど)そうした用途に向けてAndroidではwebサイトから送信されるjavascript内で拡張したDOMオブジェクトにアクセスすることが可能となる。web系を知らない方へ:htmlやxmlの要素同士をツリー上に表したのが標準的なDOMオブジェクトで動的にコンテンツの更新が可能となる。ただし、セキュリティの観点からアクセスできるのは同じドメインのリソースだけ(こっそり有料コンテンツの別サーバーにアクセスされたら困る)でローカルマシンのリソース(こっそりパスワードファイルなどにアクセスされたら困る)にはアクセスできない。

javascript側から文字列を指定し、それをAndroidアプリケーション側で表示させる簡単な例を見てみよう。まずはAndroidアプリケーション側から示す。ここで注意すべき点はjavascriptはメインスレッドと異なるスレッドから起動されるということで、AndroidはViewを生成したスレッド以外からViewに対して変更を加えることはできない。そのため、細工を施す必要がある。詳細についてはサービスの解説で実施しているのでそちらを参照して欲しい。

public class WebAccess extends Activity {
	private WebView web_;
	private static final int JS_NOTIFY = 1000;
	/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        web_ = (WebView)findViewById(R.id.web);
        final Button button = (Button)findViewById(R.id.GoButton);
        final OnClickListener listener = new OnClickListener() {
        	@Override
        	public void onClick(View v) {
        		final EditText edit = (EditText)findViewById(R.id.UriText);
        		final String uriString = edit.getText().toString();
        		web_.loadUrl(uriString);
        		web_.requestFocus();
        	}
        };
        button.setOnClickListener(listener);

        //javascriptの設定はデフォルトで無効のためjavascriptを有効にする
        web_.getSettings().setJavaScriptEnabled(true);
        //javascript側からJSKicker.xxxxxでJSKikerのメソッドにアクセスできるように宣言する
        //Androidは最初の引数のインスタンスからpublic void メソッド名(final String パラメータ名)
        //の型を持つメソッドを引き出し、二番目の引数で指定した名前空間に配置する。
        //名前空間は適当につけているので本当はもっと考えるべき
        web_.addJavascriptInterface(new JSKicker(), "JSKicker");
    }
    //インターフェースを実装しなくても良く、Androidのライブラリ側がリフレクションで解析してくれる
    private class JSKicker {
    	public void kickAndroid(final String argument) {
			//ハンドラにメッセージを送信
			//メッセージはいちいち作成するとコストがかかるため、メッセージプールにあるインスタンスを使用する(obtainMessage)
			//送信内容はメッセージ種別と引数(引数に応じてオーバーロードされているobtainMessageのバリエーションが使える)
			handler_.sendMessage(handler_.obtainMessage(JS_NOTIFY, argument));
    	}
    }

	private Handler handler_ = new Handler() {
		public void handleMessage(android.os.Message msg) {
			switch(msg.what) {
			case JS_NOTIFY:
				//通知された時間を表示する。
	    		final EditText edit = (EditText)findViewById(R.id.UriText);
	    		edit.setText((String)msg.obj);
				break;
			default:
				super.handleMessage(msg);
				break;
			}
		}
	};
}
	

次にjavascript側になる。ただし、webサーバを準備するのは手間なのでアプリケーションにリソースとしてwebページを括り付ける方法を取る。以下の様にassets配下に格納されたファイルはAndroidアプリケーションからfile:///android_asset/~といった特殊なURLでアクセスできるようになる。(もちろんアプリケーション毎のエリアとなる)

テスト用のwebページのファイルについては特に解説しない。(HTMLやjavascriptの資料を見て欲しい。)また、eclipseのデフォルトの設定によって文字エンコードがShift_JISになっている可能性があるので、その場合はUTF-8にしておく必要がある。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>Android連携用webサイト1</title>
</head>
<body>
	<h1>Android連携用webサイト1</h1>
	<form>
		<button onclick="window.JSKicker.kickAndroid('http://www.google.co.jp/');">Android起動</button>
	</form>
</body>
</html>
	

アクセスした際の動作は以下の様になる

クリックした後の動作は以下。

最後に注意を促す。この機能はセキュリティに対する考慮が重要であり、最低限のwebサイトに限定してアクセス化とすべきである。また、サーバー認証していない(SSLで)webサイトに対してアクセスする場合も同様に危険だという事を意識しておいた方が良い。(偽サイトからAndroid端末の中身の情報が盗まれてしまうかもしれない。)

AndroidアプリケーションとWebサイトの連携(Androidアプリケーション→Webサイト)

上記の例とは逆に今度はAndroidアプリケーションからjavascriptへ情報を通知してみる。Androidアプリケーションからjavascriptへの通知は簡単でWebViewクラスのloadUrlメソッドで特殊なjavascript向けのurlを指定すれば良い。

実際の例を見てみよう。まずはjavascript側を示す。testFunctionがAndroidアプリケーションから呼ばれる予定の関数でAndroidアプリケーションから通知された文字列の要素をhtmlドキュメントの一番最後に追加する処理となる。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>Android連携用webサイト1</title>
	<script language="JavaScript">
		function testFunction(argument) {
			var body = document.getElementsByTagName('body')[0];
			var p = document.createElement('p');
			p.appendChild(document.createTextNode(argument));
			body.appendChild(p);
		}
	</script>
</head>
<body>
	<h1>Android連携用webサイト1</h1>
	<form>
		<button onclick="window.JSKicker.kickAndroid('http://www.google.co.jp/');">Android起動</button>
	</form>
</body>
</html>
	

次にソース側となる。単純にloadUrlメソッドに引数として("javascript:testFunction('call test function')"を渡しているだけとなる。

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        web_ = (WebView)findViewById(R.id.web);
        final Button button = (Button)findViewById(R.id.GoButton);
        final OnClickListener listener = new OnClickListener() {
        	@Override
        	public void onClick(View v) {
        		final EditText edit = (EditText)findViewById(R.id.UriText);
        		final String uriString = edit.getText().toString();
        		web_.loadUrl(uriString);
        		web_.requestFocus();
        	}
        };
        button.setOnClickListener(listener);

        final Button addButton = (Button)findViewById(R.id.AddButton);
        final OnClickListener addListener = new OnClickListener() {
        	@Override
        	public void onClick(View v) {
        		web_.loadUrl("javascript:testFunction('call test function')");
        	}
        };
        addButton.setOnClickListener(addListener);

        //javascriptの設定はデフォルトで無効のためjavascriptを有効にする
        web_.getSettings().setJavaScriptEnabled(true);
        //javascript側からJSKicker.xxxxxでJSKikerのメソッドにアクセスできるように宣言する
        //Androidは最初の引数のインスタンスからpublic void メソッド名(final String パラメータ名)
        //の型を持つメソッドを引き出し、二番目の引数で指定した名前空間に配置する。
        //名前空間は適当につけているので本当はもっと考えるべき
        web_.addJavascriptInterface(new JSKicker(), "JSKicker");
    }
	

起動して4回addボタンを押した後の動作は以下の様になる

参考資料

調査中。(あまり良い資料がない)