Android入門―本気で使える電卓アプリの開発―5

新しい状態(State)を追加する

まだ電卓にはエラー処理が実装されていません。例えば1÷0=と入力すると「Infinity」と表示されてしまいます。桁あふれのエラー処理もないため、桁あふれしたことが分かりません。
また、エラーが発生した場合にはAC(All Clear)ボタン以外受け付けないように制御したいとおもいます。そのためには新しい状態(State)を追加します。
新しい状態(State)を追加するのは簡単です。Stateインタフェースを実装したクラスを追加します。そしてAC(All Clear)ボタンが押されたときにエラーをクリアするように実装します。

[java]
public class ErrorState implements State {
    private static ErrorState singleton = new ErrorState();
    private ErrorState() { // コンストラクタはprivate
    }
    public static State getInstance() { // 唯一のインスタンスを得る
        return singleton;
    }
    @Override
    public void onInputNumber(Context context, Number num) {
    }
    @Override
    public void onInputOperation(Context context, Operation op) {
    }
    @Override
    public void onInputEquale(Context context) {
    }
    @Override
    public void onInputClear(Context context) {
    }
    @Override
    public void onInputAllClear(Context context) {
        context.clearA();
        context.clearB();
        context.clearDisplay();
        context.clearError();
        context.changeState(NumberAState.getInstance());
    }
}
[/java]

あとは、演算時のエラーを検出したらExceptionを継承したCalcExceptionをスローするように変更します。

Calc.java
[java]
    public double doOperation() throws CalcException{
        double result = op.eval(A, B);
        // Doubleの場合、ゼロ割でエラーが発生しないので注意が必要。
        if (Double.isInfinite(result) || Double.isNaN(result)) {
            throw new CalcException();
        }
        showDisplay(result);
        // 演算結果がディスプレイからはみ出ないかチェック
        if (disp.isOverflow(result)) {
            throw new CalcException();
        }
        return result;
    }
[/java]

上記変更により、CalcクラスのdoOperation()メソッドを呼び出している箇所がエラーになりますので、try-catch文でErrorStateへ遷移するように変更します。

NumberBState.java
[java]
    public void onInputOperation(Context context, Operation op) {
        try {
            context.saveDisplayNumberToB();
            context.doOperation();
            context.setOp(op);
            context.saveDisplayNumberToA();
            context.changeState(OperationState.getInstance());
        } catch (CalcException e) {
            context.setError();
            context.changeState(ErrorState.getInstance());
        }
    }
[/java]

そのほかのdoOperation()メソッドを呼び出している箇所も同様に修正します。
修正は以上です。
実際に動かしてみましょう。例えばゼロ割である1÷0=や、桁溢れする9999999×9999999=などを計算してみてください。ErrorStateに遷移するのでACボタン以外反応しなくなることが分かると思います。
どうでしょうか。簡単にエラー状態が追加できることが分かるかと思います。
電卓アプリ内部ではErrorStateによりエラー状態であることがわかりますが、Androidアプリ上ではエラーになったことが分かりません。そこでAndroidの通知機能「トースト」を使用してエラーを通知するように機能を追加します。

トースト通知の追加

トーストとは前面に表示される通知のことです。画面の下方に表示され、自動でフェードイン、フェードアウトされます。トースト表示中も現在の画面が見えたままなので、アプリケーションの操作も可能です。
トーストの表示自体は大変簡単です。Calcクラスに追加しましょう。トースト表示用にContextを保持できるようにメンバ変数parentを追加します。setDispメソッドでContextを受け取りparentに設定することにします。

[java]
    // Toast表示用にcontextを持つ。
    protected android.content.Context parent;
    public void setDisp(TextView txt,android.content.Context parent){
        this.disp = new StringDisplay(txt);
        this.parent = parent;
    }
[/java]

MainActivityクラスのsetDisp呼び出しも修正しておきます。

[java]
calc.setDisp(txtDisp,this);
[/java]

トーストを表示するにはToast クラスを使用します。

[java]
    public void setError() {
        if (parent != null){
            Toast.makeText(parent, "ERROR", Toast.LENGTH_LONG).show();
        }
        disp.setError();
    }
[/java]

実行してみましょう。演算エラーになる操作を行うとERRORの文字が表示されます。

AndroidのボタンデザインをXMLで変更する

初期状態でのAndroidのボタンは味気ないので、ボタンのデザインを変更してみましょう。
ボタンのデザインに画像を使用する事も出来ますが、XMLでシェイプを指定して気軽にデザイン変更をすることが出来ます。まずは、ボタンのデザインを用意します。
ボタンには、ボタンの押下の有無、フォーカスの有無により合計4つの状態がありますので、それぞれの状態に対してシェイプを指定します。
数値のボタン用に黒いデザインを作ってみましょう。ソースは次のようになります。

button_black.xml
[xml]
<!-- ボタンが押されている --> <!-- フォーカスされていない --> <item android:state_pressed="true" android:state_focused="false"> <layer-list> <item > <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:startColor="@color/push_start" android:centerColor="@color/push_center" android:endColor="@color/push_end" android:centerY="0.2" android:angle="-90"/> <padding android:left="1dip" android:top="dip" android:right="1dip" android:bottom="1dip" /> <corners android:radius="15dip" /> <stroke android:width="1dip" android:color="#999999" /> </shape> </item> </layer-list> </item> <!-- ボタンが押されていない --> <!-- フォーカスされていない --> <item android:state_pressed="false" android:state_focused="false"> <layer-list> <item > <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:startColor="@color/black_start" android:centerColor="@color/black_center" android:endColor="@color/black_end" android:centerY="0.2" android:angle="-90"/> <padding android:left="1dip" android:top="1dip" android:right="1dip" android:bottom="1dip" /> <corners android:radius="15dip" /> <stroke android:width="1dip" android:color="@color/black_center" /> </shape> </item> </layer-list> </item> <!-- ボタンが押されている --> <!-- フォーカスされた --> <item android:state_pressed="true" android:state_focused="true"> <layer-list> <item > <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:startColor="@color/push_start" android:centerColor="@color/push_center" android:endColor="@color/push_end" android:centerY="0.2" android:angle="-90"/> <padding android:left="1dip" android:top="1dip" android:right="1dip" android:bottom="1dip" /> <corners android:radius="15dip" /> <stroke android:width="1dip" android:color="#FF6600" /> </shape> </item> </layer-list> </item> <!-- ボタンが押されていない --> <!-- フォーカスされた --> <item android:state_pressed="false" android:state_focused="true"> <layer-list> <item > <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:startColor="@color/black_start" android:centerColor="@color/black_center" android:endColor="@color/black_end" android:centerY="0.2" android:angle="-90"/> <padding android:left="1dip" android:top="1dip" android:right="1dip" android:bottom="1dip" /> <corners android:radius="15dip" /> <stroke android:width="1dip" android:color="#FF6600" /> </shape> </item> </layer-list> </item> </selector>
[/xml]

このファイルを「/res/drawable-nodip」に配置します。
他にも演算操作のボタン用にグレーのデザイン、クリア、オールクリアのボタン用に赤のデザインを用意します。
android:startColor="@color/black_start"で色を定義しています。実際の色の指定は「/res/values/color.xml」の中で設定します。メニューより[ファイル]-[新規]-[Android XML File]を開き、color.xmlを作成してください。

  • Project:MyCalc
  • File:color.xml
  • リソースタイプ:Values


color.xmlに色の定義を追加します。

color.xml
[xml]
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="red_start">#ff4040</color>
    <color name="red_center">#541515</color>
    <color name="red_end">#a82a2a</color>
    <color name="gray_start">#e0e0e0</color>
    <color name="gray_center">#666666</color>
    <color name="gray_end">#999999</color>
    <color name="black_start">#eeeeee</color>
    <color name="black_center">#3e3e3e</color>
    <color name="black_end">#555555</color>
    <color name="push_start">#8bff00</color>
    <color name="push_center">#5da800</color>
    <color name="push_end">#2e5400</color>
</resources>
[/xml]

作成したデザインをボタンに反映します。main.xmを開き、各ボタンにandroid:background追加してデザインbutton_blackを指定します。また、フォントサイズ指定android:textSizeと、文字色指定android:textColorも指定します。

main.xml
[xml]
・・・省略・・・
<Button
android:background="@drawable/button_black"
android:textSize="50sp"
android:textColor="#FFFFFF"
・・・省略・・・
[/xml]

実行してボタンのデザインが変わったことを確認してみましょう。

どうですか?AndroidではXMLによるシェイプの設定によりこんなに簡単にデザインを変更できるんです。android:background=には画像も指定できるので、さらにグラフィカルな表現にすることも出来ます。
今回使用したプロジェクトファイルはこちらからダウンロードできます。

更なる機能拡張

一般の電卓にはメモリー機能のボタンや%のボタンがあります。これらを追加するには状態遷移の追加が必要になりますがStateインタフェースの導入により容易にコーディング出きるでしょう。また、%ボタンの処理を追加するには新しい「状態依存の処理」を追加することになるでしょう。このばあいStateインタフェースにメソッドを追加することになります。それによりStateインタフェースを実装しているクラスすべてに修正が必要です。修正クラスが多くなり大変ですが、うっかりメソッドの追加を忘れてもコンパイルエラーとなり知らせてくれるので、実装漏れの心配が無くなります。
もしStateパターンを使っていなかったとしたらどうでしょうか。従来ながらの手法、フラグとif文でコーディングする方法であれば、処理の実装漏れをしてもバグが発生するまで気づかないことでしょう。Stateパターンは安全に機能拡張を行える優れた手法であると言えるでしょう。

まとめ

  • 電卓のベース処理ロジックにはGoFのデザインパターン「Stateパターン」を適用します。
  • 状態遷移を表現するには「Stateパターン」が有効です。
  • 「Stateパターン」により、機能追加により状態が増えた場合にも修正が少なくて済みます。
  • Androidアプリ開発では画面デザインをXMLで作成することができます。プログラムの完成後にデザインを変更する場合も、プログラム本体に修正を加えることなく、XMLの修正だけでデザインを変更できます。
  • ユーザのフィードバックが不要なちょっとしたメッセージはToast通知を使用します。

Android入門―本気で使える電卓アプリの開発―4

Androidのプロジェクトを作る

まずはAndroidのプロジェクトを作成しましょう。[ファイル]メニューより[新規]-[Android Project]を選択します。
New Android Projectウィンドウが開くので、次の値を設定して、プロジェクトを作成してください。

  • Project name:MyCalc(任意設定)
  • Build Target:Android 1.6
  • Application name:MyCalc(任意設定)
  • Package name:my.android.mycalc(任意設定)
  • Create Activity:MainActivity
  • Min SDK Version:4

Projectが作成されると、srcフォルダに「MainActivity.java」ファイルが作成されていると思います。これが今回のメイン画面になります。電卓なので画面はシンプルにMainActivity1画面のみで構成します。
「MainActivity.java」ファイルを開きましょう。雛形なのでほとんどなにもありません。
setContentView(R.layout.main);という一文があります。これはリソースファイルからmainという画面デザインを読み込むよう指示しています。では画面デザインmainを見ていきましょう。

Androidの画面デザインをXMLで行う

画面デザインは「res/layout」フォルダ内にある「main.xml」というxmlファイルで行います。「main.xml」ファイルを開くと、編集画面になります。下部にあるタブからGraphical Layoutを選ぶとグラフィカルな編集画面になり、main.xlmを選ぶとテキストによる編集画面になります。
タブでmain.xmlを選択してテキスト編集画面を開いてください。LinearLayoutが一つ、そのなかにTextViewが1つ配置されているのが分かると思います。ここに電卓のボタンを配置していきます。
LinerLayoutは、テキストやボタンなどの画面要素を直線的に縦または横に並べるためのレイアウトです。LinerLayoutの中にさらに2つのLinerLayoutを配置してください。
1つ目のLinerLayoutの中にTextViewを1つ配置します。これが電卓の液晶ディスプレイになります。2つ目のLinerLayoutの中には、TableLayoutを配置します。TableLayoutは電卓のボタンを格子状に並べるために使用します。
2つ目のTableLayoutの中には、行を表すTableRowを4つ配置します。各行にはボタンを4つずつ配置してください。
あとはボタンに名前をつけ、液晶ディスプレイの背景色と文字色を変更します。完成した「main.xml」は次のようになりました。

[xml]
<?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:id="@+id/linearLayout1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical|center_horizontal"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/display" android:textSize="40sp" android:textColor="#505050" android:background="#EEEEEE" android:gravity="right"></TextView> </LinearLayout> <LinearLayout android:id="@+id/linearLayout2" android:layout_width="fill_parent" android:orientation="horizontal" android:layout_height="fill_parent"> <TableLayout android:id="@+id/tableLayout1" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TableRow android:layout_weight="1" android:layout_height="fill_parent" android:layout_width="fill_parent" android:id="@+id/tableRow1"> <Button android:layout_height="fill_parent" android:text="+/-" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/sign" android:layout_margin="5px" android:layout_weight="1" android:minWidth="60dip"></Button> <Button android:layout_height="fill_parent" android:text="7" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/seven" android:layout_margin="5px" android:layout_weight="1" android:minWidth="60dip"></Button> <Button android:layout_height="fill_parent" android:text="8" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/eight" android:layout_margin="5px" android:layout_weight="1" android:minWidth="60dip"></Button> <Button android:layout_height="fill_parent" android:text="9" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/nine" android:layout_margin="5px" android:layout_weight="1" android:minWidth="60dip"></Button> <Button android:layout_height="fill_parent" android:text="歎" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/divide" android:layout_margin="5px" android:layout_weight="1" android:minWidth="60dip"></Button> </TableRow> <TableRow android:layout_weight="1" android:layout_height="wrap_content" android:layout_width="wrap_content" android:id="@+id/tableRow2"> <Button android:layout_height="fill_parent" android:text="C" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/clear" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="4" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/four" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="5" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/five" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="6" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/six" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="x" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/times" android:layout_margin="5px" android:layout_weight="1"></Button> </TableRow> <TableRow android:layout_weight="1" android:layout_height="fill_parent" android:layout_width="fill_parent" android:id="@+id/tableRow3"> <Button android:layout_height="fill_parent" android:text="AC" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/allclear" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="1" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/one" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="2" android:onClick="onClickButton" android:layout_width="wrap_content" android:id="@+id/two" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="3" android:onClick="onClickButton" android:layout_width="wrap_content" android:id="@+id/three" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="-" android:onClick="onClickButton" android:layout_width="wrap_content" android:id="@+id/minus" android:layout_margin="5px" android:layout_weight="1"></Button> </TableRow> <TableRow android:layout_weight="1" android:layout_height="fill_parent" android:layout_width="wrap_content" android:id="@+id/tableRow4"> <Button android:layout_height="fill_parent" android:text="0" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/zero" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="00" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/doublezero" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="." android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/comma" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="=" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/equal" android:layout_margin="5px" android:layout_weight="1"></Button> <Button android:layout_height="fill_parent" android:text="+" android:onClick="onClickButton" android:layout_width="fill_parent" android:id="@+id/plus" android:layout_margin="5px" android:layout_weight="1"></Button> </TableRow> </TableLayout> </LinearLayout> </LinearLayout>
[/xml]

各ボタンにはandroid:onClick="onClickButton"のようにonClick属性を追加してください。この指定をすることで、ボタンが押されたときにMainActivityクラスのonClickButtonメソッドが呼び出されます。

電卓のベース処理を修正する

さて、これまでに作成した電卓のベース部分では、演算結果を標準出力に表示する仕様でした。これを、先ほどXMLでレイアウトしたTextViewに表示するように変更します。
「StringDisplay.java」のshowDisplayメソッドを修正して、TextViewを更新出来るように変更します。

[java]
public class StringDisplay extends AbstractDisplay {
    private TextView txt;  //  ←追加
    public StringDisplay( TextView disp ) {
        clear();
        this.txt=disp;  //  ←追加
    }
    ・・・省略・・・
    public void showDisplay(boolean format) {
        ・・・省略・・・
        txt.setText(sb);  //  ←追加
    }
・・・省略・・・
[/java]

「Calc.java」では、StringDisplayに更新対象となるTextViewのインスタンスを渡すためのメソッドを追加します。

[java]
public void setDisp(TextView txt){
    this.disp = new StringDisplay(txt);
}
[/java]

電卓のベース処理をMainActivity.javaに組み込む

では次にお待ちかねのAndroidのプログラミングを説明していきます。「MainActivity.java」の編集です。電卓のベース処理を組み込み、MainActivityクラスにonClickButtonメソッドを実装してボタンが押されたときの処理を記述していきます。

[java]
public class MainActivity extends Activity {
    Calc calc = new Calc();
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        TextView txtDisp = (TextView) findViewById(R.id.display);
        calc.setDisp(txtDisp);
    }
    public void onClickButton(View view) {
        switch (view.getId()) {
        case R.id.zero:
            calc.onButtonNumber(Number.ZERO);
            break;
        case R.id.doublezero:
            calc.onButtonNumber(Number.DOUBLE_ZERO);
            break;
        case R.id.one:
            calc.onButtonNumber(Number.ONE);
            break;
        case R.id.two:
            calc.onButtonNumber(Number.TWO);
            break;
        case R.id.three:
            calc.onButtonNumber(Number.THREE);
            break;
        case R.id.four:
            calc.onButtonNumber(Number.FOUR);
            break;
        case R.id.five:
            calc.onButtonNumber(Number.FIVE);
            break;
        case R.id.six:
            calc.onButtonNumber(Number.SIX);
            break;
        case R.id.seven:
            calc.onButtonNumber(Number.SEVEN);
            break;
        case R.id.eight:
            calc.onButtonNumber(Number.EIGHT);
            break;
        case R.id.nine:
            calc.onButtonNumber(Number.NINE);
            break;
        case R.id.plus:
            calc.onButtonOp(Operation.PLUS);
            break;
        case R.id.minus:
            calc.onButtonOp(Operation.MINUS);
            break;
        case R.id.times:
            calc.onButtonOp(Operation.TIMES);
            break;
        case R.id.divide:
            calc.onButtonOp(Operation.DIVIDE);
            break;
        case R.id.comma:
            calc.onButtonNumber(Number.COMMA);
            break;
        case R.id.allclear:
            calc.onButtonAllClear();
            break;
        case R.id.clear:
            calc.onButtonClear();
            break;
        case R.id.equal:
            calc.onButtonEquale();
            break;
        case R.id.sign:
            calc.changeSign();
            break;
        default:
            break;
        }
    }
}
[/java]

onClickButtonメソッド内では、switch-case文で押されたボタンを判定して、Calcクラスのメソッドを呼び出しているだけです。
実はAndroid側のコーディングはたったこれだけです。これだけのコーディングでもちゃんと動いてくれます。
では早速実行してみましょう。

どうでしょう!これだけでも十分に電卓としての役目を果たしてくれまています!
ただ、まだエラーハンドリングが出来ていませんので、1÷0=と入力すると「Infinity」と表示されてしまいます。また演算結果が12桁以上になり、電卓の表示桁から溢れてしまう場合、上位桁しか表示されませんが、桁あふれしたことが分かりません。これは正しくエラー処理して、ユーザに通知する必要があります。
次からはエラー処理の追加をしていきたいと思います。
今回使用したプロジェクトファイルはこちらからダウンロード出来ます。

Android入門―本気で使える電卓アプリの開発―3

電卓の基礎ロジックの作成

最初に電卓の基礎部分を作ります。 単にA+B結果を表示するだけなのですが、 0から9の数値ボタン、四則演算ボタン、イコールのボタンとそれぞれ動作を割り当てなければなりません。 さらに今Aを入力中なのかBを入力中なのか、演算結果を表示中なのかを判定して各ボタンの動作を決めていかないといけないのです。簡単そうに思えて以外に複雑なんですね。
通常のプログラミングなら、このようなアプリケーションは状態をフラグで管理してifやcaseによる分岐を延々とコーディングしていくことになります。状態や動作が増えるにつれフラグは増え分岐は巨大となり収拾がつかなくなってしまいます。機能追加ともなれば全てのifやcase文に手を加える作業が待っています。これは大変です。
今回はデザインパターンを使用してifやcaseによる条件分岐を排除し、見通しのよい構成にしてみたいと思います。

GoFのデザインパターン

GoFのデザインパターンとはオブジェクト指向言語においてよく使われる設計の「パターン」であり「ノウハウ」です。プログラム設計の定石なのです。
今回、電卓の基礎部分では、SingletonパターンとStateパターンを使用します。パターンを使用することで、再利用しやすく、機能拡張しやすい構成になります。

Stateパターン

これは状態に対して動作を規定する場合に最適なデザインパターンです。Stateパターンでは状態をクラスで表します。状態が遷移している様子はクラスを差し替えて表現します。

Singletonパターン

これはそのクラスのインスタンスが1つしか生成されないことを保証するためのクラス設計です。 状態を表すクラスは1つだけで良いのです。もし状態が変わるたびにクラスを作成してしまったなら無駄なメモリを使ってしまいますので、Singletonパターンを用いて、状態を表すクラスはたった1つだけ生成されることを保証しましょう。

オートマトン理論と状態遷移表

遷移するルールと状態が遷移したときに何をするのかを定義しているものを有限オートマトンと呼びます。 それを図で表現したものが状態遷移図です。同じく表で表現したものを状態遷移表と呼びます。

電卓のオートマトンと状態遷移表

電卓には色々なボタンがありますね。これらは外部からの入力(イベント)を受け付るものであると考えます。イベントには次のようなものが考えられます。

  • イベント1)0~9のボタンが押される
  • イベント2)四則演算のボタン(×÷+-)が押される
  • イベント3)=のボタンが押される

そのほか、%のボタン、クリアボタンなどがありますね。これらもすべてイベントです。 電卓はこれらイベントの発生によって次々に状態が変化するわけです。
次に電卓の状態を考えましょう。電卓の場合、次の状態(State)が考えられます。

  • 状態1)数値Aの入力中
  • 状態2)四則演算の選択(×÷+-)
  • 状態3)数値Bの入力中
  • 状態4)演算した結果を表示中

次に電卓の動作です。電卓の基本動作になります。

  • 動作1)数値Aを保持する
  • 動作2)数値Bを保持する
  • 動作3)押された四則演算のボタンを保持する
  • 動作4)「数値A (× or ÷ or + or -) 数値B」を計算して表示する

このような状態と動作の紐付け、そして状態の遷移先を表にまとめると次のようになります。

図:状態遷移表
電卓 状態遷移表 状態 State
A:数値入力状態A NumberAState C:演算モード OperationState B:数値入力状態B NumberBState R:結果表示状態 ResultState
イベント event 0,~,9 入力をディスプレイに反映 →A ディスプレイクリア 入力をディスプレイに反映 →B 入力をディスプレイに反映 →B ディスプレイクリア 入力をディスプレイに反映 →A
四則演算 ÷×-+ ディスプレイを変数Aへ 演算子記憶 →C 演算子記憶 →C ディスプレイを変数Bへ反映 「変数A 演算子 変数B」をディスプレイ表示 ディスプレイ表示(演算結果)を変数Aへ 演算子記憶 →C 「変数A 演算子 変数B」をディスプレイ表示 ディスプレイ表示(演算結果)を変数Aへ 算子記憶 →C
入力中の値を表示 →R ÷,×の場合、「変数A 演算子 変数A」をディスプレイ表示 +,-の場合、変数Aをディスプレイ表示 →R 「変数A 演算子 変数B」をディスプレイ表示 →R なにもしない →R
Clear 変数Aクリア ディスプレイクリア →A 変数Aクリア ディスプレイクリア →A 変数Bクリア ディスプレイクリア →B 変数Aクリア 変数Bクリア ディスプレイクリア →A
AllClear 変数Aクリア 変数Bクリア ディスプレイクリア →A 変数Aクリア 変数Bクリア ディスプレイクリア →A 変数Aクリア 変数Bクリア ディスプレイクリア →A 変数Aクリア 変数Bクリア ディスプレイクリア →A

状態遷移表が出来たならまあほとんど完成したも同然ですね。あとは各状態を、Stateインタフェースを実装したクラスで表現していきます。
クラス内にはその状態の場合にイベントが発生した場合の動作を記載しておきます。 状態遷移表の各マスに記された動作の実装はContextに定義し、必要に応じてStateからContextに委譲します。
そして電卓クラスはContextを実装します。電卓には状態を保持しますので、Stateクラスを持ちます。 ではソースを見ていきましょう。
全ソースのダウンロードはこちらです。
まずStateインタフェースです。

[java]
public interface State {
    /**
     * 数値ボタン
     *
     * @param context
     * @param num
     *            数値ボタン
     */
    public abstract void onInputNumber(Context context, Number num);
    /**
     * 四則演算ボタン
     *
     * @param context
     * @param op
     *            演算子
     */
    public abstract void onInputOperation(Context context,
            Operation op);
    /**
     * =ボタン
     *
     * @param context
     */
    public abstract void onInputEquale(Context context);
    /**
     * クリアボタン
     *
     * @param context
     */
    public abstract void onInputClear(Context context);
    /**
     * オールクリアボタン
     *
     * @param context
     */
    public abstract void onInputAllClear(Context context);
}
[/java]

Stateでは、電卓に対するイベントを処理します。イベントは「数値ボタン」「四則演算ボタン」「=ボタン」「クリアボタン」「オールクリアボタン」の5つを規定しています。
ではこれを実装する数値入力状態A(NumberAState )の実装をみてみましょう。

[java]
public class NumberAState implements State {
    private static NumberAState singleton = new NumberAState();
    private NumberAState() { // コンストラクタはprivate
    }
    public static State getInstance() { // 唯一のインスタンスを得る
        return singleton;
    }
    @Override
    public void onInputNumber(Context context, Number num) {
        context.addDisplayNumber(num);
    }
    @Override
    public void onInputOperation(Context context, Operation op) {
        context.saveDisplayNumberToA();
        context.setOp(op);
        context.changeState(OperationState.getInstance());// 次の状態
    }
    @Override
    public void onInputEquale(Context context) {
        context.saveDisplayNumberToA();
        context.showDisplay(context.getA());
        context.changeState(ResultState.getInstance());
    }
    @Override
    public void onInputClear(Context context) {
        context.clearA();
        context.clearDisplay();
    }
    @Override
    public void onInputAllClear(Context context) {
        context.clearA();
        context.clearB();
        context.clearDisplay();
    }
}
[/java]

最初の部分である、

[java]
private static NumberAState singleton = new NumberAState();
private NumberAState() { // コンストラクタはprivate
[/java]

が、Singletonパターンになります。よく見るとコンストラクタがprivateになっています。こうすることでクラスのインスタンスを新たにNewできなくなります。じゃあどうやって利用するのかというと、唯一のインスタンスを取得するためにgetInstanceメソッドを呼びます。

[java]
public static State getInstance() { // 唯一のインスタンスを得る
return singleton;
}
[/java]

この単純な仕組みにより、クラスのインスタンスが唯一つであることを保証しているわけです。
では、電卓のイベントの実装です。

[java]
public void onInputNumber(Context context, Number num) {
context.addDisplayNumber(num);
}
[/java]

onInputNumberメソッドでは数値ボタンが押されたときのイベントを処理しています。contextのaddDisplayNumberメソッドを呼び出して、押された数字をディスプレイに追加します。
数値ボタンが押された場合、次の状態も「数値入力状態A(NumberAState )」ですので状態遷移はありません。
次は四則演算のボタンが押された場合の処理です。状態遷移表には
ディスプレイを変数Aへ
演算子記憶
→C
と記載されていますのでその通り実装していきます。

[java]
public void onInputOperation(Context context, Operation op) {
    context.saveDisplayNumberToA();		// ディスプレイを変数Aへ
    context.setOp(op);					// 押された演算子を保持
    context.changeState(OperationState.getInstance());// 次の状態へ遷移
}
[/java]

context.changeState(OperationState.getInstance())で、次の状態に遷移しています。四則演算のボタンが押された場合は、状態C:「演算モード(OperationState)」に遷移します。
その他の状態も実装してきます。
Stateを実装するクラスの各イベントメソッドでは、contextのメソッドを状態にあわせて呼び出しています。このcontextは電卓の基本機能そのものです。

[java]
public interface Context {
    // 状態遷移
    public abstract void changeState(State state);
    // 演算実行し結果をディスプレイに表示します。
    public abstract double doOperation();
    // ディスプレイ表示を更新します。
    void showDisplay();
    // ディスプレイ表示を引数の値で更新します。
    public abstract void showDisplay(double d);
    // ディスプレイ表示に数値を追加します。
    public abstract void addDisplayNumber(Number num);
    // ディスプレイ表示を変数Aへ保存します。
    public abstract void saveDisplayNumberToA();
    // ディスプレイ表示を変数Bへ保存します。
    public abstract void saveDisplayNumberToB();
    // 変数Aをクリアします。
    public abstract void clearA();
    // 変数Bをクリアします。
    public abstract void clearB();
    // 演算子を取得します。
    public abstract Operation getOp();
    // 演算子を設定します。
    public abstract void setOp(Operation op);
    // ディスプレイをクリアします。
    public abstract void clearDisplay();
    // メモリAからBへコピーします。
    public abstract void copyAtoB();
    // メモリAの取得です。
    public abstract double getA();
    // エラー表示を設定します。
    public abstract void setError();
    // エラー表示を解除します。
    public abstract void clearError();
    // +/-記号を反転します。
    public abstract void changeSign();
}
[/java]

メインとなる電卓クラスはContextを実装します。これらは各Stateからのコールバック関数となります。実際の挙動については電卓クラスに実装して、各StateからはContextに記載された動作を呼び出すように実装します。
また、電卓は唯一の出力デバイスとして液晶ディスプレイを持ちます。このディスプレイはクラスで表現しましょう。これにより、ディスプレイを差し替えて電卓の機能アップが容易に行えるように準備しておくためです。今回は単にテキストで表示する12桁のディスプレイを想定しましたが、後にグラフィックを用いたディスプレイクラスへと変更するのも面白いでしょう。
では、電卓クラスです。

[java]
public class Calc implements Context {
    private double A;// 電卓はメモリAを持ちます。
    private double B;// 電卓はメモリBを持ちます。
    private Operation op;// 電卓は演算子を持ちます。
    protected AbstractDisplay disp; // 電卓はディスプレイを持ちます。
    protected State state; // 電卓の状態を表すクラス
    public Calc() {
        A = 0d;
        B = 0d;
        op = null;
        changeState(NumberAState.getInstance());
        disp = new StringDisplay();
    }
    public void onButtonNumber(Number num) {
        state.onInputNumber(this, num);
    }
    public void onButtonOp(Operation op) {
        state.onInputOperation(this, op);
    }
    public void onButtonClear() {
        state.onInputClear(this);
    }
    public void onButtonAllClear() {
        state.onInputAllClear(this);
    }
    public void onButtonEquale() {
        state.onInputEquale(this);
    }
    @Override
    public void addDisplayNumber(Number num) {
        if (num == Number.ZERO || num == Number.DOUBLE_ZERO) {
            if (disp.displayChar.size() == 0 && !disp.commaMode) {
                disp.showDisplay(false);
                return;
            }
        }
        if (num == Number.COMMA && !disp.commaMode && disp.displayChar.size() == 0) {
            disp.onInputNumber(Number.ZERO);
        }
        disp.onInputNumber(num);
        disp.showDisplay(false);
    }
    @Override
    public void clearDisplay() {
        disp.clear();
        disp.showDisplay(false);
    }
    @Override
    public void clearA() {
        A = 0d;
    }
    @Override
    public void clearB() {
        B = 0d;
    }
    @Override
    public double doOperation() {
        double result = op.eval(A, B);
        showDisplay(result);
        return result;
    }
    @Override
    public void saveDisplayNumberToA() {
        A = disp.getNumber();
    }
    @Override
    public void saveDisplayNumberToB() {
        B = disp.getNumber();
    }
    @Override
    public void showDisplay() {
        disp.showDisplay(false);
    }
    @Override
    public void showDisplay(double d) {
        disp.setNumber(d);
        disp.showDisplay(true);
    }
    @Override
    public Operation getOp() {
        return op;
    }
    @Override
    public void setOp(Operation op) {
        this.op = op;
    }
    public double getA() {
        return A;
    }
    public double getB() {
        return B;
    }
    @Override
    public void changeState(State state) {
        this.state = state;
    }
    @Override
    public void copyAtoB() {
        B = A;
    }
    @Override
    public void clearError() {
        disp.clearError();
    }
    @Override
    public void setError() {
        disp.setError();
    }
    @Override
    public void changeSign() {
        if (disp.getNumber() != 0d) {
            disp.minus = !disp.minus;
            disp.showDisplay(false);
        }
    }
}
[/java]

いかがでしょう。メインとなる電卓クラスにはごちゃごちゃとしたIF文やCASE文も無く、実にすっきりしていますね。イベントが発生したら、stateのメソッドを呼び出しているだけです。あとは状態にあわせた適切な処理をstateが選択し実行してくれるという感じです。

[java]
 public void onButtonNumber(Number num) {
        state.onInputNumber(this, num);
    }
    public void onButtonOp(Operation op) {
        state.onInputOperation(this, op);
    }
    public void onButtonClear() {
        state.onInputClear(this);
    }
    public void onButtonAllClear() {
        state.onInputAllClear(this);
    }
    public void onButtonEquale() {
        state.onInputEquale(this);
    }
[/java]

電卓の液晶ディスプレイに数値を表示するStringDisplayは、内部に12桁の文字列を保持するクラスです。表示に関する処理はほとんとStringDisplayに委譲しています。StringDisplayの実装はダウンロードしたサンプルの中にありますので参考にしてください。ポイントは12桁の文字列をスタックで保持している点です。電卓の入力は後入れ先出し型のスタック形式なのです。スタックで保持することにより、今後BS(バックスペース)ボタンを追加したとき、「液晶ディスプレイに表示された数字を後ろから1つ削除する」といった操作に簡単に対応できることを狙っています。
さて、電卓クラスの唯一の機能である演算ですが、電卓クラスのdoOperationメソッドに実装されています。でも実際の演算らしきものが一切ありませんね。

[java]
public double doOperation() {
    double result = op.eval(A, B);
    showDisplay(result);
    return result;
}
[/java]

これは四則演算処理を列挙型で定義しているからです。javaの列挙型はその実態はクラスですので、処理を実装することも出来るわけです。これによりどの演算子であってもop.eval(A, B)という記述で四則演算ができてしまいます。 列挙型Operationのソースです。

[java]
public enum Operation {
    PLUS   { double eval(double x, double y) { return x + y; } },
    MINUS  { double eval(double x, double y) { return x - y; } },
    TIMES  { double eval(double x, double y) { return x * y; } },
    DIVIDE { double eval(double x, double y) { return x / y; } };
    abstract double eval(double x, double y);
}
[/java]

列挙型でメソッドの抽象を宣言し、定数ごとに具象メソッドでオーバーライドしています。そのようなメソッドを「定数固有」メソッドと呼びます。やや複雑な仕組ですが、知っておくととても便利です。
これで電卓の基本部分の説明は終わりです。
さてなかなかAndroidの話に入れませんね、電卓のベース部分が完成しましたので、次回からお待ちかねAndroidへの実装を行います。あと暫くお付き合い下さい。

参考文献

増補改訂版Java言語で学ぶデザインパターン入門
StateパターンでCSVを読む
デザインパターンの使い方: State

Android入門―本気で使える電卓アプリの開発―1

はじめに

Androidをはじめとするスマートフォンが盛り上がりをみせています。2011年の上半期の携帯電話販売ランキングの上位はスマートフォンがほぼ独占、爆発的な増加となっています。
スマートフォンのシェアが急激に拡大する中、開発の現場にも変化が起こっています。スマートフォン案件の増加です。iPhone用のアプリをAndroidに対応する案件や、業務端末としてAndroidやiPadを使いたいといった案件が増えてきました。スマートフォンへ開発のニーズが高まっているのを感じます
本稿はサンプルアプリの作成を通じてAndroid開発について解説していきます。
電卓アプリをサンプルとして使用しますが、よくある機能が限定されたようなサンプルではありません。実用に耐えうる電卓アプリの基盤を作成していきます。
本稿を読み終えた後は、電卓アプリにさらに機能追加を行うことが可能となりあなただけのカスタマイズ電卓が作成できるようになります。
完成したサンプルアプリはAndroid Marketからダウンロードできます。MyCalcというアプリですのでAndoroid Marketで検索してみてください。
URLはこちらです。
本稿でAndroid開発の全てを網羅できるわけではありませんが「Andoroid開発はこんな雰囲気だ」ということを肌で感じ、本格的なサンプルアプリを通して仕組みが分かる作りを目指しています。
まず、電卓アプリの中心となる計算処理を作成します。GoFのデザインパターンのうち「Stateパターン」を適用して電卓の計算処理を作成します。デザインパターンという高度な設計により電卓の機能拡張が容易であることを学習します。
次に、作成した計算処理をAndroidに組み込みます。
最後に、電卓のデザインを変更します。XMLの設定だけでグラフィカルな表現が可能であることを学習します。

Googleに上納金25ドルを納める

GoogleのAndroid Marketでアプリを公開してみる。手順は

  1. Googleにデベロッパー登録する
  2. 登録料25.00 USDをカードで支払う
  3. アプリを登録する。

なにはともあれ、やってみよう。ということで、まずはデベロッパー登録。個人出費25ドルはちと辛いが、まあAppleの上納金に比べればずいぶんマシか。

スマートフォン&モバイルEXPOに行くよ!Androidの市場調査に出発です!



本日より、東京ビックサイトにて「スマートフォン&モバイルEXPO」が開催されています。私も明日から参加してきますよ。

スマートフォンといえばiPhoneでしたが、いまや市場はAndoridですね。あとはWindowsPhone7の動向も気になるところです。スマートフォン市場の今を肌で感じてきたいとおもいます。
今回の調査のメインは、Andoridの開発ツールですので、事前調査しておきたいと思います。
今回の目的である、「Android™開発ゾーン」のブースです。

Android™開発ゾーン

西3-49
アイコムシステックhttp://www.icomsys.co.jp/
グローバルビジネス
アソシエイツ
西5-1
サンダーソフト
西6-8
レインボー・ジャパンhttp://www.rainbow.co.jp/original/iphone.html
西6-10
キャセイ・トライテックhttp://www.cathay.jp/
西5-7
ハートランド・データhttp://www.hldc.co.jp/
西5-6
ゼロソフトhttp://www.zerosoft.co.jp/product/index.html
電気通信大学
次に気になるのが、「テスト・検証ゾーン」

テスト・検証ゾーン

西8-2
シーイーシーhttp://www.cec-ltd.co.jp/seminar/2011_0511.html

Android API仕様確認サービス
-Android APIの解析ツール「PARA」
Android開発効率向上サービス -Android開発効率向上ツール「PEDM」
Android高速化サービス -Android機器のデータベース高速化ツール
Androidセキュリティ検証サービス -Android環境セキュリティ検査ツール
「VEX for スマートフォン」
Androidマネジメントサービス -Android端末管理サービス

西7-1
日本イントリピッドコントロールシステムズ
西6-1
ベリサーブhttp://www.veriserve.co.jp/
アクセンチュア
NTTエレクトロニクス
ニッコム
日本ノーベルhttp://www.jnovel.co.jp/index.html
日本ヒューレット・パッカード
西6-2
バルテス
西8-70
日本ノーベルhttp://www.jnovel.co.jp/index.html
西8-8
IT検証産業協会http://www.ivia.or.jp/
西8-12
ヴェス
西7-7
アリオンhttp://www.allion.co.jp/
西7-9
SOLID STATE
DRIVE ALLIANCE
西7-11
リアクティブシステムズ
西7-8
産業タイムズ社http://www.sangyo-times.co.jp/index.htm
西7-10
ARCO INFOCOMM

その他の情報

NIDhttp://www.nid.co.jp/news/2011/110428.html

GoFのデザインパターンのうちStateパターンについて調査した

電卓の状態遷移を表す為に、ステートパターンを調査してみる。状態遷移なんて久しぶりすぎて忘れている。
調べたサイトへのリンク
http://japan.internet.com/column/developer/20081224/26.html
http://blogs.wankuma.com/nagise/archive/2008/06/05/141783.aspx
http://www002.upp.so-net.ne.jp/ys_oota/mdp/State/index.htm
http://www5.plala.or.jp/igjhmc/calc/spec.html