【電卓アプリを作る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
前回まで・・・
電卓のアルゴリズムについて考えました
今回は、ソースコードを掲載します。
参考書のような詳細な解説は割愛しますが、
状態遷移表とコードを何度も見比べれば、
理解できると思います。
アンドロイドスタジオのエクスプローラービューは↓こうなってます
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種類に絞っています。
電卓においては、特別な機種を除いて、キーの操作によってのみ、状態の変化が起きます。
キーの操作と、状態の関係を表すと、以下のようになります
見づらくて申し訳ありません
今回の電卓では、%キーと、定数計算をカバーしています。
「%の処理」とありますが、%キーの処理は、
入力された演算子が[+][-]の場合と、[×][÷]の場合で違ってくるためです。
たかが電卓と、侮ることはできません。
でも、状態遷移をしっかり意識することで、
信頼性の高いアプリになっていきます。
次回は、この表を元に、状態によって処理を分ける仕組みを作っていきます!
【電卓アプリを作る】はじめに
電卓アプリを作る!
信頼性が高く、デザインも良い、
小数点セレクターやラウンドセレクターで、
会社の方針に沿った計算結果を表示。
%や定数計算にも対応!
あなただけのオリジナルな電卓も作れるように、
汎用性も高い!
↑こんな電卓を、これから何回かに分けて、
作っていきたいと思います。
注意点!
・javaと、アンドロイドについて、
ある程度基本を理解されている方を対象としています。
・コードが汚い、見辛いなどあるかも知れませんが、
ご容赦ください。
・こうしたらもっと良くなるよ!などのアイデアは大歓迎です。
・説明下手ですので、
解説が解りづらい等あると思いますが、
ご了承ください。
・生意気な響きですが、習うより慣れろスタイルです。
説明下手ですので(笑)
一目惚れして買った電卓です↓
電卓の実機が手元にあると便利ですが、すぐには使いません(笑)
電卓の挙動は、カシオ式、非カシオ式など、
仕様が様々あります。
カシオ計算機や、シャープの電卓の公式サイトなどを見比べてみるのも、
電卓を作る上で、理解度を高めるのでオススメです。
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)
イメージビューにカラーフィルターを掛けた時の効果のサンプルです。
適当なボタンの画像と、ドロイド君に、Color.REDと、各モードのカラーフィルターを施しました。
ボタン部分とドロイド君は不透明で、外側は透明です。
もっと透明度に対する影響がわかりやすい画像にすればよかったと後悔・・・
不透明の赤で各フィルター処理。
MULTIPLYのドロイド君がゴ○ブリに見えてしまう・・・
以下は、半透明の赤でフィルターを掛けています。
ボタンなどを、ひとつのリソースから色違いを作るには、MULTIPLYでよさそうです。
ADDは、絵の具を混ぜたような、
DARKENは影に入ったように暗く見え、LIGHTENは、なんとなく輝いて見える。
Bitmapを操作しなくても、ちょっとしたエフェクトを掛けたような効果は得られそうですね。
せっかくなので、テスト用に作った上のスクショのアプリをプレイストアで公開しました!
赤以外にも様々な色や、リソースを試すことができます。
透明度も指定できます。ついでに色のHTML表記も表示します。
広告は一切表示されません。
開発のお役に立てれば幸いです。
PorterDuff.Mode - Google Play の Android アプリ