スマートフォン・ジン | Smartphone-Zine

引っ越し先→ https://smartphone-zine.com/

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通知を使用します。