寒川アクアブログ

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

Kojittoアプリ プライバシーポリシー

プライバシーポリシー

2016年12月22日 改訂
開発者:Kojitto
mail:kentaro198477@gmail.com

Kojittoの提供するAndroidアプリ「タイマークラシック」、「Digital Timer - free」(以下、当アプリ)では、
ユーザー様の個人情報(年齢・性別・位置情報等)の一切を取得しておりません。
よって、第三者への提供、開示等行われることもありません。
当アプリのダウンロード時に必要となる権限「カメラ機能の利用」については、
当アプリ内でのタイマー終了時におけるユーザー様への通知の手段として、端末のフラッシュライトを点灯させることがあり、
これは端末のカメラ機能の一部を利用することで実現しています。撮影・録画等を行うプログラムは一切含まれておりません。
プライバシーポリシーの変更を行った場合は、当ブログにて通知いたします。

アクティビティをスタックに残さなくする

例えばタイマーアプリを作っていて、

タイマー終了時に透明なアクティビティを開始して、

アラートダイアログを表示し、

OKが押されたり、dismissされたりしたときに

そのアクティビティをフィニッシュするとします。


この透明アクティビティがスタックに残っていると、

端末の”□”をタップしたときに表示され、ユーザーがそれをタップすると

またアラートダイアログが表示されるので、タイマーの挙動としては不自然です。

透明アクティビティがスタックに残らないようにするには、マニフェストに以下のように記述します。

<activity
    android:name=".MyActivity"
    android:excludeFromRecents="true"
    android:theme="@android:style/Theme.Translucent.NoTitleBar"
         >

android:excludeFromRecentsをtrueにするだけです。

また、テーマを@android:style/Theme.Translucent.NoTitleBarにした場合、
アラートダイアログのUIデザインが昔っぽいものになってしまうので、
これを避けるには、独自にスタイルを定義する必要があります。

【電卓アプリを作るvol.7】アプリ終了時の状態を保存

前回まで・・・
計算結果に端数が出た場合に、
それを丸めて表示する静的メソッドを実装しました。

今回は、
計算中に電話がかかってきて、
戻ったら全てクリアーされていたという、
悲しいことが起こらないように、
終了時の状態を保存し、
再開したときにロードする機能を実装していきます。

何を保存するべきか

電卓を開いたときに、
どのような情報があれば、
同じ状態を再現できるでしょうか?

最後に見ていたものと、再開直後が違っていたら、
不安になってACを押してしまうでしょう。
なので、
①ディスプレイに表示されている文字列
は欠かせません。
見たままを保存します。
単純に、StringBuilder.toString()で得られる文字列を保存します。

電卓には状態があると前途しました。
状態によって呼び分けられるメソッドが変わってきます。
②現在の状態
を保存します。
現在の状態を知るためのメソッド、
int getStateValue()をすでに用意していますので、
それによって得られた定数も保存します。

状態が右辺入力中なら、
③左辺の情報が無いと計算できません。
演算子も同様です。
定数計算のために、
⑤右辺の値も必要です。

今回はメモリー機能やグランドトータル機能は実装していませんので、
これら5つの情報を保存すれば、
どの状況も再現が可能です。

再現するには

アプリのオンポーズ時にこれらを保存し、
オンレジューム時に、
Calcクラスのインスタンスのフィールドに、
上記の情報を格納します。
stateに状態に応じたクラスのインスタンス
value1に左辺の値をdoubleで、
value2に右辺の値を同じくdouble、
enzanshiは、演算子を示す定数、
stringBuilderに、最後に見ていたのと同じ文字列を入れます。

Calc内に、これら5情報を保持するクラスを作ります

コンストラクターも用意します。

    public class CalcHolder{

        public int state;
        public String dispString;
        public double leftValu,rightValue;
        public int enzanshi;

        public CalcHolder(int state,String dispString, double leftValue,
                          double rightValue, int enzanshi) {

            this.state = state;
            this.dispString = dispString;
            this.leftValu = leftValue;
            this.rightValue = rightValue;
            this.enzanshi = enzanshi;
        }
    }

同じくCalc内に、セーブ用とロード用のパブリックなメソッドを定義します。
セーブ用のgetHolderでは、CalcHolderのインスタンスを返し、
ロード用のsetHolderは、CalcHolderのインスタンスを渡すことによって、
前回の状態に復帰します。

   public CalcHolder getHolder(){
        return new CalcHolder(state.getStateValue(this),stringBuilder.toString(),
                value1,value2,enzanshi);
    }
    
    public void setHolder(CalcHolder holder){
        switch (holder.state){
            case State.STATE_A:
                changeState(StateA.getInstance());
                break;
            case State.STATE_B:
                changeState(StateB.getInstance());
                break;
            case State.STATE_C:
                changeState(StateC.getInstance());
                break;
            case State.STATE_D:
                changeState(StateD.getInstance());
                break;
        }
        this.value1 = holder.leftValu;
        this.value2 = holder.rightValue;
        this.enzanshi = holder.enzanshi;
        set(holder.dispString);
    callback.onTextChanged(stringBuilder.toString());
    }


メインアクティビティのonPauseとonResumeに保存・復帰の処理を記述します。
Gsonを利用しますので、Gsonライブラリーを読み込んで置きます。
(Gsonについての関連記事)

  @Override
    protected void onPause() {
        super.onPause();
        Calc.CalcHolder holder = calc.getHolder();
        Gson gson = new Gson();
        String js = gson.toJson(holder, Calc.CalcHolder.class);
        getSharedPreferences("pref",MODE_PRIVATE).edit().putString("holder",js).apply();
    }

    @Override
    protected void onResume() {
        super.onResume();
        final String NULL = "null";
        Gson gson = new Gson();
        String js = getSharedPreferences("pref",MODE_PRIVATE).getString("holder",NULL);
        if (js.equals(NULL)){
            calc.allClear();
            //    初回起動
        } else {
            calc.setHolder(gson.fromJson(js, Calc.CalcHolder.class));
        }
    }

正しく動作するか確かめてみてください。
マニフェストでアプリをポートレイトにしていなければ、
端末の向きを変えてみて、数字がリセットされないことでも確認できます。

ちなみに、onResume内の「初回起動」とコメントアウトした部分は、
そのまま、ダウンロード後の第一回目の起動なので、
サンキューメッセージ等を表示するのもいいかもしれません。

【電卓アプリを作るvol.6】計算結果を成型する

前回まで・・・
とりあえず、必要最低限の電卓ができました。
今回は、計算結果を丸めて表示する機能を実装します。

現状の問題点

今の段階では、1+1=の結果が、

"2.0" (シンプルに "2" でいいんじゃない?)

また、10÷6のように割り切れない計算をすると
桁数が非常に多くなってしまううえ、

"1.6666666666666667"

というふうに、最後が謎の7になっています。
これはコンピューター上の仕様のようですが、
一般的には、「6が永遠と続く」という認識なので、
ユーザーからしたら不具合かと思われかねません。

そこで、一般的な電卓に習い、12桁までの表示にするとか、
小数点以下n位までの表示にする、
といった機能を実装していきます。

一般電卓の多くは、「小数点セレクター」「ラウンドセレクター」
という機能があり、
「小数点セレクター」は、小数点以下第何位まで表示するか、
「ラウンドセレクター」は、小数点セレクターで設定した位置に丸める際、
切り上げか、切捨てか、四捨五入するかなどを設定できる機能です。
カシオ計算機のサイトなどで使い方が詳しく載っています。
今回の実装ではこれらの機能をお手本にしているので、
理解度を高めるためにも、一度ご覧になることをオススメします。

自作のRoundSelectorクラス

Calcから送られてきた文字列を、成型した文字列に変換するクラスです。
引数に、計算結果のdouble、小数点の位置、丸めの方法を定数で指定します。
テキストビューにセットする前にフィルターを挟むように使います。
静的メソッドとして定義しました。

public class RoundSelector {

    public static final int POINT_5 = 5,POINT_4 = 4,POINT_3 = 3,POINT_2 = 2,POINT_1 = 1,POINT_0 = 0;
    public static final int ROUND_F = 10,ROUND_CUT = 11,ROUND_UP = 12,ROUND_5_4 = 14;

    public static String conversion(double value, int point, int round){
        String string;

        BigDecimal bd = new BigDecimal(value);

        int mode = 0;
        switch (round){
            case ROUND_F:
                return bd.toString();
            case ROUND_CUT:
                mode = BigDecimal.ROUND_DOWN;
                break;
            case ROUND_UP:
                mode = BigDecimal.ROUND_UP;
                break;
            case ROUND_5_4:
                mode = BigDecimal.ROUND_HALF_UP;
                break;
        }

        BigDecimal bigDecimal = bd.setScale(point,mode);
        string = bigDecimal.toString();
        Log.d("hogehoge","bd =" + bigDecimal.toString() );
        
        return string;
    }

    public static String conversion(String value,int point,int round){
        return conversion(Double.parseDouble(value),point,round);
    }

    public static String formatBasic(String string){

        DecimalFormat df1 = new DecimalFormat("#,##0.############");
        return df1.format(Double.valueOf(string));

    }

}

メインアクティビティの、Calcからのコールバックで送られてくる文字列に対して、
RoundSelectorを適用します。

calc = new Calc();
calc.setListener(new Calc.Callback() {
            @Override
            public void onTextChanged(String string) {
                displayView.setText(string);
                Log.d("hoge", RoundSelector.conversion(string,0,RoundSelector.ROUND_CUT));
                Log.d("hoge", RoundSelector.conversion(string,3,RoundSelector.ROUND_CUT));
                Log.d("hoge", RoundSelector.conversion(string,5,RoundSelector.ROUND_5_4));
                Log.d("hoge", RoundSelector.conversion(string,3,RoundSelector.ROUND_F));
            }
        });

10÷6は、以下のような結果となります。

D/hoge: 1
D/hoge: 1.666
D/hoge: 1.66667
D/hoge: 1.6666666666666667406815349750104360282421112060546875

ただし、現段階ではコールバックで送られてくるテキスト全てに適用しているので、
ユーザーが少数を入力した際も丸められてしまいます。
テキストが、入力中なのか、計算の答えとして送られてきたものなのか、
判別する処理が必要です。
私の場合は、State.getStateValue()で得られる定数がSTATE_D(答えを表示している状態)
であれば数値の文字列を成型するように修正しましたが、
もっといいやり方もあると思うので、
独自の実装をしてみてください。


端数処理方法の設定として、

・小数点以下の桁数
・丸める方法

の定数を、SharedPreferencesに保存しておくと、
ユーザーの使用環境に合わせた、
利便性の高い電卓にすることができます。

【電卓アプリを作るvol.5】UIをレイアウトする

前回まで・・・
汎用性の高いCalcクラスを作りました
今回は、最低限のUIと電卓として機能するまでを行います。

UIをレイアウトします。
お好みのようにデザインしてください。
基本的には、電卓の液晶画面にあたる部分をTextViewで、
キーはButtonで配置しましたが、
キャンバスで描画する芸術的な電卓もありだと思います。

今後、押したら画像が変わる9パッチのボタン画像や、
レイアウトのサイズに合わせて文字列を自動で拡大・縮小する
ビューもご紹介しようと思いますが、
今回は最低限の作業を行っていきます。

基本的なレイアウトを作りました。
f:id:kentaro198477:20160925025608p:plain

Calcのインスタンス化と液晶表示

Calcクラス型のフィールドを定義します。
calcとしました。
メインアクティビティのオンクリエイト等で、
Calcをインスタンス化します。
Calcで定義したコールバックを受け取れるようにします。

calc = new Calc();
        calc.setListener(new Calc.Callback() {
            @Override
            public void onTextChanged(String string) {
                textView.setText(string);
            }
        });

onTextChangedの引数に入っている文字列が、
電卓の液晶画面に表示すべき文字列です。
まだ、doubleがそのまま文字列になっただけなので、
割り切れない数は、かなり桁数が多くなりますが、
今後、フォーマットして表示できるように改良します。

ボタン操作

ボタン全てにリスナーを付けます。
ボタンが押されたら、それに対応した定数を
calc.input()の引数に渡します。
[=]キーだったら、calc.input(Calc.KEY_EQUAL);という具合です。
数字キーは、ボタンテキストをそのままint型にして
calc.input(5);のようにします。
もし、和風な電卓で、表示が「壱、弐、参」などにしていたら、
それぞれにも定数を用意する必要があります。
Integer.parseInt("壱");ってできるのかな?(笑)

全てのボタンに適用したら、
何かボタンが押されるたびに、コールバックで文字列が送られてくるので、
それを表示すれば、電卓としては目的達成です!

電卓って、意外とボタンが多いことに気づきます(笑)
以下のように、IDで判断して処理を分けてもいいですが、
文字列で判定すると、ローカライズの際に面倒になる可能性があります。

public void onKeyClick(View v) {
    switch (v.getId()) {
            case R.id.b_00:
                calc.input(Calc.KEY_00);
                break;
            case R.id.b_dot:
                calc.input(Calc.KEY_DOT);
                break;
            case R.id.b_equal:
                calc.input(Calc.KEY_EQUAL);
                break;
            case R.id.b_tasu:
                calc.input(Calc.KEY_TASU);
                break;

      ・・・

次回は、”計算結果を丸め”て、会社の端数処理基準等に合わせた表示を可能にする、
小数点セレクター・ラウンドセレクターを実装していきます。

【電卓アプリを作る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();

    }