2011年3月31日木曜日

GWT UiBinder を使う

GWT 2.0 から UiBinder を使うことで、 XML で Widget と DOM structure を作成できるようになりました。

利点
 ・ UI(XMLテンプレートでの宣言)とプログラムの動作(Javaクラス)を
  分離できる
 ・ 生産性とメンテナンスに優れている
   ・ スクラッチからUIを作成するのが簡単
   ・ テンプレートをまたいで簡単にコピペできる
 ・ Java のコードより XML,HTML,CSS に親しいデザイナーとのコラボレートが
  楽になる
 ・ HTMLのモックからインタラクティブなUIへの段階的な移行が可能
 ・ コンパイル時に Java ソースとXML間の相互参照をチェックできる
 ・ GWT's i18n facility と連携して国際化を直接サポートする
 ・ 重い widget や panel よりも軽い HTML element を使いやすく
  することで、ブラウザリソースをより効果的に使うことができる

注意点
 ・ レンダラーではない
 ・ ループや条件分岐はマークアップ内にはない

---------------------------------------------

■ New UiBinder Wizard

 eclipse で

 [File] - [New] - [UiBinder]

 で UiBinder 生成ダイアログが表示されます。




 ・ Souce folder: ソースフォルダ

 ・ Package: 生成される UiBinder のファイルを入れるパッケージ

 ・ Name: 生成される UiBinder のファイル名
    例) Hoge と入力すると Hoge.ui.xml と Hoge.java が作成される

 ・Create UI based on:
   ベースを widget にするか HTML にするか選択する

   ・widget: ベースが <g:HTMLPanel\> になる


<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
<ui:style>
.important {
font-weight: bold;
}
</ui:style>
<g:HTMLPanel>
Hello,
<g:Button styleName="{style.important}" ui:field="button" />
</g:HTMLPanel>
</ui:UiBinder>


   ・HTML: ベースが <div> になる


<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder">
<ui:style>
.important {
font-weight: bold;
}
</ui:style>
<div>
Hello,
<span class="{style.important}" ui:field="nameSpan" />
</div>
</ui:UiBinder>


 ・Do you want to add sample content?:
   生成される UiBinder にサンプルコードを入れるかどうか

 ・Do you want to add comments?:
   生成される UiBinder にコメントを入れるかどうか


■ owner class of UiBinder templates

 レイアウトを定義した XML (Hoge.ui.xml) のエレメントをコードから扱うための owner class があります。
 New UiBinder Widzard を使って UiBinder を生成した場合は、この owner class も自動で生成されます。


 ・ HTMLベースの場合

public class Main2 extends UIObject {

private static Main2UiBinder uiBinder = GWT.create(Main2UiBinder.class);

interface Main2UiBinder extends UiBinder<Element, Main2> {
}

@UiField
SpanElement nameSpan;

public Main2() {
setElement(uiBinder.createAndBindUi(this));
}

public Main2(String firstName) {
setElement(uiBinder.createAndBindUi(this));
nameSpan.setInnerText(firstName);
}

public void setName(String name) { nameSpan.setInnerText(name); }
}

HTMLベースの場合は owner class は UIObject を継承します。


 ・ Widgetベースの場合

public class Main extends Composite {

private static MainUiBinder uiBinder = GWT.create(MainUiBinder.class);

interface MainUiBinder extends UiBinder<Widget, Main> {
}

public Main() {
initWidget(uiBinder.createAndBindUi(this));
}

@UiField
Button button;

public Main(String firstName) {
initWidget(uiBinder.createAndBindUi(this));
button.setText(firstName);
}

@UiHandler("button")
void onClick(ClickEvent e) {
Window.alert("Hello!");
}
}

Widget ベースの場合は owner class は Composite を継承します。

 また、xmlns:g='urn:import:com.google.gwt.user.client.ui' が宣言されていることで、com.google.gwt.user.client.ui パッケージのクラスを g プレフィックスと Java クラス名から <g:ListBox> のようにエレメントとして使うことができます。
 各ウィジェットのプロパティを設定するための Java-Bean スタイル規則に従ったメソッドは、次の方法でエレメントの属性として使うことができます。

 Widget#setHogeFuga() → <g:Widget hogeFuga="value">

 set をはずし最初を小文字にします。


Widgetの階層内で HTML markup を使いたい場合は、HTMLPanel か HTMLWidget のインスタンスが必要です。そのため、<g:HTMLPanel></g:HTMLPanel> や <g:HTMLWidget></g:HTMLWidget>の中に HTML markup を書きます。



■ インスタンスの生成

EntryPoint などから owner クラスのインスタンスを生成することでUIを作成します。


Main2 main2 = new Main2();
Document.get().getBody().appendChild(main.getElement());
main2.setName("World");




RootPanel.get().add(new Main());



・UiBinder<U, O> interface

UiBinder の owner class は生成するUIが定義された XML を指定するための UiBinder<U, O> interface を持ちます。

 ・ U : ui.xml ファイルで定義されているルートエレメントのタイプ
    例) HTMLベースの場合 : Element
    例) Widgetベースの場合 : Widget
 ・ O : @UiFeilds を埋めるオーナータイプ
    Hoge.ui.xml の Hoge 部分


 ui.xml ファイルでは、任意のオブジェクト(任意の DOM element を含む)を定義できる。
 定義した任意のオブジェクトは ui:field 属性で名前をつけることで、Java コードから扱えるようになる。ui:field を同じ名前の変数に @UiField アノテーションをつけて定義すると、uiBinder.createAndBindUi(this) の実行時に、SpanElement の変数に適切なインスタンスがセットされる。
@UiField をつけた変数は private にできない。



 

2011年3月30日水曜日

GWT 主要クラスの javadoc へのリンク

Fundamental classes used in client-side GWT code.
  ・com.google.gwt.core.client
    ・EntryPoint
    ・GWT

Classes for HTML Canvas 2D support.
  ・com.google.gwt.canvas.client
  ・com.google.gwt.canvas.dom.client

Types related to DOM events
  ・com.google.gwt.event.dom.client


Classes for parsing and creating JSON encoded values
  ・com.google.gwt.json.client

java.util.logging support in GWT
  ・com.google.gwt.logging.client
  ・com.google.gwt.logging.server
  ・com.google.gwt.logging.shared


A package for manging client-server requests
  ・com.google.gwt.requestfactory.client


Contains the client-side APIs for deRPC
  ・com.google.gwt.rpc.client


Classes used to generate user interfaces using declarative ui.xml files
  ・com.google.gwt.uibinder.client


Fundamental user-interface classes used in client-side GWT code
  ・com.google.gwt.user.client

Widgets, Panels, and other user-interface classes
  ・com.google.gwt.user.client.ui



 
 

2011年3月29日火曜日

GWT Development Mode の shell にログを出す

GWT クラスの static メソッドの log() を使います。

log(String message)
  Development Mode shell のログにメッセージを出力する
  例) GWT.log("hoge");
  

log(String message, java.lang.Throwable e)
  Development Mode shell のログにメッセージを出力する
  Throwable 付き
  例) GWT.log("hoge", new java.lang.Throwable("fuga"));



 

2011年3月28日月曜日

GWT ページのベース情報を取得する

GWT クラスの Static メソッドで URL などのさまざまな情報を取得することができます。

例として、

http://hoge.com/fuga.html がモジュールの HTML で、

Fuga.gwt.xml が次のようになっているとします。


<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='fuga'>
...
<entry-point class='yanzm.products.favapp.client.Fuga'/>
..
</module>


getHostPageBaseURL()
  ホストページの URL prefix
  例)http://hoge.com/

getModuleBaseURL()
  モジュールのベース URL
  例)http://hoge.com/fuga/

getModuleName()
  モジュール名
  例)fuga

getPermutationStrongName()
  permutation's strong name  (* よくわからない)
  例)HostedMode /

getUniqueThreadId()
  Production Mode では空の文字列
  Development Mode では各スレッドに対する一意の文字列
  別のタブやウィンドウで Development Mode にアクセスすると
  ことなる文字列になる
  例)DevModeThread1, DevModeThread12, ...

getVersion()
  GWT のバージョン
  例)2.2.0

isClient()
  通常の GWT 環境内で動作している場合 true を返す

isProdMode()
  Production Mode で動作しいる場合 true を返す

isScript()
  走っているプログラムがスクリプトかバイトコードかを返す
  スクリプトの場合 true を返す




 

2011年3月21日月曜日

Android TableLayout, RelativeLayout で RadioButton を使う

複数の RadioButton をまとめて、そのスコープを設定するための RadioGroup という ViewGroup があります。

この RadioGroup は LinearLayout を継承していて、子要素が RadioButton でなければいけません。
そのため、TableLayout や RelativeLayout のレイアウトでは、RadioButton のスコープを設定することができません。

「ないなら、作ってしまえ」

ということで、作りました。
RadioGroup のソースを見て、ほとんどそのまま LinearLayout を TableLayout, RelativeLayout に置き換えた感じです。


■ TableRadioGroup

 ・ダウンロードはここから TableRadioGroup.java at github

■ RelativeRadioGroup

 ・ダウンロードはここから RelativeRadioGroup.java at github


■ 使い方

 ・TableRadioGroup




<?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"
>
<yanzm.products.customview.TableRadioGroup
android:id="@+id/table"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<TableRow
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<RadioButton
android:id="@+id/radio1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="radio1"
/>
<RadioButton
android:id="@+id/radio2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="radio2"
/>
<RadioButton
android:id="@+id/radio3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="radio3"
/>
</TableRow>
<TableRow
android:id="@+id/group2"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<RadioButton
android:id="@+id/radio4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="radio4"
/>
<RadioButton
android:id="@+id/radio5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="radio5"
/>
<RadioButton
android:id="@+id/radio6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="radio6"
/>
</TableRow>
<TableRow
android:id="@+id/group3"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<RadioButton
android:id="@+id/radio6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="radio7"
/>
<RadioButton
android:id="@+id/radio8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="radio8"
/>
<RadioButton
android:id="@+id/radio9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="radio9"
/>
</TableRow>
</yanzm.products.customview.TableRadioGroup>
<TextView
android:id="@+id/textview"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>



public class MainActivity extends Activity {

private TextView mTextView;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

mTextView = (TextView)findViewById(R.id.textview);

TableRadioGroup group = (TableRadioGroup)findViewById(R.id.table);
group.setOnCheckedChangeListener(new TableRadioGroup.OnCheckedChangeListener() {

public void onCheckedChanged(TableRadioGroup group, int checkedId) {
mTextView.setText("CheckedId : " + checkedId);
}
});
}
}




 ・RelativeRadioGroup




<?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"
>
<yanzm.products.customview.RelativeRadioGroup
android:id="@+id/relative"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<RadioButton
android:id="@+id/radio1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:text="radio1"
/>
<RadioButton
android:id="@+id/radio2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/radio1"
android:text="radio2"
/>
<RadioButton
android:id="@+id/radio3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/radio1"
android:layout_below="@id/radio2"
android:text="radio3"
/>
<RadioButton
android:id="@+id/radio4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/radio1"
android:layout_below="@id/radio3"
android:text="radio4"
/>
<RadioButton
android:id="@+id/radio5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/radio4"
android:layout_below="@id/radio3"
android:text="radio5"
/>
<RadioButton
android:id="@+id/radio6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/radio4"
android:layout_below="@id/radio5"
android:text="radio6"
/>
<RadioButton
android:id="@+id/radio7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/radio4"
android:layout_below="@id/radio6"
android:text="radio7"
/>
<RadioButton
android:id="@+id/radio8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="radio8"
android:layout_below="@id/radio6"
android:layout_toLeftOf="@id/radio7"
/>
<RadioButton
android:id="@+id/radio9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/radio6"
android:layout_toLeftOf="@id/radio8"
android:text="radio9"
/>
</yanzm.products.customview.RelativeRadioGroup>
<TextView
android:id="@+id/textview"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>



public class MainActivity extends Activity {

private TextView mTextView;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

mTextView = (TextView)findViewById(R.id.textview);

RelativeRadioGroup group = (RelativeRadioGroup)findViewById(R.id.relative);
group.setOnCheckedChangeListener(new RelativeRadioGroup.OnCheckedChangeListener() {

public void onCheckedChanged(RelativeRadioGroup group, int checkedId) {
mTextView.setText("CheckedId : " + checkedId);
}
});
}
}




 

512x512 のデフォアイコン

現在の Android Developer Console では、512x512 の画像をアップロードしないとアプリを公開できません。
それだけならまだいいのですが、512x512 の画像が必須になる前のアプリを非公開にする場合も、これをアプロードしなければいけません。

ということで、デフォルトアイコンに 512x512 の白背景をつけただけの画像を作りました。



ご自由にお使いくださいませ。

ダウンロードはこちらから icon_512.png



 

2011年3月19日土曜日

Android 複雑な文字列を xml で定義する

Android では、文字列を res/values/strings.xml の中に定義します。
*別に strings.xml という名前である必要はありません。stringsForScreen1.xml など任意のファイル名が使えます。

例えば、eclipse で Android プロジェクトを作成した場合、
デフォルトで作られる strings.xml は次のようになっています。


<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hello">Hello World, MainActivity!</string>
<string name="app_name">HelloWorld</string>
</resources>


この文字列にアクセスするには、例えば、


<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello"
/>





TextView textView = (TextView)findViewById(R.id.textview);
textView.setText(R.string.hello);

String hello = getString(R.string.hello);
String hello2 = (String) getText(R.string.hello);


のようにします。

ここまでが基本的な strings.xml の話です。


ここからが本題

文字列の一部をプログラムから指定できるようしてみます。

■ "本を◯◯冊買います" の場合


<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="buybooks">本を%1$d冊買います</string>
</resources>



String buybooks = getString(R.string.buybooks, 4);


%1$d の 1$ は getString() の2番目以降の引数を割り当てるときに、何番目の引数かを指定する番号です。
%d は数字, %s は文字列です。このあたりは Formatter で規定されています。


■ "☓☓を◯◯冊買います" の場合


<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="buyitems">%2$sを%1$d冊買います</string>
</resources>



String buybooks = getString(R.string.buybooks, 4, ”本");

String buybooks2 = String.format(getString(R.string.buybooks), 4 , "本");


1$, 2$ で順番を指定できると、多言語の文法に対応することができます。


<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="buyitems">Buy %1$d %2$s</string>
</resources>



String buybooks = getString(R.string.buybooks, 4, books);

String buybooks2 = String.format(getString(R.string.buybooks), 4 , books);



■ 文法の複数形に対応する - plurals -

さて、英語の場合、次の書き方では問題があります。


<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="buybooks">Buy %1$d books</string>
</resources>


買う本が1冊の場合、文法的には "Buy 1 book" が正しいからです。

このように、単数形と複数形の違いに対応するために plurals が用意されています。

res/values/ の下の任意の xml ファイルで plurals を定義します。

例えば、
res/values/plurals.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="plural_book">
<item quantity="one">Buy one book</item>
<item quantity="other">Buy %1$d books</item>
</plurals>
</resources>


quantity に設定できる値は "zero" | "one" | "two" | "few" | "many" | "other" です。
item の値の形式は string と同じ


String buy1book = getResources().getQuantityString(R.plurals.buybooks, 1);
String buynbooks = getResources().getQuantityString(R.plurals.buybooks, 4, 4);


getQuantityString(int id, int qunatity)getQuantityString(int id, int quantity, Object... fromatArgs) で取得する。

■ HTML markup でスタイリング

xml で定義する文字列には、<b>bold text</b>, <i>italic text</i>, <u>underline text<u> が使えます。
例えば、

<string name="hello">Hello World, <b>MainActivity!</b></string>


ただし、この場合 getText(int resId) での取得はエラーになります。


format + HTML markup の場合

<string name="buybooksbold">本を<b>%1$d</b>冊買います</string>



String buybooksbold = getString(R.string.buybooksbold, 4);

TextView textView = (TextView)findViewById(R.id.textview);
textView.setText(buybooksbold);


と書いても、太字になりません。
format されるときに、文字列のすべてのスタイル情報を取り除いてしまうからです。

そのため、まず、html エスケープな文字列として xml に定義し、format したあとで fromHtml(String) メソッドを使って HTML text をスタイルします。


<string name="buybooksbold">本を&lt;b>%1$d&lt;/b>冊買います</string>



String buybooksbold = getString(R.string.buybooksbold, 4);

CharSequence styledText = Html.fromHtml(buybooksbold);

TextView textView = (TextView)findViewById(R.id.textview);
textView.setText(styledText);


Document には、
”fromHtml(String) はすべての HTML entity に対して適応されてしまいますので、format で挿入する文字列も htmlEncode(String) でエスケープする必要があります。”
と書いてあるんだけど、エスケープしなくてもいけたのでよくわからない。
ちなみにエスケープするなら、こんな感じになります。


<string name="buyitemsbold">&lt;b>%2$s&lt;/b>を&lt;b>%1$d&lt;/b>冊買います</string>



String buyitemsbold = getString(R.string.buyitemsbold, 4, TextUtils.htmlEncode("本"));

CharSequence styledText = Html.fromHtml(buyitemsbold);

TextView textView = (TextView)findViewById(R.id.textview);
textView.setText(styledText);



■ 注意 : xml で定義する文字列に ' が含まれている場合、エスケープが必要

これはOK

<string name="good_example">"This'll work"</string>
<string name="good_example_2">This\'ll also work</string>


これはダメ

<string name="bad_example">This doesn't work</string>
<string name="bad_example_2">XML encodings don't work</string>



String Resources
public final String getString(int resId)
public final CharSequence getText(int resId)



 

2011年3月18日金曜日

Exif

Exif (Exchangeable Image File Format)

・読み方は "イグジフ" or "エグジフ"

1994年に富士フィルムが提唱したデジタルカメラ用の画像ファイルの規格
JEIDA(日本電子工業振興協会)によって標準化され、各社のデジタルカメラに採用されている
最新版はExif 2.2(Exif Print)
TIFF形式で画像についての情報や撮影日時などの付加情報を記録できるほか、縮小画像(サムネイル)を記録することができる
画像形式はRGB無圧縮方式やJPEG方式など複数の形式をサポートしている

by IT用語辞典 e-Words (http://e-words.jp/w/Exif.html)


つまり、画像情報を画像自身に埋め込むための規格
 ・カメラの機種
 ・撮影日時
 ・絞り
 ・画素数
 ・ISO感度
 ・色空間
 ・シャッタースピード
などなど


■ Exifファイルフォーマット参考サイト

Exchangeable image file format - Wikipedia -
けんしのページ - Exifファイルフォーマット -
Exif TAG -
Exif file format -

■ Android の Exif 対応

* Android 2.3

 ・ JPEG ファイルの EXIF メタデータで新しく altitude tag (高度情報)に対応した
 ・ EXIF altitude tag の値を取得するために新しく getAltitude() メソッドが追加された

 http://developer.android.com/sdk/android-2.3.html#api
 の Mixable audio effects 部分


* Android 3.0
 ・photo aperture, ISO, exposure time が新しく ExifInterface のフィールドとして追加された

 http://developer.android.com/sdk/android-3.0.html
 の Media EXIF data 部分


Camera.Parameters

 ・int getJpegThumbnailQuality()
  JPEG 画像の EXIF サムネイル用に設定された画質を返す

 ・void setJpegThumbnailQuality(int quality)
  JPEG 画像の EXIF サムネイル用の画質を設定する
  (quality は 1 - 100, 100 がベスト)

 ・Camera.Size getJpegThumbnailSize()
  JPEG 画像の EXIF サムネイル用のサイズを返す

 ・void setJpegThumbnailSize(int width, int height)
  JPEG 画像の EXIF サムネイル用サイズを設定する
  (width と height に 0 が設定された場合、EXIF はサムネイルを含まない)
  (アプリケーションは画面の向きを考慮する必要がある)

 ・void setRotation(int rotation)
  回転をセット
  引数の rotation はカメラの向きに対する回転角度、0、90、180、270 のみ

 ・void setGpsAltitude(double altitude)
  JPEG EXIF ヘッダに保存される GPS 高度(altitude [m]) を設定する

 ・void setGpsLatitude(double latitude)
  JPEG EXIF ヘッダに保存される GPS 緯度(latitude) を設定する

 ・void setGpsLongitude(double longitude)
  JPEG EXIF ヘッダに保存される GPS 経度(longitude) を設定する

 ・void setGpsProcessingMethod(String processing_method)
  GPS processing method を設定する
  JPEG EXIF ヘッダには32文字まで保存される

 ・void setGpsTimestamp(long timestamp)
  JPEG EXIF ヘッダに保存される GPS timestamp を設定する
   (timestamp は January 1, 1970 からの経過秒数[UTC]).


ExifInterface (Since : API Level 5)

EXIF 用タグフィールド

TAG_APERTURE
  "FNumber"
  F値 : 文字列

TAG_DATETIME
  "DateTime"
  撮影日時 : 文字列 (YYYY:MM:DD hh:mm:ss)

TAG_EXPOSURE_TIME
  "ExposureTime"
  露出時間 : 文字列

TAG_FLASH
  "Flash"
  フラッシュ : int (0 : なし, 1 : あり)

TAG_FOCAL_LENGTH
  "FocalLength"
  レンズ焦点距離 : 適切なタイプ

TAG_GPS_ALTITUDE
  "GPSAltitude"
  TAG_GPS_ALTITUDE_REF を基準としてメートル単位の高度

TAG_GPS_ALTITUDE_REF
  "GPSAltitudeRef"
  高度が海抜の場合 0
  
TAG_GPS_DATESTAMP
  "GPSDateStamp"
  GPS date : 文字列
  
TAG_GPS_LATITUDE
  "GPSLatitude"
  緯度 : "num1/denom1,num2/denom2,num3/denom3" 形式のフォーマット

TAG_GPS_LATITUDE_REF
  "GPSLatitudeRef"
  北緯(N) or 南緯(S)

TAG_GPS_LONGITUDE
  "GPSLongitude"
  経度 : "num1/denom1,num2/denom2,num3/denom3" 形式のフォーマット

TAG_GPS_LONGITUDE_REF
  "GPSLongitudeRef"
  東経(E) or 西経(W)

TAG_GPS_PROCESSING_METHOD
  "GPSProcessingMethod"
  位置特定に使用する GPS processing method の名前

TAG_GPS_TIMESTAMP
  "GPSTimeStamp"
  GPS 時間 (原子時計の時間)

TAG_IMAGE_LENGTH
  "ImageLength"
  画像の長さ : int

TAG_IMAGE_WIDTH
  "ImageWidth"
  画像の幅 : int

TAG_ISO
  "ISOSpeedRatings"
  ISO スピードレート

TAG_MAKE
  "Make"
  画像入力機器のメーカー名

TAG_MODEL
  "Model"
  画像入力機器のモデル名

TAG_ORIENTATION
  "Orientation"
  画像方向 : int
  0 (0x00000000) = ORIENTATION_UNDEFINED
  1 (0x00000001) = ORIENTATION_NORMAL
  2 (0x00000002) = ORIENTATION_FLIP_HORIZONTAL
  3 (0x00000003) = ORIENTATION_ROTATE_180
  4 (0x00000004) = ORIENTATION_FLIP_VERTICAL
  5 (0x00000005) = ORIENTATION_ROTATE_90
  6 (0x00000006) = ORIENTATION_TRANSVERSE
  7 (0x00000007) = ORIENTATION_TRANSPOSE
  8 (0x00000008) = ORIENTATION_ROTATE_270

TAG_WHITE_BALANCE
  "WhiteBalance"
  ホワイトバランス : int
  0 (0x00000000) = WHITEBALANCE_AUTO
  1 (0x00000001) = WHITEBALANCE_MANUAL


・ double getAltitude(double defaultValue)
  Since: API Level 9
  メートル単位での高度を返す
  EXIF タグがない場合、defaultValue を返す


・ String getAttribute (String tag)
  Since: API Level 5
  指定されたタブの値を返す
  JPEGファイルに指定されたタグがない場合は null を返す


・ double getAttributeDouble (String tag, double defaultValue)
  Since: API Level 8
  指定されたタグの double 値を返す
  JPEGファイルに指定されたタグがない場合や、double 値にパース出来ない場合は defaultValue を返す


・ int getAttributeInt (String tag, int defaultValue)
  Since: API Level 5
  指定されたタグの int 値を返す
  JPEGファイルに指定されたタグがない場合や、int 値にパース出来ない場合は defaultValue を返す

・ boolean getLatLong (float[] output)
  Since: API Level 5
  渡された float 配列に緯度経度を保存する
  最初が緯度(latitude)、2番目が軽度(longitude)
  EXIF タグが使用できない場合、false を返す

・ byte[] getThumbnail ()
  Since: API Level 5
  JPEG ファイル内のサムネイルを返す
  サムネイルがない場合は null を返す
  返ってくるデータは JPEG フォーマット
  decodeByteArray(byte[], int, int) を使ってでコード可能

・ boolean hasThumbnail ()
  Since: API Level 5
  JPEG ファイルがサムネイルを持っている場合 true を返す

・ void saveAttributes ()
  Since: API Level 5
  タグデータを JPEG ファイルに保存する
  この処理は 全てのJPEG データをあるファイルから別のファイルにコピーし、古いファイルを削除し新しいファイルをリネームするため、負荷が大きい
  そのため、 setAttribute(String, String) を使って各属性を個別に設定するほうがよい

・ void setAttribute (String tag, String value)
  Since: API Level 5
  指定されたタグの値を設定する


* Gallery アプリ
  [メニュー] - [その他] - [詳細情報]で一部の EXIF 情報が表示される


* Exif情報を見てみる


public class MainActivity extends Activity {

static final String TAG = "ExifSample";

private ExifInterface exifInterface;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

// get file state
String status = Environment.getExternalStorageState();
Log.d(TAG, "status : " + status);

// get file path
//String filename = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/droids.jpg" ; // since API level 8
String filename = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/droids_flash.jpg" ; // since API level 8
Log.d(TAG, "filename : " + filename);

// below API level 8
// String filepath = Environment.getExternalStorageDirectory().toString() + "/DCIM/droids.png";

if (!status.equals(Environment.MEDIA_MOUNTED)) {
// media is not mounted
Log.d(TAG, "media is not mounted");
addTableRow("Error", "media is not mounted");
return;
} else if (!(new File(filename)).exists()) {
// file does not exists
Log.d(TAG, "file does not exists");
addTableRow("Error", "file does not exists");
return;
}

if(!filename.endsWith(".jpg") && !filename.endsWith(".jpeg") && !filename.endsWith(".JPG") && !filename.endsWith(".JPEG")) {
// file is not JPEG
Log.d(TAG, "file is not JPEG format");
addTableRow("Error", "file is not JPEG format");
return;
}

try {
exifInterface = new ExifInterface(filename);
} catch (IOException e) {
e.printStackTrace();
Log.d(TAG, e.getMessage());
addTableRow("Error", e.getMessage());
return;
}

showExifInfo(filename);
ImageView imageView = (ImageView)findViewById(R.id.thumbnail);
imageView.setImageBitmap(getExifThumbnail(filename));
}

private Bitmap getExifThumbnail(){
if(exifInterface != null) {
// get thumbnail
byte[] thumbnail = exifInterface.getThumbnail();
if(thumbnail != null) {
return BitmapFactory.decodeByteArray(thumbnail, 0, thumbnail.length);
}
}
return null;
}

private void showExifInfo(){
if(exifInterface != null) {
// get latitude and longitude
float[] latlong = new float[2];
exifInterface.getLatLong(latlong);

//String aperture = exifInterface.getAttribute (ExifInterface.TAG_APERTURE); // since API Level 11
String datetime = exifInterface.getAttribute (ExifInterface.TAG_DATETIME);
//String exposure = exifInterface.getAttribute (ExifInterface.TAG_EXPOSURE_TIME); // since API Level 11
int flash = exifInterface.getAttributeInt (ExifInterface.TAG_FLASH, 0);
double focalLength = exifInterface.getAttributeDouble (ExifInterface.TAG_FOCAL_LENGTH, 0);
double altitude = exifInterface.getAttributeDouble (ExifInterface.TAG_GPS_ALTITUDE, 0); // since API Level 9
double altitudeRef = exifInterface.getAttributeDouble (ExifInterface.TAG_GPS_ALTITUDE_REF, 0); // since API Level 9
String datestamp = exifInterface.getAttribute (ExifInterface.TAG_GPS_DATESTAMP);
String latitude = exifInterface.getAttribute (ExifInterface.TAG_GPS_LATITUDE);
String latitudeRef = exifInterface.getAttribute (ExifInterface.TAG_GPS_LATITUDE_REF);
String longitude = exifInterface.getAttribute (ExifInterface.TAG_GPS_LONGITUDE);
String longitudeRef = exifInterface.getAttribute (ExifInterface.TAG_GPS_LONGITUDE_REF);
String processing = exifInterface.getAttribute (ExifInterface.TAG_GPS_PROCESSING_METHOD);
String timestamp = exifInterface.getAttribute (ExifInterface.TAG_GPS_TIMESTAMP);
int imageLength = exifInterface.getAttributeInt (ExifInterface.TAG_IMAGE_LENGTH, 0);
int imageWidth = exifInterface.getAttributeInt (ExifInterface.TAG_IMAGE_WIDTH, 0);
//String iso = exifInterface.getAttribute (ExifInterface.TAG_ISO); // since API Level 11
String make = exifInterface.getAttribute (ExifInterface.TAG_MAKE);
String model = exifInterface.getAttribute (ExifInterface.TAG_MODEL);
int orientation = exifInterface.getAttributeInt (ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
int whitebalance = exifInterface.getAttributeInt (ExifInterface.TAG_WHITE_BALANCE, ExifInterface.WHITEBALANCE_AUTO);

String orientationInfo = "";
switch(orientation) {
case ExifInterface.ORIENTATION_UNDEFINED :
orientationInfo = "UNDEFINED";
break;
case ExifInterface.ORIENTATION_NORMAL :
orientationInfo = "NORMAL";
break;
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL :
orientationInfo = "FLIP_HORIZONTAL";
break;
case ExifInterface.ORIENTATION_ROTATE_180 :
orientationInfo = "ROTATE_180";
break;
case ExifInterface.ORIENTATION_FLIP_VERTICAL :
orientationInfo = "FLIP_VERTICAL";
break;
case ExifInterface.ORIENTATION_ROTATE_90 :
orientationInfo = "ROTATE_90";
break;
case ExifInterface.ORIENTATION_TRANSVERSE :
orientationInfo = "TRANSVERSE";
break;
case ExifInterface.ORIENTATION_TRANSPOSE :
orientationInfo = "TRANSPOSE";
break;
case ExifInterface.ORIENTATION_ROTATE_270 :
orientationInfo = "ROTATE_270";
break;
}

addTableRow("latlong", latlong[0] + ", " + latlong[1]);
addTableRow("datetime", datetime);
addTableRow("flash", flash + " (" + (flash == 1 ? "on" : "off") + ")");
addTableRow("focalLength", focalLength + "");
addTableRow("datestamp", datestamp);
addTableRow("altitude", altitude + "");
addTableRow("altitudeRef", altitudeRef + "");
addTableRow("latitude", latitude);
addTableRow("latitudeRef", latitudeRef);
addTableRow("longitude", longitude);
addTableRow("longitudeRef", longitudeRef);
addTableRow("processing", processing);
addTableRow("timestamp", timestamp);
addTableRow("imageLength", imageLength + "");
addTableRow("imageWidth", imageWidth + "");
addTableRow("make", make);
addTableRow("model", model);
addTableRow("orientation", orientation + " (" + orientationInfo + ")");
addTableRow("whitebalance", whitebalance + " (" + (whitebalance == 1 ? "manual" : "auto") + ")");

Log.d(TAG, "latlong : " + latlong[0] + ", " + latlong[1]);
Log.d(TAG, "datetime : " + datetime);
Log.d(TAG, "flash : " + flash + " (" + (flash == 1 ? "on" : "off") + ")");
Log.d(TAG, "focalLength : " + focalLength + "");
Log.d(TAG, "datestamp : " + datestamp);
Log.d(TAG, "altitude : " + altitude);
Log.d(TAG, "altitudeRef : " + altitudeRef);
Log.d(TAG, "latitude : " + latitude);
Log.d(TAG, "latitudeRef : " + latitudeRef);
Log.d(TAG, "longitude : " + longitude);
Log.d(TAG, "longitudeRef : " + longitudeRef);
Log.d(TAG, "processing : " + processing);
Log.d(TAG, "timestamp : " + timestamp);
Log.d(TAG, "imageLength : " + imageLength + "");
Log.d(TAG, "imageWidth : " + imageWidth + "");
Log.d(TAG, "make : " + make);
Log.d(TAG, "model : " + model);
Log.d(TAG, "orientation : " + orientation + " (" + orientationInfo + ")");
Log.d(TAG, "whitebalance : " + whitebalance + " " + (whitebalance == 1 ? "manual" : "auto"));
}
}

private void addTableRow(String name, String value) {
TableLayout tl = (TableLayout)findViewById(R.id.table);

TableRow tbr = new TableRow(this);
TextView tv = new TextView(this);
tv.setText(name);
tv.setPadding(0, 0, 10, 0);
tbr.addView(tv);
TextView tv2 = new TextView(this);
tv2.setText(value);
tbr.addView(tv2);
tl.addView(tbr);
}
}



<?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"
android:padding="10dip"
android:gravity="center_horizontal"
>
<ImageView
android:id="@+id/thumbnail"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:cropToPadding="true"
/>
<TableLayout
android:id="@+id/table"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="10dip"
/>
</LinearLayout>



* その1

 Nexus S のデフォルトカメラで撮影
 フォーカス : マクロ
 露出 : +2
 シーンモード : オート
 位置情報記録 : ON
 フラッシュ : OFF

 縦で撮影





* その2

 Nexus S のデフォルトカメラで撮影
 フォーカス : 無限遠
 露出 : -1
 シーンモード : 横向き
 位置情報記録 : ON
 フラッシュ : ON

 横で撮影




* 値を設定する


public class MainActivity2 extends Activity {

static final String TAG = "ExifSample";

private ExifInterface exifInterface;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main2);

// get file state
String status = Environment.getExternalStorageState();
Log.d(TAG, "status : " + status);

// get file path
//String filename = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/droids.jpg" ; // since API level 8
//String filename = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/droids_flash.jpg" ; // since API level 8

// below API level 8
String filename = Environment.getExternalStorageDirectory().toString() + "/droids.jpg";
//String filename = Environment.getExternalStorageDirectory().toString() + "/droids_flash.jpg";
Log.d(TAG, "filename : " + filename);

if (!status.equals(Environment.MEDIA_MOUNTED)) {
// media is not mounted
Log.d(TAG, "media is not mounted");
setText("media is not mounted");
return;
} else if (!(new File(filename)).exists()) {
// file does not exists
Log.d(TAG, "file does not exists");
setText("file does not exists");
return;
}

if(!filename.endsWith(".jpg") && !filename.endsWith(".jpeg") && !filename.endsWith(".JPG") && !filename.endsWith(".JPEG")) {
// file is not JPEG
Log.d(TAG, "file is not JPEG format");
setText("file is not JPEG format");
return;
}

try {
exifInterface = new ExifInterface(filename);
} catch (IOException e) {
e.printStackTrace();
Log.d(TAG, e.getMessage());
setText(e.getMessage());
return;
}

showExifInfo();
ImageView imageView = (ImageView)findViewById(R.id.thumbnail);
imageView.setImageBitmap(getExifThumbnail());
}

private Bitmap getExifThumbnail(){
if(exifInterface != null) {
// get thumbnail
byte[] thumbnail = exifInterface.getThumbnail();
if(thumbnail != null) {
return BitmapFactory.decodeByteArray(thumbnail, 0, thumbnail.length);
}
}
return null;
}

private void showExifInfo(){
if(exifInterface != null) {
String datetime = exifInterface.getAttribute (ExifInterface.TAG_DATETIME);
setText(datetime);
}
}

private void setText(String text) {
TextView textView = (TextView)findViewById(R.id.textview);
textView.setText(text);
}

public void updateDate(View v) {
EditText editText = (EditText)findViewById(R.id.edittext);
String date = editText.getText().toString();

if(exifInterface != null) {
exifInterface.setAttribute(ExifInterface.TAG_DATETIME, date);
}
showExifInfo();
}
}



<?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"
android:padding="10dip"
android:gravity="center_horizontal"
>
<ImageView
android:id="@+id/thumbnail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:cropToPadding="true"
/>
<TextView
android:id="@+id/textview"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
/>
<EditText
android:id="@+id/edittext"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
<Button
android:id="@+id/button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="10dip"
android:onClick="updateDate"
android:text="Update!"
/>
</LinearLayout>



2011年3月17日木曜日

Android タブをサイドにする

こういうのってなんていうのかな?
横タブ?



結構簡単にできちゃいました。

タブのつまみ部分は TabWidget というクラスです。

TabWidget のソースをみてみればわかりますが、このクラスは LinearLayout を継承しています。
つまり、各つまみは LinearLayout の子要素ということになります。


まずコンストラクタをみると

75 public TabWidget(Context context, AttributeSet attrs, int defStyle) {
76 super(context, attrs);
77
78 TypedArray a =
79 context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TabWidget,
80 defStyle, 0);
81
82 mDrawBottomStrips = a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true);
83 mDividerDrawable = a.getDrawable(R.styleable.TabWidget_divider);
84 mLeftStrip = a.getDrawable(R.styleable.TabWidget_tabStripLeft);
85 mRightStrip = a.getDrawable(R.styleable.TabWidget_tabStripRight);
86
87 a.recycle();
88
89 initTabWidget();
90 }


initTabWdiget() があやしい。。。


111 private void initTabWidget() {
112 setOrientation(LinearLayout.HORIZONTAL);
113 mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER;
114
115 final Context context = mContext;
116 final Resources resources = context.getResources();
117
118 if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
119 // Donut apps get old color scheme
120 if (mLeftStrip == null) {
121 mLeftStrip = resources.getDrawable(
122 com.android.internal.R.drawable.tab_bottom_left_v4);
123 }
124 if (mRightStrip == null) {
125 mRightStrip = resources.getDrawable(
126 com.android.internal.R.drawable.tab_bottom_right_v4);
127 }
128 } else {
129 // Use modern color scheme for Eclair and beyond
130 if (mLeftStrip == null) {
131 mLeftStrip = resources.getDrawable(
132 com.android.internal.R.drawable.tab_bottom_left);
133 }
134 if (mRightStrip == null) {
135 mRightStrip = resources.getDrawable(
136 com.android.internal.R.drawable.tab_bottom_right);
137 }
138 }
139
140 // Deal with focus, as we don't want the focus to go by default
141 // to a tab other than the current tab
142 setFocusable(true);
143 setOnFocusChangeListener(this);
144 }



112 setOrientation(LinearLayout.HORIZONTAL);


でがっつり horizontal 指定してますね。

なので、TabWdiget を継承した独自クラスを作って、そのコンストラクタで super() を読んだ後に

setOrientation(LinearLayout.VERTICAL);

を呼んであげます。


これだけではダメなんです。



この TabWidget (つまりレイアウト的には LinearLayout) に子要素を追加する部分もカスタマイズする必要があります。

385 @Override
386 public void addView(View child) {
387 if (child.getLayoutParams() == null) {
388 final LinearLayout.LayoutParams lp = new LayoutParams(
389 0,
390 ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
391 lp.setMargins(0, 0, 0, 0);
392 child.setLayoutParams(lp);
393 }
394
395 // Ensure you can navigate to the tab with the keyboard, and you can touch it
396 child.setFocusable(true);
397 child.setClickable(true);
398
399 // If we have dividers between the tabs and we already have at least one
400 // tab, then add a divider before adding the next tab.
401 if (mDividerDrawable != null && getTabCount() > 0) {
402 ImageView divider = new ImageView(mContext);
403 final LinearLayout.LayoutParams lp = new LayoutParams(
404 mDividerDrawable.getIntrinsicWidth(),
405 LayoutParams.MATCH_PARENT);
406 lp.setMargins(0, 0, 0, 0);
407 divider.setLayoutParams(lp);
408 divider.setBackgroundDrawable(mDividerDrawable);
409 super.addView(divider);
410 }
411 super.addView(child);
412
413 // TODO: detect this via geometry with a tabwidget listener rather
414 // than potentially interfere with the view's listener
415 child.setOnClickListener(new TabClickListener(getTabCount() - 1));
416 child.setOnFocusChangeListener(this);
417 }


LinearLayout に子要素を追加するメソッドの addView で


387 if (child.getLayoutParams() == null) {
388 final LinearLayout.LayoutParams lp = new LayoutParams(
389 0,
390 ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
391 lp.setMargins(0, 0, 0, 0);
392 child.setLayoutParams(lp);
393 }


と、width に 0, height に ViewGroup.LayoutParams.MATCH_PARENT が指定されています。
これを反対にします。

ということで完成コードはこんな感じ。


public class SideTabWidget extends TabWidget {

public SideTabWidget(Context context) {
this(context, null);
}

public SideTabWidget(Context context, AttributeSet attrs) {
super(context, attrs);

setOrientation(LinearLayout.VERTICAL);
}

@Override
public void addView(View child) {
if (child.getLayoutParams() == null) {
final LinearLayout.LayoutParams lp = new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f);
lp.setMargins(0, 0, 0, 0);
child.setLayoutParams(lp);
}
super.addView(child);
}
}



<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/tabhost"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:paddingBottom="10dip" >
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="0dip"
android:layout_height="fill_parent"
android:layout_weight="1"
android:background="@color/basic_white2"
android:padding="10dip"
/>
<layoutbook.example.sidetab.SideTabWidget
android:orientation="vertical"
android:id="@android:id/tabs"
android:layout_width="60dip"
android:layout_height="fill_parent" />
</LinearLayout>
</TabHost>

2011年3月15日火曜日

AlarmManager

AlarmManager

・システムの AlarmService を使うためのクラス
・アプリケーションを将来のあるポイントで起動するようスケジュールできる

・Alarm が開始すると、システムによって Intent が broadcast される
・この Intent に起動したいアプリケーションを登録しておく
・これにより、現状で起動していないアプリケーションが自動で起動する

・登録された Alarm はデバイスがスリープ状態の間保持される(オプションでデバイスがオフの場合に wake up させることができる)が、再起動したり電源を切ると登録はクリアされる


・Alarm Manager は alarm receiver の onReceive() メソッドが実行されているのと同じだけ CPU を hold する
・これはブロードキャストの処理が終了するまで電話がスリープ状態にならないことを保証する

・一度 onReceive() を返したら、Alarm Manager はこの wake lock を離す
・これは、onReceive() メソッドが完了してすぐにスリープ状態になる場合があることを意味する
・もし alarm receiver が Context.startService() を呼ぶ場合、service を起動するリクエストが完了するまえに電話がスリープ状態になる可能性がある
・これを防止するために、BroadcastReceiver と Service は、service が利用可能になるまで電話が動作し続けていることを確認するために、別々の wake lock policy を実装する必要がある


注意:
・Alarm Manager は現在アプリケーションが走っていなくても、特定の時刻にアプリケーションコードを実行したい場合を対象としている
・一般的なタイミング操作(タイミング計測、時間計測など) は Handler を使ったほうがより簡単で効果的


このクラスを直接インスタンス化してはいけない
Context.getSystemService(Context.ALARM_SERVICE) を通して取得する

-----------------------------

getSystemService(Context.ALARM_SERVICE)
で AlarmManager のインスタンスを取得

このインスタンスに対して

・set(int type, log triggerAtTime, PendingIntent operation)
・setInexactRepeating(int type, long triggerAtTime, long interval, PendingIntent operation)
・setRepeating(int type, long triggerAtTime, long interval, PendingIntent operation)

で alarm を登録する


まずは最小構成で。



・MainActivity : alarm をセット
・AlarmReceiver : alarm を受信
  AndroidManifest.xml に <receiver> で登録


public class MainActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

Intent intent = new Intent(MainActivity.this, AlarmReceiver.class);
PendingIntent sender = PendingIntent.getBroadcast(MainActivity.this, 0, intent, 0);

Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.add(Calendar.SECOND, 20);

AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
// one shot
alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), sender);

Toast.makeText(MainActivity.this, "Start Alarm!", Toast.LENGTH_SHORT).show();
}
}



public class AlarmReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Alarm Received!", Toast.LENGTH_SHORT).show();
Log.d("AlarmReceiver", "Alarm Received! : " + intent.getIntExtra(Intent.EXTRA_ALARM_COUNT, 0));
}
}


AndroidManifest.xml
(一部の属性省略)

<manifest>
<application>
<activity android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<receiver android:name=".AlarmReceiver" android:process=":remote" />

</application>
</manifest>


アプリを起動して 20秒後に alarm が起動して Toast が表示される
AlarmManager.RTC_WAKEUP を指定しているので、デバイスがスリープ状態でも LogCat に "Alarm Received! : ..." が出力される

repeat の場合

// repeat
long firstTime = SystemClock.elapsedRealtime();
firstTime += 15*1000;
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, firstTime, 15*1000, sender);


おおまかな repeat の場合

// repeat
Calendar cal = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
cal.roll(Calendar.HOUR, true);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
Log.d("Calendar", cal.toString());

alarmManager.setRepeating(AlarmManager.RTC, cal.getTimeInMillis(), AlarmManager.INTERVAL_HOUR, sender);


など

repeat のキャンセルは

// cancel
alarmManager.cancel(sender);



スリープ状態をやめる



最初に書いたように、Alarm Manager を使うときの注意点として、
"onReceive() を返したあとは、すぐにスリープ状態になることがある" ということ

これを回避するために、onReceive() 内で、別の wake lock policy を使ってスリープ状態をめるようにしてみる


public class AlarmReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Alarm Received!", Toast.LENGTH_SHORT).show();
Log.d("AlarmReceiver", "Alarm Received! : " + intent.getIntExtra(Intent.EXTRA_ALARM_COUNT, 0));

// スクリーンオン
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "My Tag");
wl.acquire(20000);
//wl.acquire();
}
}


acquire() でもいいが、wakelock は電池を食うので、timeout を設定する acquire(long timeout) を使うべき


public class AutoStartActivity extends Activity {

....

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

keyguard = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
keylock = keyguard.newKeyguardLock("keyLock");
}

private KeyguardManager keyguard;
private KeyguardLock keylock;

@Override
public void onResume() {
super.onResume();
// キーロックオフ
keylock.disableKeyguard();
Log.d("MainActivity", "keylock off");
}

@Override
public void onPause() {
super.onPause();
// キーロックオン
keylock.reenableKeyguard();
Log.d("MainActivity", "keylock on");
}
}


wakelock を使うには android.permission.WAKE_LOCK
keylock を使うには android.permission.DISABLE_KEYGUARD
を AndroidManifest.xml の <uses-permission> タグで追加する必要がある


AlarmManager の method と field



* set(int type, log triggerAtTime, PendingIntent operation)

・1回だけの alarm を登録する
・すでに同じ IntentSender に対してスケジュールされている場合、最初のがキャンセルされる
・過去の時間を指定した場合、ただちに alarm が起動する
・Alarm intent は、Intent.EXTRA_ALARM_COUNT というタイプの extra data を持っている
・これは、この intent broadcast に過去の alarm event が何個蓄積されているかを示す
・定期的な alarm がスリープ状態によって起動されてなかった場合、この値は 1以上になる可能性がある


* setRepeating(int type, long triggerAtTime, interval, PendingIntent)

・一定間隔で繰り返す定期的 alarm
・cancel(PendingIntent) で明示的に除かれるまで繰り返しは継続される
・設定時間が過去の場合、alarm は直ちに実装され、alarm count は繰り返し間隔に比べてどれだけトリガー時間が遠いかに依存する
・_WAKEUP ではない alarm type などでシステムのスリーブ状態で遅延された alarm は繰り返しはスキップされ、可能になった段階ですぐに実行される
・その後はもともとのスケージュールで alarm が実行される
・over time はドリフトされない
・over time をドリフトしたい場合は、one-time alarm を使って、その alarm が呼ばれたときに次の alarm をセットするようにする


* setInexactRepeating(int type, long triggerAtTime, long interval, PendingIntent operation)

・おおまかなトリガー時間が必要な定期的 alarm のスケジュール
・例えば、毎時間繰り返したいが、毎時0分ぴったりでなくていい場合など
・それ以外は setRepeating と同じ


type : ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP, RTC, RTC_WAKEUP のどれか

triggerAtTime : type 設定した時計での alarm が起動する時間

interval : 繰り返し間隔、INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_HALF_DAY, or INTERVAL_DAY のどれかが指定されている場合、余計な wakeup を減らすために、他の alarm と phase-aligned される

operation : alarm が起動したときに実行する処理、一般的には IntentSender.getBroadcast() で取得する


* interval

 INTERVAL_FIFTEEN_MINUTES
   Constant Value: 900000 (0x00000000000dbba0)
   15分

 INTERVAL_HALF_HOUR
   Constant Value: 1800000 (0x00000000001b7740)
   30分

 INTERVAL_HOUR
   Constant Value: 3600000 (0x000000000036ee80)
   1時間

 INTERVAL_HALF_DAY
   Constant Value: 43200000 (0x0000000002932e00)
   半日

 INTERVAL_DAY
   Constant Value: 86400000 (0x0000000005265c00)
   一日


* type

 ELAPSED_REALTIME
   Constant Value: 3 (0x00000003)
   SystemClock.elapsedRealtime() での時間 (= ブートしてからの時間、スリープ状態を含む)
   alarm はデバイスを起こさない
   デバイスがスリープ状態の場合に alarm が動作しても、次にデバイスが起きるまで機能しない

 ELAPSED_REALTIME_WAKEUP
   Constant Value: 2 (0x00000002)
   SystemClock.elapsedRealtime() での時間 (= ブートしてからの時間、スリープ状態を含む)
   デバイスがスリープ状態の場合に alarm が動作した場合、alarm はデバイスを起こす
   
 RTC
   Constant Value: 1 (0x00000001)
   System.currentTimeMillis() での時間 (= UTC での wall clock time)
   alarm はデバイスを起こさない
   デバイスがスリープ状態の場合に alarm が動作しても、次にデバイスが起きるまで機能しない

 RTC_WAKEUP
   Constant Value: 0 (0x00000000)
   System.currentTimeMillis() での時間 (= UTC での wall clock time)
   デバイスがスリープ状態の場合に alarm が動作した場合、alarm はデバイスを起こす


 

2011年3月14日月曜日

Corona を使ってみた。

Corona SDK は AnscaMobile 社 (http://www.anscamobile.com/) が提供しているライブラリ

・iPhone/iPad/Android 向けのアプリケーションを作成できる
・Lua 言語で記述 (AS2 に似てるらしい)
・開発環境は Mac OS X か Microsoft Windows
・UI の API は OpenGL ベース

# 残念ながら linux は未対応です。

・自分の端末に入れるだけならばトライアル(無料・無期限)で使える
・商用としてiPhone/Androidマーケットに登録するアプリをビルドするときに商用ライセンス(一年)が発生する


■ 参考サイト
・日本 Android の会定例(2011年3月)の木村君の講演
 ・Ustream 録画
 ・スライド

・面白法人カヤックのFlashチームのブログ
 ・Corona で スマートフォン アプリ開発をしよう。インストール編 | エントリー | _level0.KAYAC | flash ActionScript blog -
 ・Corona で スマートフォン アプリ開発をしよう。HelloWorld 編 | エントリー | _level0.KAYAC | flash ActionScript blog -
 ・Corona で スマートフォン アプリ開発をしよう。開発編 | エントリー | _level0.KAYAC | flash ActionScript blog -


■ 事前準備

* Android の apk を出力するには、JDK をインストールして PATH を通しておく


■ Corona SDK のインストール

* http://www.anscamobile.com/corona/ の Download からダウンロード
* メールとパスワード設定
* registering メールが来るので、validate リンクをクリック

* Mac or Windows の Corona SDK をダウンロードし、インストーラを起動


■ Corona Simulator

* Corona SDK をインストールすると、スタートメニューに Corona SDK というのができるので、この中の Corona Simulator を起動

* [Window] - [View As] で端末のスキンを変えられる (Droid, NexusOne, myTouch, GalaxyTab)


■ 端末にインストールできる実行ファイル(Android だと apk)の作成

* Corona Simulator で [File] - [Build...] から Build for Android ダイアログを開く
* Application Name, Version, Package, Target OS Compativility, Save to Folder などを設定
* 出力先に apk ファイルが作成される


■ Hello Corona

画像を表示

local sky = display.newImage("sky.jpg")




local sky = display.newImage("sky.jpg")
local cloud = display.newImage("cloud2.png")
cloud.x = 0
cloud.y = 0




x, y は画像の中心位置っぽい


local sky = display.newImage("sky.jpg")
local cloud = display.newImage("cloud2.png")

-- display 480x800

cloud.x = 240
cloud.y = 600




local sky = display.newImage("sky.jpg")
local cloud = display.newImage("cloud2.png")

-- display 480x800

cloud.x = 240
cloud.y = 600

local star = display.newImage("star.png")
star.x = 180
star.y = 200
star.rotation = 20




-- 物理エンジンを使う
local physics = require("physics")
physics.start()

local sky = display.newImage("sky.jpg")
local cloud = display.newImage("cloud2.png")

-- display 480x800

cloud.x = 240
cloud.y = 600

-- 物理エンジンの対象に追加
-- friction : 摩擦
physics.addBody(cloud, {friction = 0.5})
-- cloud を背景に固定
cloud.bodyType = "static"

local star = display.newImage("star.png")
star.x = 180
star.y = 200
star.rotation = 20

-- 物理エンジンの対象に追加
-- density : 密度
-- bounce : 跳ね返り係数
physics.addBody(star, {density=2.0, friction=0.5, bounce=0.5})





-- 物理エンジンを使う
local physics = require("physics")
physics.start()

local sky = display.newImage("sky.jpg")
local cloud = display.newImage("cloud2.png")

-- display 480x800

cloud.x = 240
cloud.y = 600

-- 物理エンジンの対象に追加
-- friction : 摩擦
physics.addBody(cloud, {friction = 0.5})
-- cloud を背景に固定
cloud.bodyType = "static"

-- 星の生成を関数にする
-- math.random(hoge) : 0 ~ hoge - 1
local function createStar()
local star = display.newImage("star.png")
star.x = math.random(480)
star.y = -100
star.rotation = 20

-- 物理エンジンの対象に追加
-- density : 密度
-- bounce : 跳ね返り係数
physics.addBody(star, {density=2.0, friction=0.5, bounce=0.5})
end

-- 500ms 毎 createStart() を 10 回呼び出す
timer.performWithDelay(500, createStar, 10)





■ Hello Corona2

テキストを表示

local sky = display.newImage("sky.jpg")
local hello = display.newText("hello", 0, 0)
local hello2 = display.newText("hello world", 50, 50, native.systemFont, 40)
local hello3 = display.newText("hello corona", 0, 0, native.systemFont, 80)
hello3.x = display.contentWidth / 2
hello3.y = display.contentHeight / 2
hello3:setTextColor(0, 0, 128)





■ Eclipse プラグイン

LuaForge: LuaEclipse: Project Filelist

luaeclipse-1.2.0.zip をダウンロードし、解凍すると features, plugins フォルダができるので、中のファイルを eclipse.exe と同じディレクトリの features, plugins フォルダ内にコピーする。

Eclipse を起動して、
[Help] - [Install New Software...]

Add ボタンをクリックして以下を追加

LuaEclipse Update Site

LuaEclipse 1.2 にチェックマークつけてインストール

# 真ん中を除かないとエラーが消えなかった




■ Lua インタプリタ

Lua Binaries Download

* Windows の場合

lua5_1_4_Win32_bin.zip
をダウンロードして解凍

Eclipse を起動して
[Window] - [Preferences] - [Lua] - [Installed Interpreters]
を開いて Add ボタンをクリックして、Path に解凍したフォルダ内の lua5.1.exe を指定する
チェックマークにチェックを入れて OK を押す




■ Eclipse で lua

[File] - [New] - [Project...]

Lua から New Lua Project を選択して Next
Project name: を入力して Finish




■ PhysicsEditor (http://www.physicseditor.de/)

* 透過 png からアウトラインを抽出して .lua ファイルにエクスポートしてくれるツール


■ Hello Corona with PhysicsEditor


local screenW, screenH = display.contentWidth, display.contentHeight

local sky = display.newImage("sky.jpg")

local physics = require("physics")
physics.start()

-- display.setStatusBar( display.HiddenStatusBar )

local droid = display.newImage("androids2.png")
droid.x = screenW / 2
droid.y = 720
physics.addBody(droid, "static", {friction=0.5, bounce=0.3})

local scaleFactor = 1.0
local physicsItem1 = (require "cupcake").physicsData(scaleFactor)
local physicsItem2 = (require "donut").physicsData(scaleFactor)
local physicsItem3 = (require "eclair").physicsData(scaleFactor)

local cupcake = display.newImage("cupcake.png")
cupcake.x = screenW / 2 - 100
local donut = display.newImage("donut.png")
donut.x = screenW / 2
local eclair = display.newImage("eclair.png")
eclair.x = screenW / 2 + 100

physics.addBody( cupcake, physicsItem1:get("cupcake") )
physics.addBody( donut, physicsItem2:get("donut") )
physics.addBody( eclair, physicsItem3:get("eclair") )
--cupcake.x = screenW / 2;
--cupcake.y = cupcake.height / 2;

local function addItemTouch( event )
local s = display.getCurrentStage()
if "began" == event.phase then
local scaleFactor = 1.0
local id = math.random(3);
local physicsData;
local item;
if id == 1 then
physicsData = (require "cupcake").physicsData(scaleFactor)
item = display.newImage("cupcake.png")
physics.addBody( item, physicsData:get("cupcake") )
elseif id == 2 then
physicsData = (require "donut").physicsData(scaleFactor)
item = display.newImage("donut.png")
physics.addBody( item, physicsData:get("donut") )
else
physicsData = (require "eclair").physicsData(scaleFactor)
item = display.newImage("eclair.png")
physics.addBody( item, physicsData:get("eclair") )
end

item.x = event.x
item.y = event.y
item.isFocus = true
item.id = event.id
end
end

local xyText = display.newText( "Tap Screen!", 0, 0, native.systemFont, 40 )
xyText.x = screenW / 2
xyText.y = 40
xyText:setTextColor(255, 255, 255)

system.activate( "multitouch" )
Runtime:addEventListener( "touch", addItemTouch )




遊んでみたい方はこちらから。(Android 2.2 以上 : スペック低いと厳しいかも)
DroidFall.apk


■ SpriteDeck (http://www.spritedeck.com)

* Visual Game Designer for Corona SDK
* Corona で使える画面を作成できるツール
* 画面を作成してエクスポートすると **.lua と _**.lua が作成される
* _**.lua : アプリケーション画面
* **.lua : アプリケーションロジック
* ライセンスを購入しないとファイルが保存できない(トライアルあり)


■ Hello Corona with SpriteDeck

* SpriteDeck を使って作ってみた。

* 感想
 * いまいちなところ
  * Hello Corona! って表示されるだけのアプリの容量が 1.8M
  * SpriteDeck では、円、矩形、テキスト、ボタン くらい
  * グラデーションなどは指定できない

 * いいところ
  * 座標は数値指定で細かく調整できる
  * サイズを変えたり、配置を換えたりが簡単
  * テキストの Font は変えられない
  * Physics Body, Body Type, Sensor などが GUI から設定できる
  * Density, Bounce, Friction も GUI から設定できる

 * その他
  * 無料版だとプロジェクトが保存できないので、実質あまり使えない




■ Lua 言語

* Lua 5.1 リファレンスマニュアル
* ドキュメント
* デモ

* Lua アプリケーションコードは main.lua から実行される
* 追加のコードは require 関数でファイル名を指定

* 変数名を数字からはじめることはできない
* 大文字小文字を区別する
* 1.2e-2, 0xff などの表記が可能

* 変数の型を宣言する必要はない
* 基本的な型 : nil, boolean, number, string, function, table

* table : 連想配列、nil 以外の任意の値をインデックスとして使用可能
  * フィールドにメソッドを保持することが可能
  * t = {} のように記述して table 作成

* コメントは -- (ハイフン2つ)
* ブロックをコメントアウトするときは [[ hoge ]]

-- ここはコメント

--[[
ここはコメント
--]]

---[[
ここはコメントではない
--]]


* if文

if hoge == true then
fuga()
end

* 1行に2文書くときは ; を付ける?1行ならいらない
* インクリメント (++) は使えない hoge += 1 のように書く
* 文字列連結は + ではなく ..

* オブジェクトの主なプロパティ
 * object.x
 * object.y
 * object.width
 * object.height
 * object.rotation
   回転角度
   360 以上もあり

 * object.alpha
   透過率 : 0 - 1.0
   1.0 を超えるとエラーになる

 * object.isVisible
 * object.xScale
 * object.yScale
 * object.contentWidth
 * object.contentHeight






 

2011年3月8日火曜日

Geo Tech Talk に行ってきました。

3/8 に TokyoGTUG 主催で行われた Geo Teck Talk に行ってきました。

Manomarks さんのスライド
TokyoGTUG Geo Techtalk togetter

■ Fusion Table について
・SQLライクなクエリ

layer = new google.maps.FusionTablesLayer(someid, {
query: "SELECT address FROM someid WHERE ridership > 5000"}
);
layer.setMap(map);

 ・近くのカフェ10個
 ・10km以内のガソリンスタンド
 ・ある領域内の店舗
 など
 # 思いっきり SQL だね。

・Read-only なら認証不要(一般に公開される)
・Write は認証が必要
 ・クライアントライブラリを使う
 ・OpenDataKit や App Inventor を使ってデータコレクションする


・クリックできるラスター画像を置ける
・100,000 アイテムをレンダリングできる
・簡単
・クラウド上にデータを配置
・バックエンドAPI


Styled Maps



Shape to Fusion
Google Fusion Tables に shapefile をインポートするためのサイト

AWUILA Shoes
・MAP をカスタマイズした例
・色をモノクロにしてサイトの雰囲気と合わせてある

Athens Taxi Calculator
・右上で Night モードにすると、地図が夜っぽ感じになる

Word Only Map
・文字だけ残した地図
・拡大していくとなんとなく形が見えてきて面白い

Google Maps API Styled Map Wizard
・マップをカスタマイズするウィザード
・いろいろできて便利


Custom Street View



Street View Zombie Apocalypse
・ゾンビが追いかけてくるデモ
・ゾンビが小さくてあんまり怖くない
# Manomarks さんに聞いたら、Zoom の度合いに合わせてゾンビの大きさを変えることは可能
# なんで開発者がゾンビを小さくしているのかは僕にもわからないとのこと

Digitas Happy New Year
・建物の中に入れる StreetView
・プロモーション
・パノラマ画像は自分たちで用意
# 自分でもやってみたいけど、パノラマ画像を用意するのが大変?


Earth API Demos



Drive the A-Team Van
・Google Earth の中をバンでドライブ
・クラッシュしたら映像がでるらしい(デモではうまく出なかった)

3D Las Vegas
・ラスベガスを 3D で紹介
・ツアーモードにすると自動で視点が移動


 

2011年3月3日木曜日

Libraroid Advanced Plugin Beta

Libraroid に機能を追加するためのプラグインをリリースしました。

Libraroid Advanced Plugin Beta - Android Market -




■ 機能

 現状では、以下の機能が Libraroid に追加されます

  ・ Libraroid で白スキンが使える
  ・ 保存しておいた複数アカウントを Libraroid で予約するときに使える
  ・ Libraroid の MyBookList を CSV 形式で SD カードに保存できる



* メイン画面



* スキンを黒と白から選べます


 白を選んで、Libraroid を再起動すると、こんな感じ



* アカウントを5個まで登録しておけます


 Plugin をインストールすると、Libraroid の予約ダイアログに切り替えボタンがつくので、これをタップして上記で設定したアカウントから選択できます



* Libraroid の MyBookList を CSV 形式で SD カードに保存します
保存先は <:sd card directory>/LibraroidPlugin/LibraroidMyBookList.csv です
すでにファイルがある場合は上書きされます

2011年3月2日水曜日

Android Emulator の起動サイズを変える

Honeycomb でかいよね!
そのまま起動すると大変!

Honeycomb に入っている Skin は WXGA (1280x800) だけです。
このまま起動すると 1280x800 なので、私のノパソの画面には
入りません。



Android SDK and AVD Manager の Virtual devices で
”Start...”をクリックすると、Launch Options の
ダイアログになります。

ここで、Scale display to real size にチェックを入れて
各値を設定することで、エミュレータの起動サイズを
変更することができます。



設定値には

 ・ Screen Size (in) : インチ
 ・ Monitor dpi
 ・ Scale

があり、

Scale = Screen Size * Monitor dpi * なにか ( 〜 0.0001 * 6.62 くらい)

になっているよう、、、

Monitor dpi の横の ? を押すと、Screen Size と Resolution を設定
するようになっている。



Monitor dpi = なにか / Screen Size

みたい、、、

# Resolution を変えても、Monitor dpi は変わらなかった


結局よくわからないけど、Skin のサイズを Scale 倍して
Screen Size (in) のサイズになるってことっぽい。

# たぶんね。

# 3 inch の Honeycomb はさすがに厳しかった
# 9 inch くらいがよさげ?
# Resolution と Monitor サイズを正しくしないと、Honeycomb の背景とか一部描画されなかった
# 画面が小さいからといってエミュレータの動作がサクサクになるわけではありません。。。