ラベル CountDownTimer の投稿を表示しています。 すべての投稿を表示
ラベル CountDownTimer の投稿を表示しています。 すべての投稿を表示

2015年3月11日水曜日

【Android】【CountDownTimer】 一時停止の仕組みを改善する その②

その①に引き続き、一時停止の仕組みについて検討します。

改善策② 残り時間(millisUntilFinished)を、インターバル毎に記録する。


millisUntilFinishedは、CountDownTimerクラスのonTick()メソッドで使用される変数で、残り時間(ミリ秒)の値が格納されています。これを、一時停止時に使用出来れば良いのですが、この変数はonTick()メソッドの範囲外では使えません。

そこで、あらかじめ定義した別の変数(countMillis)にmillisUntilFinishedの値をコピーする処理を、onTick()メソッドの中に追加します。これで、インターバル時間毎に、残り時間がcountMillisに記録されます。

public void onTick(long millisUntilFinished) {
   textView.setText(String.valueOf(millisUntilFinished / 1000)); // ミリ秒→秒に変換して)残り時間を表示
      countMillis = millisUntilFinished; // 残り時間をcountMillisに代入
}

カウントダウンタイマーを再スタートする際に、countMillisを残り時間(millisInFuture)にセットしてインスタンス化すれば、ミリ秒を切り捨てずに残り時間からスタートできます。(ただし、正確性は残り時間(millisUntilFinished)を記録する頻度、つまりインターバル時間にも依存します。)

// トグルON
if (isChecked) {
   myCountDownTimer = new MyCountDownTimer(countMillis, 100); //countMillisを残り時間にセット
      myCountDownTimer.start(); // タイマーをスタート
// トグルOFF
} else {
   myCountDownTimer.cancel(); // タイマーをストップ
}

なお、countMillisは、変数を定義する際に忘れずに初期化(初期値をセット)しておきます。そうしなければ、最初にタイマーをスタートする際に、countMillisには値が何も入っていないのでエラーに成ってしまいます。
また、リセットの処理にも、countMillisに初期値をセットする処理を追加します。


思いついた処理は昨日の対応策①と合わせてこの2つです。
個人的には今回紹介した②の方が、強引さが無くて好きです。

参考までに、Javaのコード全文を掲載します。
(XMLは、『【Android】【CountDownTimer】カウントダウンタイマーを、1つのボタンで開始、一時停止、リセットする。』と同じ)


import android.os.Bundle;
import android.os.CountDownTimer;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.TextView;
import android.widget.ToggleButton;

public class MainActivity extends ActionBarActivity {

    private static MyCountDownTimer myCountDownTimer;
    private static long countMillis = 15000; // カウントダウンの残り時間(初期値)

    // CountDownTimerクラスを継承して、MyCountDownTimerを定義
    class MyCountDownTimer extends CountDownTimer {

        // 秒を表示するテキストビュー、トグルボタンのビューを取得
        TextView textView = (TextView)findViewById(R.id.textView);
        ToggleButton toggleButton = (ToggleButton)findViewById(R.id.toggleButton);

        public MyCountDownTimer(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }

        // カウントダウン処理
        @Override
        public void onTick(long millisUntilFinished) {
            textView.setText(String.valueOf(millisUntilFinished / 1000)); // ミリ秒→秒に変換して)残り時間を表示
            countMillis = millisUntilFinished; // 残り時間をcountMillisに代入
        }

        // カウントダウン終了後の処理
        @Override
        public void onFinish() {
            toggleButton.setChecked(false); // toggleボタンをオフにする
            textView.setText("0");
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // トグルボタンをタップした時の処理
        ((ToggleButton)findViewById(R.id.toggleButton)).setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

                // トグルON
                if (isChecked) {
                    myCountDownTimer = new MyCountDownTimer(countMillis, 100); //countMillisを残り時間にセット

                    myCountDownTimer.start(); // タイマーをスタート

                    // トグルOFF
                } else {
                    myCountDownTimer.cancel(); // タイマーをストップ
                }
            }
        });

        // トグルボタンをロングタップした時の処理
        ((ToggleButton)findViewById(R.id.toggleButton)).setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                myCountDownTimer.cancel(); // タイマーをストップ
                countMillis = 15000; // カウントダウン時間を初期値にリセット
                ((TextView)findViewById(R.id.textView)).setText(String.valueOf(countMillis / 1000)); // テキストビューに初期値をセット
                ((ToggleButton)findViewById(R.id.toggleButton)).setChecked(false); // toggleボタンをオフにする
                return true;
            }
        });
    }
}

(関連)
【Android】【CountDownTimer】カウントダウンタイマーを、1つのボタンで開始、一時停止、リセットする。
【Android】【CountDownTimer】 0秒まで表示する (インターバルの挙動を調べてみる)
【Android】【CountDownTimer】 一時停止の仕組みを改善する その①

2015年3月10日火曜日

【Android】【CountDownTimer】 一時停止の仕組みを改善する その①

先日の投稿で作成したタイマーアプリですが、一時停止の仕組みは以下のようになっています。

① [スタート] 初期値(ミリ秒)をセットして、CountDownTimer をインスタンス化 ⇒ スタート。
② [動作中] onTick毎にtextViewに残り時間を表示。この際、「秒」に換算して表示。
③ [一時停止] CountDownTimer をキャンセル。
④ [再スタート] textViewに表示されている時間(秒)をミリ秒に換算し、新たにCountDownTimer
   をインスタンス化 ⇒ スタート

秒 ⇔ ミリ秒 の変換は、1000 を 乗 or 除算しています。

ここで問題になるのは、一時停止の際に、 1秒以下の値が切り捨てられることです。

より具体的に説明すると、

残り時間(millisUntilFuture) 5999 ミリ秒の時、残り時間は「秒」に換算(1000で除算)されて、画面には 5 と表示されます。

この時一時停止 → 再スタートすると、新たなCountDownTimerは、画面に表示されている値 5 に 1000 を掛けて、残り時間 5000ミリ秒 でスタートします。

つまり、一時停止 → 再スタートで、最大約1秒の程度の切り捨てが発生してしまうのです。

用途にもよりますが、これはイマイチだなと思い、一時停止の仕組みを改善する事にしました。

改善策① 画面表示する単位を変更する。


大変単純な案です。切り捨ての原因になる単位の変換をしない、またはする範囲を狭めます。

例えば、ミリ秒まで画面に表示するようにすると、先ほどの5999はそのままの値で画面に記録され、そのままの値で新たな CountDownTimer に利用できます。

ミリ秒まで使わなくても、一桁、二桁でも1秒以下の単位を増やすと、その分精度が上がります。

例えば、画面上の表示を [ 5.86 ] みたいに、下2桁まで表示してストップウォッチ風にするというのも面白そうです。

どうしても秒単位で表示にしたい場合は、.86 のテキストビューだけ表示を透明にすれば良いのではないでしょうか。(ちょっと場当たり的な感じがしますが。)

ストップウォッチ風の表示にするため、以下の様にコードを変更しました。



activity_main.xml


 ・ 1/10, 1/100秒を表示するためのテキストビュー(textView2)を追加
 ・ 区切りの“.”を表示するテキストビュー(textView3)を追加
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="15"
        android:id="@+id/textView"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="00"
        android:id="@+id/textView2"
        android:layout_alignTop="@+id/textView"
        android:layout_toRightOf="@+id/textView3" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="."
        android:id="@+id/textView3"
        android:layout_centerVertical="true"
        android:layout_alignTop="@+id/textView"
        android:layout_toRightOf="@+id/textView" />

    <ToggleButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="New ToggleButton"
        android:id="@+id/toggleButton"
        android:layout_below="@+id/textView"
        android:layout_centerHorizontal="true" />
</RelativeLayout>

MainActivity.java


 ・ textView2に、1/10, 1/100秒の桁を表示する処理を追加
  ⇒ カウントダウンタイマーのonTick()メソッドに、次の処理を追加

    textView2.setText(String.valueOf((millisUntilFinished % 1000) / 10));

    millisUntilFinished % 1000
    ⇒ millisUntilFinished(残り時間) % 1000 を秒(1000ミリ秒)で割った余り。
       秒の桁を捨てます。
    (例) 5678 % 1000 = 678 (ミリ秒)

    さらに10で割ることで、1/1000 秒の桁を捨てています。
    (例) 678 / 10 = 67 (10ミリ秒)

 ・ カウントダウンタイマーをスタートする際、textView2からも値を取得して、残り時間をセットする
    処理を追加

  String time1 = ((TextView) findViewById(R.id.textView)).getText().toString();
  String time2 = ((TextView) findViewById(R.id.textView2)).getText().toString();
  myCountDownTimer = new MyCountDownTimer(Integer.parseInt(time1) * 1000
   + Integer.parseInt(time2) * 10, 10);

  ⇒ time1、time2 の値をミリ秒に戻して、合計した値を残り時間にセットしています。

 ・ インターバル時間を10ミリ秒に設定
  ⇒ここの値が大きいと、textView2の表示がガックガックになります。

  なお、先日投稿した様に、インターバルの処理には10~20ミリ秒の誤差があります。
  したがって、(早すぎて見えませんが)1/100 秒の桁はところどころ数字が抜けていると
  思われます。ストップウォッチの精度としては、1/10 秒までが限界かと思います。

 ・ タイマー終了時に、textView1、textView2にそれぞれ"0", "00" をセット

import android.os.Bundle;
import android.os.CountDownTimer;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.TextView;
import android.widget.ToggleButton;

public class MainActivity extends ActionBarActivity {

    static MyCountDownTimer myCountDownTimer;

    // CountDownTimerクラスを継承して、MyCountDownTimerを定義
    class MyCountDownTimer extends CountDownTimer {

        TextView textView = (TextView)findViewById(R.id.textView);
        TextView textView2 = (TextView)findViewById(R.id.textView2);
        ToggleButton toggleButton = (ToggleButton)findViewById(R.id.toggleButton);

        public MyCountDownTimer(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }

        // カウントダウン処理
        @Override
        public void onTick(long millisUntilFinished) {
            textView.setText(String.valueOf(millisUntilFinished / 1000)); // 秒の桁を表示
            textView2.setText(String.valueOf((millisUntilFinished % 1000) / 10)); // 10,100分の1秒の桁を表示
        }

        // カウントダウン終了後の処理
        @Override
        public void onFinish() {
            toggleButton.setChecked(false); // toggleボタンをオフにする
            ((TextView)findViewById(R.id.textView)).setText("0");
            ((TextView)findViewById(R.id.textView2)).setText("00"); // テキストビューに00を表示
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // トグルボタンをタップした時の処理
        ((ToggleButton)findViewById(R.id.toggleButton)).setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

                // トグルON
                if (isChecked) {
                    String time1 = ((TextView) findViewById(R.id.textView)).getText().toString(); // textViewの文字列を取得して、timeに格納
                    String time2 = ((TextView) findViewById(R.id.textView2)).getText().toString(); // textView2の文字列を取得して、timeに格納
                    myCountDownTimer = new MyCountDownTimer(Integer.parseInt(time1) * 1000 + Integer.parseInt(time2) * 10, 10); // time1,time2をミリ秒換算して合計し,その値をセットしたmyCountDownTimerをインスタンス化
                    myCountDownTimer.start(); // タイマーをスタート

                    // トグルOFF
                } else {
                    myCountDownTimer.cancel(); // タイマーをストップ
                }
            }
        });

        // トグルボタンをロングタップした時の処理
        ((ToggleButton)findViewById(R.id.toggleButton)).setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                myCountDownTimer.cancel(); // タイマーをストップ
                ((TextView)findViewById(R.id.textView)).setText("15"); // テキストビューに初期値をセット
                ((TextView)findViewById(R.id.textView2)).setText("00"); // テキストビューに初期値をセット
                ((ToggleButton)findViewById(R.id.toggleButton)).setChecked(false); // toggleボタンをオフにする
                return true;
            }
        });
    }
}
(関連)
【Android】【CountDownTimer】カウントダウンタイマーを、1つのボタンで開始、一時停止、リセットする。
【Android】【CountDownTimer】 0秒まで表示する (インターバルの挙動を調べてみる)
【Android】【CountDownTimer】 一時停止の仕組みを改善する その②

【Android】【CountDownTimer】 0秒まで表示する (インターバルの挙動を調べてみる)

先日、CountDownTimer を用いたプログラムをつくってみました。

CountDownTimer クラスの基本的な使い方としては

CountDownTimer myCountDownTimer
               = new CowntDownTimer(long millisInFuture, long countDownInterval)

上記の様にインスタンス化の際に、カウントダウン時間(millisInFuture)、インターバル時間(countDownInterval)をミリ秒で渡します。

インターバル時間は、onTick()メソッドに記載した処理が行われる間隔になります。

秒単位のタイマーであれば、残り時間の表示を1秒に1回書き換えればいいので、インターバル時間は、1秒 = 1000ミリ秒の指定で問題ないように思われます。

しかし、実際に1000ミリ秒を指定すると、タイマー終了時の時間表示が“1”で止まってしまいます。

何故なのか解明するため、ログをとってみました。


ログの取り方

onTick()メソッド内に、下記の行を追加します。
onTick()毎に、残り時間がログに出力されます。

Log.i("MainActivity", "残り時間 = " + String.valueOf(millisUntilFinished));

インターバル時間を、1000, 100, 10, 1 と変えて、試してみました。

① 1000


② 100


③ 10


④ 1



これらから、以下の様に推測しました。

  • インターバルは、ミリ秒単位で正確に刻まれている訳ではなく、誤差がある。(1000, 100 ミリ秒インターバルでは、1回毎に10ミリ秒程度、10, 1 ミリ秒インターバルでは、1回毎に10~20ミリ秒程度)
  • 最後の1回の処理は飛ばされている?(④の1ミリ秒を除けば、もう1回処理されても良さそうな時間が残っているのに、されていない。)

これによって起こりそうな問題は、以下の2つが考えられます。


① 誤差による処理の抜け落ち

例えば、秒単位で時間を表示する場合、インターバルが1000ミリ秒だと、
1005 → 895 のようにonTickが刻まれてしまう可能性があり、その際は秒数の表示が 10 → 8 となり、9秒が抜け落ちる。

② 最後の秒(0秒)が表示されない

冒頭の通り。

これを防ぐためには、


 → 誤差を考慮に入れて、短めのインターバル時間を設定する。
 → これも考慮に入れて、さらに短めのインターバル時間を表示する...というのも手ですが、最後に表示する値は、onFinish()メソッドに記載する、とした方が、ムダが無く確実な気がします。

先日のカウントダウンタイマーの場合、以下の様にonFinish()内に、textViewに“0”を表示する処理を追記しました。
 
    // カウントダウン終了後の処理
        @Override
        public void onFinish() {
            toggleButton.setChecked(false); // toggleボタンをオフにする
            textView.setText("0"); // 0を表示
        }



2015年3月1日日曜日

【Android】【CountDownTimer】カウントダウンタイマーを、1つのボタンで開始、一時停止、リセットする。

現在タイマーを使ったアプリを作成している。

CountDownTimer というクラスが用意されているので、それを使えばタイマー作成には結構便利。

CountDownTimer は、そのままだと一時停止が使えないので、一時停止するにはちょっと工夫が必要だ。そのやり方については、こちらの記事を参考にさせて頂いた。

ただ、参考元のやり方そのままだと、ボタンが増えて画面がちょっと煩雑になってしまう。

ストップウォッチ感覚で直感的に操作できるようにしたいので、スタート、ストップは同じボタンにしたい。

Androidには標準で『トグルボタン』という、ON/OFFを切り替えられる便利なボタンがあるので、それを利用してタイマーを自分なりに再作成してみた。



トグルボタンは、AndroidStudioのパレットからドラッグ・アンド・ドロップでXML上に配置できる。


配置したトグルボタンは、JavaでOnCheckedChangeListener に関連付け、下記の様に、if (isChecked) - else で分けて、ON/OFF時の処理をそれぞれ記載する。

        toggleButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

                
                if (isChecked) {

                    // トグルボタンがONの際の処理を記述
                    
                } else {

                    // トグルボタンがOFFの際の処理を記述

                }
            }
        });

今回のタイマーでは、

[ON] タイマーをインスタンス化 → スタート
[OFF] タイマーをストップ

という処理を記述した。

ついでに setOnLongClickListener で、トグルボタンをロングクリックした時にタイマーをリセットするように設定した。
ロングクリックでリセットする際には、トグルボタンをOFFに変更する。そうしないと、タイマーはリセットされて動いていないのに、トグルがONのままという状況が発生してしまう。

以下、ソースコード全体

MainActivity.java
import android.os.CountDownTimer;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.TextView;
import android.widget.ToggleButton;

public class MainActivity extends ActionBarActivity {

    static MyCountDownTimer myCountDownTimer;

    // CountDownTimerクラスを継承して、MyCountDownTimerを定義
    class MyCountDownTimer extends CountDownTimer {

        TextView textView = (TextView)findViewById(R.id.textView);
        ToggleButton toggleButton = (ToggleButton)findViewById(R.id.toggleButton);

        public MyCountDownTimer(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }

        // カウントダウン処理
        @Override
        public void onTick(long millisUntilFinished) {
            textView.setText(String.valueOf(millisUntilFinished / 1000)); // ミリ秒 → 秒に変換して)残り時間を表示
        }

        // カウントダウン終了後の処理
        @Override
        public void onFinish() {
            toggleButton.setChecked(false); // toggleボタンをオフにする
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // トグルボタンをタップした時の処理
        ((ToggleButton)findViewById(R.id.toggleButton)).setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

                // トグルON
                if (isChecked) {
                    String time = ((TextView) findViewById(R.id.textView)).getText().toString(); // テキストビューの文字列を取得して、timeに格納
                    myCountDownTimer = new MyCountDownTimer(Integer.parseInt(time) * 1000, 100); // timeをint型に変換した後、ミリ秒に変換。その時間をセットしたmyCountDownTimerをインスタンス化
                    myCountDownTimer.start(); // タイマーをスタート

                    // トグルOFF
                } else {
                    myCountDownTimer.cancel(); // タイマーをストップ
                }
            }
        });

        // トグルボタンをロングタップした時の処理
        ((ToggleButton)findViewById(R.id.toggleButton)).setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                myCountDownTimer.cancel(); // タイマーをストップ
                ((TextView)findViewById(R.id.textView)).setText("15"); // テキストビューに初期値をセット
                ((ToggleButton)findViewById(R.id.toggleButton)).setChecked(false); // toggleボタンをオフにする
                return true;
            }
        });
    }
}

activity_main.xml
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="15"
        android:id="@+id/textView"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />

    <ToggleButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="New ToggleButton"
        android:id="@+id/toggleButton"
        android:layout_below="@+id/textView"
        android:layout_centerHorizontal="true" />
</RelativeLayout>

如何でしょうか。


(関連)
2015/3/10 【Android】【CountDownTimer】 0秒まで表示する (インターバルの挙動を調べてみる)
2015/3/10 【Android】【CountDownTimer】 一時停止の仕組みを改善する その①
2015/3/11 【Android】【CountDownTimer】 一時停止の仕組みを改善する その②