minghaiの日記 このページをアンテナに追加 RSSフィード

2008-05-13 Androidに美人さんを召喚してみた

Androidに美人さんを召喚してみた

非常にひさしぶりの更新です。*1

今回は人にお見せする目的があるためいつもと違い「ですます調」にて記述しております。


最近、Androidプログラミングを勉強し始めたのですが、とりあえずサンプルをいじって何か面白い物を作ってみようということになり、こんなものを作ってみました。

f:id:minghai:20080514041109p:image

これじゃなんだかわかりません。アホですね。(^^;

このプログラムは一時期各種ニュースサイトやはてブでも大フィーバーを起こした美人さん共有サイト、4uからフィードを読みこみ、そのイメージを表示し、かつ気に入ったイメージをAndroidの壁紙に設定できるという物です。

プログラムAndroidのサンプルにある、API Demo -> Animation -> 3D Transitionを改変させて頂きました。

元のプログラムからの変更点は次になります。

  1. フィードを読みこんでXMLを解析する
  2. イメージをリソースではなく、ネットワークから取ってきてViewに表示する。
  3. メニューを追加し、壁紙の設定を行う

プログラムはこちらにて公開しております。

http://code.google.com/p/feedimageviewer/

".apk"はadb installで入れてください。jarのほうにソースが入っています。まだsubversionの設定をしていないためsubversionから落とすことができません。Subversionから落とせるようにしました。ソースはsvnを用いて落としてください。

さて、以下簡単な解説です。

onCreate

Androidプログラムは基本的にActivityというクラスを用いて作成します。

このActivityは以前にも書きましたが、JavaAppletのような物で、Appletの開発を行った経験のある方でしたら非常に理解が早いと思います。逆にAppletを含めたイベントドリブンなオブジェクト指向プログラムの開発をやったことがない人には最初が少しつらいかもしれません。


通常のプログラムと異なり、Androidプログラムはイベントの発生に従ってイベントハンドラと呼ばれる個々のメソッドが呼び出しを受け、実行される形となります。

Androidではアプリケーションが実行されると、まず起動用のActivityのインスタンスが作成され、そのActivityのonCreateメソッドが実行されるわけです。

とりあえずはAndroidプログラムはonCreateから始まると思って頂いてかまいません。


では実行されるActivityはどこで決定されるのでしょうか?

これはAndroidManifest.xmlです。

ここでAndroidプロジェクトのディレクトリツリーをご覧頂きましょう。

f:id:minghai:20080515085453p:image

これらのディレクトリ(またはフォルダ)はAndroidプロジェクトを作成すると自動的に作成されます。

AndroidManifest.xmlも同じくです。EclipseのWizardが最初に必要な中身を書いてくれるので、必要のない限り編集する必要もありません。

重要なことはAndroidの開発ではディレクトリ構成が決まっていることです。特に"res"と名づけられたディレクトリは後で説明する"R"というクラスと密接な関係にあります。

実際のonCreateです。

@Override
protected void onCreate(Bundle icicle) {
	super.onCreate(icicle);
		
        // Turn off the title bar
        requestWindowFeature(Window.FEATURE_NO_TITLE);

	try {
		URL huc = new URL(
//		"http://feeds.feedburner.com/ffffound/everyone?format=xml");
		"http://4u.straightline.jp/rss");
		InputStream is = huc.openStream();

		Xml.parse(is, Xml.Encoding.UTF_8, rch);

	} catch (IllegalStateException e) {
		Log.d("TEST", e.getMessage(), e);
		return;
	} catch (IOException e) {
		Log.d("TEST", e.getMessage(), e);
		return;
	} catch (SAXException e) {
		Log.d("TEST", e.getMessage(), e);
		return;
	}

	setContentView(R.layout.animations_main_screen);

	mPhotosList = (ListView) findViewById(android.R.id.list);
	mImageView = (ImageView) findViewById(R.id.picture);
	mContainer = (ViewGroup) findViewById(R.id.container);
		
	PHOTOS_NAMES = rch.getTitles();

	// Prepare the ListView
	final ArrayAdapter<String> adapter = 
                  new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, PHOTOS_NAMES);

	mPhotosList.setAdapter(adapter);
	mPhotosList.setOnItemClickListener(this);

	// Prepare the ImageView
	mImageView.setClickable(true);
	mImageView.setFocusable(true);
	mImageView.setOnClickListener(this);

	// Ask the parent to keep the animations after they're done so
	// that we retain the last transformation when we swap the views
	((ViewGroup)mContainer.getParent()).setKeepAnimations(true);
}


まずsuperを読んで親クラスにも引数を渡しています。ここはオブジェクト指向の継承時のお約束なのでこんなものだと思ってください。


次にrequestFeatureにてWindowのタイトルバーを消しています。これを行わないとアプリケーションの名前が一番上に表示されるため少しだけ表示エリアがせまくなります。お好みでどうぞ。


その次にて、いきなりフィードを読んでいます。(^^;

今回コードの美しさとか考えていませんので許してください。

URLがハードコードされていますが、将来的には(気が向いたらとも言う)メニューにて選択できるようにしたいと思います。


ここでandroid.util.Xmlをパーサに用いています。

本家のJavaでもそうですが、androidにはutilパッケージがあり便利なツールが揃っていますのでぜひ一度ドキュメントに目を通してください。


android.util.Xmlですが、SAXのラッパです。Googleのジェイソンさんが携帯はメモリがないのでDOMを使うはやめたほうが良いと仰っていました。

最近はJavaScript脳になっているためXPathしか使っていません。JavaもJDK1.5からXPathがあるのですが、やはりDOMということでやめました。

JavaSE1.6からStAXが使えるのでそちらを使いたかったのですが勉強したのがずっと前だったのでぶっちゃけ覚えておらず今回使っておりません。

rchがSAXのハンドラで、パースが終わるとこれにURL等が貯まります。


ここで重要なのは例外記述のandroid.util.Logです。

Androidには専用のLogツールが付属しており、EclipseのAndroid専用のパースペクティブ"DDMS"にてログを参照することができます。

第1引数がタグで、DDMSにてログを参照するときこの値でフィルタリングを行うことができます。

第2引数がメッセージ、第3引数が例外です。

ジェイソンさんがGoogleではこのLogに力を入れて開発しているのでぜひ使って欲しいとのことです。


次にsetContentViewです。ここからがAndroidの大きな特徴の話になります。

Androidでは先ほど記述したとおりディレクトリ構造が決定されています。その時、"res"ディレクトリの下にアプリケーションリソースXMLにて記述して配置します。Androidの開発環境はそれらのXMLファイルの存在を自動的に感知し、R.javaというソースを勝手に作ります。

Androidの開発者はこのRというクラスからレイアウトファイルを指定するわけです。

このプログラムの場合、"R.layout.animations_main_screen"という記述から"res/layout/animations_main_screen.xml"というファイルのレイアウトが利用されるわけです。


animation_main_screen.xmlの中身はこのようになります。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <ListView
        android:id="@android:id/list"
        android:persistentDrawingCache="animation|scrolling"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layoutAnimation="@anim/layout_bottom_to_top_slide" />

    <ImageView
        android:id="@+id/picture"
        android:scaleType="fitCenter"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:visibility="gone" />

</FrameLayout>

まずFrameLayout、これは最も基本的なレイアウトでSwingのContainerやFrameにあたります。複数のViewを持つことができ、それらは全てFrameの座標(0,0)に置かれます。ViewはXML上で一番最後に記述したものが一番上に来るようです。


次にListView。これは色々なものを複数並べて表示します。今回はイメージのタイトルをフィードから得て表示しています。


その次がImageView。これは名前どおりに画像を表示するViewです。

ここではImageViewのVisibilityを"gone"、つまり非表示にしています。そのため起動直後はListViewが表示されるわけです。


XMLで大事なことはAndroidではXMLのタグ名がAndroidのGUIコンポーネントのクラス名と1対1にて対応していることです。AndroidではGUIコンポーネントをプログラム中でもXMLでも記述することができます。

Androidのドキュメントを読むとViewのクラスにはメソッドと共にXMLの属性が記述されています。この両者の片方を用いれば良い訳です。

この時、プログラムからGUIコンポーネントにアクセスするにはXMLのID属性を用います。IDは必ず一意の名前付けを行う必要があり、R.javaに自動的に登録され、R.id."ID名"の形にてアクセスします。それが上のonCreateにおけるfindViewByIdメソッドの部分です。またIDに"android:"のXML名前空間を用いるとAndroid自体が持つコンポーネントを指すことになります。両者の使い分けは私にはまだ今一理解できていません。


XMLの属性にてListViewとImageViewの両者を画面一杯に設定し、スクロール可やアニメーションを指定しています。そしてプログラム上ではさらに設定が追加されます。


まずAdapterの設定。AdapterとはACアダプタのAdapterと同じことで、あるデータの集合をViewに対して設定するのに用いる変換器のようなものです。ここではRSSフィードを解析した結果の全てのイメージのタイトルをStringの配列として用意し、それをListViewの表示内容として設定しています。


次にリスナーの設定です。JavaAppletやSwingを利用されていた方にはお馴染みですが、Javaではイベントドリブンなプログラムを書くとき大抵Listenerというクラス、またはインタフェースを用意してコールバック関数を設定するわけです。ここでListViewにて表示されているタイトルのどれかがクリックされるとListenerとして設定されたthis、つまりこのActivityのクラス自体"FeedImageViewer"のonItemClickメソッドが実行されるわけです。


さて最後。

ImageViewに対してまずclickableを設定します。これをしないとImageViewはクリックできません。次に同じく通常は操作対象でないImageViewを操作対象にするためにfocusableを設定します。最後にImageViewがクリックされた場合のリスナーを設定します。さっきとはちょっと違った名前のonClickがこれで実行されるようになります。


ほんとの最後。

Activityを呼び出した親に対し、Activityが実行終了した場合にもAnimationの実行をキープするためのおまじない、らしい、です。

ちょっと消しても違いが体感できませんでした。

メニュー

Android携帯のエミュレータにはメニューボタンがあり、実行中のアプリ次第でメニューを利用することができます。

Androidではメニューの設定も実行もイベント駆動です。

次のソースをご覧下さい。

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    boolean result = super.onCreateOptionsMenu(menu);
    menu.add(0, Menu.FIRST, "Set wallpaper");
    return result;
}

@Override
public boolean onOptionsItemSelected(Item item) {
    switch (item.getId()) {
    case Menu.FIRST:
        try {
            setWallpaper(rch.getBitmap(currentPosition));
        } catch (IOException e) {
            Log.d("TEST", e.getLocalizedMessage(), e);
        }
        return true;
    }
    return super.onOptionsItemSelected(item);
}

メニューを作成する場合、ActivityにonCreateOptionsMenuメソッドを実装します。Activityが実行される時、自動的に呼び出されます。

メニューに要素を追加するときにIDを指定します。Menu.FIRSTから始まるint値です。この値によりユーザがメニューのどの要素を選んだかを判断します。

ユーザがメニューを利用したときに呼び出されるのがonOptionsItemSelectedです。

先ほど設定したIDから機能を選択します。今回は壁紙を設定する機能一つしかありません。

壁紙の設定に必要なのはsetWallpaper、これだけです。

あまりにも簡単すぎて感動しました。引数に現在表示しているイメージを設定しています。

onItemClick

Activityが作成され表示されると、Viewの操作に対応する処理はView自身がやってくれるのでプログラマが特に何かを書く必要はありません。ユーザが最初に表示されるListViewから適当なタイトルを選んでクリックすると初めて処理がActivityに帰ってきます。そのときに呼ばれるのが先ほど説明したonItemClickです。


ここではユーザが選択したタイトルに対応するイメージをネットワークから読み込み保存します。その後画面を回転するアニメーションを実行させるとそのずっと先にてそのイメージが表示されます。

public void onItemClick(AdapterView parent, View v, int position, long id) {
	// Pre-load the image then start the animation
	Bitmap bitmap;
	HttpURLConnection huc;
	currentPosition = position;

	try {
			
		bitmap = rch.getBitmap(position);
		if (bitmap == null) {
			huc = ((HttpURLConnection) (new URL(rch.getUrl(position)).openConnection()));
//	mImageView.setImageURI(Uri.parse(PHOTOS_URLS[position]));

			huc.setDoInput(true);
			huc.connect();

			InputStream is = huc.getInputStream();

			bitmap = BitmapFactory.decodeStream(is);
			rch.setBitmap(position, bitmap);
		}
		mImageView.setImageBitmap(bitmap);

	} catch (MalformedURLException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}

	applyRotation(position, 0, 90);
}

このプログラムで一番悩んだのはここです。

最初はImageViewにsetImageURIというメソッドがあるのをドキュメントで読んでそれを設定すればすぐにできるかと思いました。しかしそれではImageViewは全く更新されないのです。この理由はジェイソンさんがGoogleのTさんと英語で話していたのですが、聞き取れませんでした。


結局、後からググって2つ方法があることがわかりました。

1つはWebViewを使う方法。これならHTMLですぐ表示できます。

もう1つが今回採用したBitmapFactoryを用いる方法でした。

引数にInputStreamを入れるだけで勝手にBitmapを作成してくれます。

Bitmapは名前により誤解しやすいのですが、JPEGでもPNGでもGifでも読み込むことができます。


最後にapplyRotaionを呼んでいますが、これがRotation3DAnimationを用いて実際に画面を縦に回転させることになります。

ここは私が書いたプログラムではないのであまり深くは解説しませんが、引数を見るとおわかりになるように90度しかまわしていません。実は90度回してListViewが見えなくなってからListViewとImageViewのVisibilityをそれぞれ反転し、引数で渡したpositionのイメージを表示してからさらに90度回転しているのです。するとListViewが見えなくなりImageViewに切り替わるわけです。ImageViewがクリックされたときにはこれの逆回転を行っています。

Rotation3DAnimationはこのプログラムの作者が独自に作られたAnimationでこのソースを読むとAnimationがどのように作成されるのかよくわかります。またAnimationの実行制御もリスナーで行います。このプログラムではAnimationのリスナーが90度回転終了後に呼び出され続きの作業が実行されます。まさにAndroidプログラミングはリスナーの塊です。

最後に

今回、初めてAndroidプログラムを書いてみたわけですが派手なAnimationを始め高度な機能が簡単に実行できるクラスライブラリの充実ぶりはすごいものがあります。これは最近のRIAの発展に伴うプログラム言語の進化ということで素晴らしいですよね。API DEMOなどを触っているといくらでも作りたい物が浮かんできます。

ただ残念なことですが正式リリース前なので仕方が無いのですが、Androidのドキュメントは公式英文のほうも記述が足りないようです。

JavaDocにもメソッド名しか書いていないものがまだ多くあり、結局サンプルプログラムを読み込んで覚えていくしかないようです。

幸い、最近は情報がネットで蓄積されてきているのでググるだけでかなり回答が見つかります。しかしそれもほとんどは英語であるため英語の苦手な人には厳しい状況と言えるでしょう。


しかし最近になってグーグルジャパンが積極的なプロモーションを始めたり、公式コミュニティができたりしました。また秋葉でも勉強会が開催されるようです。これを機会にぜひ多くの人にAndroidに触れてみてもらえればと思います。

*1:前回のGreasemonkey記事翻訳にてGJ言ってくれた人、はてなスターを一杯つけてくれた人、ブクマしてくれた人達ありがとうございました。