寒川アクアブログ

美容師しながらアプリ開発していて水草が趣味の私のブログです

【電卓アプリを作るvol.4】ソースコード2/2

Calc

メインの電卓クラス
アクティビティからnewして使います。

public class Calc implements Context {

    //    ディスプレイ表示が変わるたびに、文字列をコールバックします
    public interface Callback{
        void onTextChanged(String string);
    }

    Callback callback;

  //  最大表示可能桁数
    private final int MAX_DIGIT = 12;

    static public final int KEY_00 = 10,
            KEY_DOT = 20,
            KEY_EQUAL = 21,
            KEY_TASU = 22,
            KEY_HIKU = 23,
            KEY_KAKERU = 24,
            KEY_WARU = 25,
            KEY_PERCENT = 26,
            KEY_SIGN_CHANGE = 27,
            KEY_C = 31,
            KEY_AC = 32,
            KEY_BACK_SPACE = 32;

    private StringBuilder stringBuilder;
    private double value1,value2;  //  左辺、右辺保持
    private int enzanshi;
    private State state;

  //  コンストラクター。ACで初期化しています
    public Calc() {
        allClear();
    }

    public void setListener(Callback callback){
        this.callback = callback;
    }

  //  現在の表示をdoubleで取得
    private double getValue(){
        return Double.valueOf(stringBuilder.toString());
    }

    //  文字列を表示用変数にセット
    private void set(String s){
        stringBuilder = new StringBuilder(s);
    }

   //  doubleを文字列に変換して、表示用変数にセット
    private void set(double d){
        stringBuilder = new StringBuilder(String.valueOf(d));
    }


    private boolean isENZAN_Key(int key) {
        if (key == KEY_TASU || key == KEY_HIKU || key == KEY_KAKERU || key == KEY_WARU){
            return true;
        }
        return false;
    }


    private boolean isNumber_key(int key) {
        if (0 <= key && key <= 9 || key == KEY_DOT){
            return true;
        }
        return false;
    }

  //  引数のキーを入力
    public void input(int inputKey) {
        if (isNumber_key(inputKey)) state.inputNumber(this, inputKey);
        else if (inputKey == KEY_00) {
            state.inputNumber(this, 0);
            state.inputNumber(this, 0);
        }
        else if (isENZAN_Key(inputKey)) state.inputEnzanshi(this, inputKey);
        else if (inputKey == KEY_EQUAL) state.inputEqual(this);
        else if (inputKey == KEY_C) state.inputClear(this);
        else if (inputKey == KEY_AC) allClear();
        else if (inputKey == KEY_BACK_SPACE) state.inputBackSpace(this);
        else if (inputKey == KEY_SIGN_CHANGE) signChange();
        else if (inputKey == KEY_PERCENT) state.inputPercent(this);

        callback.onTextChanged(stringBuilder.toString());
    }


  //  四則演算します
    private double calculation(double d1,int ope,double d2){
        try {
            double ans = 0;
            switch (ope) {
                case KEY_TASU:
                    ans = d1 + d2;
                    break;
                case KEY_HIKU:
                    ans = d1 - d2;
                    break;
                case KEY_KAKERU:
                    ans = d1 * d2;
                    break;
                case KEY_WARU:
                    if (d2 == 0){
                        // エラー
                        return 0;
                    } else {
                        ans = d1 / d2;
                    }
                    break;
            }
            return ans;
        } catch (java.lang.IllegalArgumentException a){
            // エラー
        }
        return 0;
    }


  //  +/-の変換
    private void signChange() {
        if (state.getStateValue(this) == State.STATE_B){
            setLeftDouble();
            set("-0");
            changeState(StateC.getInstance());
        } else {
            if (stringBuilder.indexOf("-") == -1){
                stringBuilder.insert(0,"-");
            } else {
                stringBuilder.deleteCharAt(0);
            }
        }
    }

  //  末尾に数字を追加
    @Override
    public void addNum(int key) {
        if (stringBuilder.length() >= MAX_DIGIT)return;
        if (key == KEY_DOT) {
            if (stringBuilder.indexOf(".") == -1) stringBuilder.append(".");
        } else if (stringBuilder.length() == 1 && stringBuilder.charAt(0) == '0') {
            stringBuilder.replace(0, 1, "" + key);
        } else if (stringBuilder.toString().equals("-0")) {
            stringBuilder.replace(1, 2, "" + key);
        } else {
            stringBuilder.append("" + key);
        }
    }

  //  表示されている数値を左辺として保持
    @Override
    public void setLeftDouble() {
        value1 = getValue();
    }

  //  表示されている数値を右辺として保持
    @Override
    public void setRightValue() {
        value2 = getValue();
    }

  //  押された演算子を保持
    @Override
    public void setEnzanshi(int key) {
        enzanshi = key;
    }


    @Override
    public void keisan() {
        double ans = calculation(value1,enzanshi,getValue());
        set(ans);
    }

  //  表示の数値と、保持していた右辺・演算子で計算
    @Override
    public void teisuKeisan() {
        double ans = calculation(getValue(),enzanshi,value2);
        set(ans);
    }

  //  演算子が+、-のときは左辺のxパーセント
    //    ×、÷のときは右辺の100分の1をセット
    @Override
    public void percent() {
        if (enzanshi == KEY_TASU || enzanshi == KEY_HIKU){
            set((getValue() * value1) / 100d);
        } else {
            set(getValue() / 100d);
        }
    }

    @Override
    public void dividedBy100() {
        set(getValue() / 100d);
    }

  //  1文字消す
    @Override
    public void backSpace() {
        if (stringBuilder.length() == 1){
            set("0");
        } else if (stringBuilder.length() == 2 && stringBuilder.indexOf("-") == 0){
            set("0");
        } else {
            stringBuilder.deleteCharAt(stringBuilder.length() - 1);
            set(stringBuilder.toString());
        }
    }


    @Override
    public void clear() {
        if (stringBuilder.toString().equals("0")) {
            allClear();
        } else {
            set("0");
        }
    }


    @Override
    public void allClear() {
        set("0");
        enzanshi = -1;
        value1 = 0;
        value2 = 0;
        changeState(StateA.getInstance());

    }


    @Override
    public void dispZero() {
        set("0");
    }

  //    状態を明示的に変更
    @Override
    public void changeState(State instance) {
        this.state = instance;
    }

}

【電卓アプリを作るvol.3】ソースコード1/2

前回まで・・・
電卓のアルゴリズムについて考えました
今回は、ソースコードを掲載します。
参考書のような詳細な解説は割愛しますが、
状態遷移表とコードを何度も見比べれば、
理解できると思います。

アンドロイドスタジオのエクスプローラービューは↓こうなってます
f:id:kentaro198477:20160919225501p:plain

Context

各状態クラスから、Calc(電卓のメインの処理)クラスに知らせるためのインターフェイス

public interface Context {

    void addNum(int key);
    void setLeftDouble();
    void setEnzanshi(int key);
    void changeState(State instance);
    void clear();
    void backSpace();
    void percent();
    void dispZero();
    void keisan();
    void allClear();
    void teisuKeisan();
    void setRightValue();
    void dividedBy100();
}
State

各状態クラスに継承させるインターフェイス

public interface State {

    static final int STATE_A = 1,STATE_B = 2,STATE_C = 3,STATE_D = 4;

    int getStateValue(Context context);
    void inputNumber(Context context, int key);
    void inputEnzanshi(Context context, int key);
    void inputClear(Context context);
    void inputEqual(Context context);
    void inputBackSpace(Context context);
    void inputPercent(Context context);
}
StateA

左辺入力中の状態
以下、各ステートは複数のインスタンスは必要ないので、
シングルトンパターンを採用しています。
シングルトンについては、詳しく解説されたサイトが多数あるので
そちらをご参考ください。

public class StateA implements State{
    private static StateA ourInstance = new StateA();

    public static StateA getInstance() {
        return ourInstance;
    }

    private StateA() {
    }
    
    @Override
    public int getStateValue(Context context) {
        return STATE_A;
    }

    @Override
    public void inputNumber(Context context, int key) {
        context.addNum(key);
    }

    @Override
    public void inputEnzanshi(Context context, int key) {
        context.setLeftDouble();
        context.setEnzanshi(key);
        context.changeState(StateB.getInstance());
    }

    @Override
    public void inputClear(Context context) {
        context.clear();
    }

    @Override
    public void inputEqual(Context context) {

    }

    @Override
    public void inputBackSpace(Context context) {
        context.backSpace();
    }

    @Override
    public void inputPercent(Context context) {
        context.dividedBy100();
    }
}
StateB

演算子入力中の状態(抽象メソッド以外は省略)

    @Override
    public int getStateValue(Context context) {
        return STATE_B;
    }

    @Override
    public void inputNumber(Context context, int key) {
        context.dispZero();
        context.addNum(key);
        context.changeState(StateC.getInstance());
    }

    @Override
    public void inputEnzanshi(Context context, int key) {
        context.setEnzanshi(key);
    }

    @Override
    public void inputClear(Context context) {
        context.clear();
        context.changeState(StateC.getInstance());
    }

    @Override
    public void inputEqual(Context context) {
        context.setRightValue();
        context.keisan();
        context.changeState(StateD.getInstance());
    }

    @Override
    public void inputBackSpace(Context context) {

    }

    @Override
    public void inputPercent(Context context) {
        context.percent();
    }
StateC

右辺入力中の状態(抽象メソッド以外は省略)

   @Override
    public int getStateValue(Context context) {
        return STATE_C;
    }

    @Override
    public void inputNumber(Context context, int key) {
        context.addNum(key);
    }

    @Override
    public void inputEnzanshi(Context context, int key) {
        context.setEnzanshi(key);
        context.keisan();
        context.setLeftDouble();
        context.setRightValue();
        context.changeState(StateB.getInstance());
    }

    @Override
    public void inputClear(Context context) {
        context.clear();
    }

    @Override
    public void inputEqual(Context context) {
        context.setRightValue();
        context.keisan();
        context.changeState(StateD.getInstance());
    }

    @Override
    public void inputBackSpace(Context context) {
        context.backSpace();
    }

    @Override
    public void inputPercent(Context context) {
        context.percent();
    }
StateD

イコールが押された直後の状態(抽象メソッド以外は省略)

 @Override
    public void inputNumber(Context context, int key) {
        context.allClear();
        context.addNum(key);
        context.changeState(StateA.getInstance());
    }

    @Override
    public void inputEnzanshi(Context context, int key) {
        context.setLeftDouble();
        context.setEnzanshi(key);
        context.changeState(StateB.getInstance());
    }

    @Override
    public void inputClear(Context context) {
        context.clear();
    }

    @Override
    public void inputEqual(Context context) {
        context.teisuKeisan();
    }

    @Override
    public void inputBackSpace(Context context) {

    }

    @Override
    public void inputPercent(Context context) {
        context.dividedBy100();

    }

【電卓アプリを作るvol.2】アルゴリズム

前回まで・・・
状態遷移表ができました。
今回はこの状態遷移表に基づいた処理を、どう実装するのかを考えます。


状態を定義

A、B、C、Dの各状態を、クラスで表現します。

「状態をクラスで・・・」というと、漠然としていますが、
結局のところ、何かイベントが発生したときに、
その時の状態に沿ったメソッドを呼び出す、
ということを意味しています。

仮に、各状態を表すクラスをA、B、C、Dとし、
これを格納する変数をstateとします。
キー操作があったときは、
state.メソッド名();
という形で実行します。
”電卓的に”状態が変わったら、
state = Bのインスタンス
といった具合にインスタンスを差し替えて、状態の遷移を明示的に行うことで、
次に同じメソッドを実行しても、こんどは状態Bの処理が行われます。


キー操作

UIをデザインし、キーとしてButtonを設置したら、
オンクリックリスナーを定義し、
クリックされたキーに対応するメソッドを、
変数stateから呼び出します。
イベントはある程度カテゴライズしないと記述が膨大になり大変なので、

・数字キーが押された
演算子キーが押された
・イコールが押された
・クリアーキーが押された
・%キーが押された

くらいに分けますが、
全てのキーを定数で定義し、
引数に渡して、最終的には明確に処理します。


電卓としての処理

ユーザーの操作をイベントとして送ったら、
現在の状態に応じた、適切な命令に辿り着くはずです。
その命令こそ、電卓の肝となる部分です。
演算はもちろん、
文字を末尾に付け加える、
押された演算子を記憶しておくなども含まれます。
電卓がやっていることを全て再現する必要があります。


電卓のディスプレイ

電卓を使う人が求めているものは、おそらく”答え”ですので、
内部で処理したものをユーザーから見えるようにしなくてはいけません。
計算結果を文字列にして、アクティビティに送り、
テキストビュー等に表示します。
電卓らしく振舞うならば、
入力途中も表示する情報はあります。
また、10 ÷ 3 の結果を
”3.3333333333333333333”にするのか
”3.333”にするのかも考えます。
12,345のように、カンマも入れた方が見やすいでしょう。
式を表示するアプリも多く見かけます。


たかが電卓と思いきや、実は複雑なことをやっております。
これが¥100ショップで買えてしまう時代です。
プレイストアで¥100以上で配布できるような電卓を創りましょう!

【電卓アプリを作るvol.1】状態遷移

電卓ロジックを考える

電卓を作るにあたって、
電卓をどのように操作していたかを思い出してみます。

まず数値を入力して、演算子キーを押し、
2つめの数値を入力して、最後に[=]キーを叩くという流れではないでしょうか。

演算子を押した直後に数値を入力すると、表示はいったんリセットされ、
また一桁目から入力していくようになります。

これは、最初は"左辺を入力している状態"になり、演算子を押した後は、
"右辺を入力する状態"になるといった具合に、
ユーザーの操作によって、内部的な"状態"が遷移していく、ということです。

状態の種類

基本的な電卓において、状態の種類を考えると、以下の4つでカバーできます。
各状態を、A・B・C・D、とします。

A 左辺入力中 (HELLO !)
B 演算子入力中(演算子が押され、右辺の入力前)
C 右辺入力中 (右辺の入力が開始された)
D 計算結果 (イコールが押された)

右は状態の特徴を説明していますが、この限りではありません。

主なキーの種類

数字キー (小数点を含む)
演算子キー
イコールキー
クリアーキー(オールクリアーも兼ねる)
%キー

・・・他にも、[+/-]チェンジキーや、メモリーキーなどありますが、
状態によって特に挙動が異なるのがこれらのキーであること、
あまり多いと記述が大変になってくるので、上記の5種類に絞っています。


電卓においては、特別な機種を除いて、キーの操作によってのみ、状態の変化が起きます。
キーの操作と、状態の関係を表すと、以下のようになります

f:id:kentaro198477:20160915214328p:plain
見づらくて申し訳ありません

今回の電卓では、%キーと、定数計算をカバーしています。
「%の処理」とありますが、%キーの処理は、
入力された演算子が[+][-]の場合と、[×][÷]の場合で違ってくるためです。


たかが電卓と、侮ることはできません。
でも、状態遷移をしっかり意識することで、
信頼性の高いアプリになっていきます。

次回は、この表を元に、状態によって処理を分ける仕組みを作っていきます!

【電卓アプリを作る】はじめに

電卓アプリを作る!

信頼性が高く、デザインも良い、
小数点セレクターやラウンドセレクターで、
会社の方針に沿った計算結果を表示。
%や定数計算にも対応!
あなただけのオリジナルな電卓も作れるように、
汎用性も高い!

↑こんな電卓を、これから何回かに分けて、
作っていきたいと思います。

注意点!

javaと、アンドロイドについて、
ある程度基本を理解されている方を対象としています。

・コードが汚い、見辛いなどあるかも知れませんが、
ご容赦ください。

・こうしたらもっと良くなるよ!などのアイデアは大歓迎です。

・説明下手ですので、
解説が解りづらい等あると思いますが、
ご了承ください。

・生意気な響きですが、習うより慣れろスタイルです。
説明下手ですので(笑)


一目惚れして買った電卓です↓
f:id:kentaro198477:20160904013238j:plain
電卓の実機が手元にあると便利ですが、すぐには使いません(笑)
電卓の挙動は、カシオ式、非カシオ式など、
仕様が様々あります。
カシオ計算機や、シャープの電卓の公式サイトなどを見比べてみるのも、
電卓を作る上で、理解度を高めるのでオススメです。

LinkedListクラスのよく使うメソッド

便利なLinked List

 リンクドリストはオブジェクトの配列なのですが、汎用性が高く、
とにかく使い勝手が良い。私の一番好きなクラスかもしれない(笑)。
LinkedListの詳細についてはここでは記載しませんが、
オブジェクトをぽんぽん入れられて、前から(後ろから)1個づつ取り出せる配列といった感じ。
取り出しつつ削除もしてくれるところから、”待ち行列”として利用できるのが特徴です。

例えば、電卓の計算履歴を残す場合だったら、
’=’が押されるたびに計算結果を文字列としてリストに追加し、
GSon経由でSharedPreferencesで保存。
キャラクターがたくさんいるゲームだったら、
アニメーションさせるキャラのクラスをリストに追加して、
1体づつ処理していくなど、使い道はさまざまです。



 利用方法は、LinkedList型の変数を用意し、newでインスタンス化します。
インスタンスに対し、各メソッドで、オブジェクトを追加、取得などを行います。

LinkedList list = new LinkedList();
list.add("りんご");
list.add("ゴリラ");

String string1 = list.pollFirst();  //  りんご size 1
String string2 = list.pollFirst();  //  ゴリラ  size 0

主なメソッド

追加系
boolean add(Object object)
リストの最後に要素を追加
void addLast(Object object)
リストの最後に要素を追加
void add(int index,Object object)
リストの指定された位置に要素を追加
indexが範囲外の時は例外 IndexOutOfBoundsException を投げる
void addFirst(Object object)
リストの先頭に要素を挿入
取り出す系
Object get(int index)
リスト内の指定された位置の要素を取得
indexが範囲外の時は、例外 IndexOutOfBoundsException を投げる
Object getFirst()
最初の要素を取得
Object getLast()
最後の要素を取得
Object pollFirst()
最初の要素を取得し、削除する。(無いときはnull)
Object pollLast()
最後の要素を取得し、削除する。(無いときはnull)
その他の操作
Object set(int index,Object object)
指定された位置にある要素を、指定された要素で置き換える
戻り値は、置き換えられた、元の要素
indexが範囲外なら例外 IndexOutOfBoundsException
void clear()
リストから全ての要素を削除
Object remove(int index)
指定された位置の要素を削除

 ここに挙げたのはほんの一部ですが、とても便利なクラスなので、
がんがん使っていきたいですね!

カラーフィルターまとめ(PorterDuff.Mode)

f:id:kentaro198477:20160908020908p:plain

イメージビューにカラーフィルターを掛けた時の効果のサンプルです。

適当なボタンの画像と、ドロイド君に、Color.REDと、各モードのカラーフィルターを施しました。

ボタン部分とドロイド君は不透明で、外側は透明です。

もっと透明度に対する影響がわかりやすい画像にすればよかったと後悔・・・


不透明ので各フィルター処理。

f:id:kentaro198477:20160908020906p:plain

MULTIPLYのドロイド君がゴ○ブリに見えてしまう・・・


以下は、半透明のでフィルターを掛けています。

f:id:kentaro198477:20160908020904p:plain


ボタンなどを、ひとつのリソースから色違いを作るには、MULTIPLYでよさそうです。

ADDは、絵の具を混ぜたような、

DARKENは影に入ったように暗く見え、LIGHTENは、なんとなく輝いて見える。

Bitmapを操作しなくても、ちょっとしたエフェクトを掛けたような効果は得られそうですね。


せっかくなので、テスト用に作った上のスクショのアプリをプレイストアで公開しました!

赤以外にも様々な色や、リソースを試すことができます。

透明度も指定できます。ついでに色のHTML表記も表示します。

広告は一切表示されません。

開発のお役に立てれば幸いです。
PorterDuff.Mode - Google Play の Android アプリ