marunomaruno-memo

marunomaruno-memo

[Android] 15 パズル (3) - 経過時間(Chronometer)

2012年02月06日 | Android
                                                                    2012-02-06
[Android] 15 パズル (3) - 経過時間(Chronometer)
================================================================================

15 パズルに、パズルを解き始めてからの経過時間を表示する。
経過時間は、Chronometer クラスを使う。
経過時間測定の開始・終了は、Chronometer クラスの start()、stop() メソッドを使う。
また、表示は res/layout/main.xml で、Chronometer 要素を使う。

ベースになるプロジェクトは前回の
前回の「15 パズル (2) - カスタム・ボタン」
http://blog.goo.ne.jp/marunomarunogoo/d/20120204
である。

今回変更したクラスはつぎの 2 つ。
FifteenPuzzleActivity    アクティビティ。Chronometer の開始・終了を追加
FifteenPuzzleBoard       ボード。パズルの終了判定追加


■ アクティビティ

Chronometer の開始・終了の処理を追加する。
終了時は、アラート・ダイアログでその経過時間を表示する。

□ FifteenPuzzleActivity.java
---
package jp.marunomaruno.android.fifteenpuzzle;

import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;
import android.widget.Chronometer;

public class FifteenPuzzleActivity extends Activity {

    private FifteenPuzzleBoard board;    // 15パズルのボード
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        board = new FifteenPuzzleBoard(this);
    }

    /**
     * 数字ボタン・クリック時のハンドラー
     * @param v
     */
    public void onClickNumberButton(View v) {
        boolean isComplete = board.onClickNumberBlock((NumberButton) v); // (1)
        
        if (isComplete) {
            // 経過時間測定の終了
            Chronometer chronometer = (Chronometer) findViewById(R.id.chronometer); // (2)
            chronometer.stop();    // (3)

            System.out
                    .println(String.format(getString(R.string.finishMessage),
                            (SystemClock.elapsedRealtime() - chronometer
                                    .getBase()) / 1000));
            
            // 完成のダイアログを表示
            new AlertDialog.Builder(this)
                .setMessage(
                    String.format(getString(R.string.finishMessage),
                            (SystemClock.elapsedRealtime() - chronometer
                                    .getBase()) / 1000))    // (4)
                .setPositiveButton("OK", null)
                .show();
        }
    }

    /**
     * 開始ボタン・クリック時のハンドラー
     * @param v
     */
    public void onClickStartButton(View v) {
        board.reset();
        
        // 経過時間測定の開始
        Chronometer chronometer = (Chronometer) findViewById(R.id.chronometer);
        chronometer.setBase(SystemClock.elapsedRealtime());    // (5)
        chronometer.start();    // (6)
    }
}
---

(1) 数字ブロックをクリックしたときにパズルの完成も判断

    boolean isComplete = board.onClickNumberBlock((NumberButton) v); // (1)


(2) クロノメーターを取得する

    Chronometer chronometer = (Chronometer) findViewById(R.id.chronometer); // (2)


・Chronometer クラス

シンプルなタイマーを実装する View クラスのサブクラス。

java.lang.Object
   +   android.view.View
         +   android.widget.TextView
               +   android.widget.Chronometer

基本的な使い方は、setBase() で、現在時刻を設定し、ここからクロノメーターを 
start() で開始する。
やめるときは stop() でやめて、やめた時点の現在時刻から getBase() で開始時点の時
刻を引けば、実行していた時間がわかる。

なお、現在時刻は、SystemClock クラスを使う。


・メソッド
---
long   getBase()            基の時間を返す
void   setBase(long base)   カウントアップする基になる時間を設定する

String getFormat()          書式文字列を返す
void   setFormat(String format) 
       書式文字列を設定する。%s は、"MM:SS" または "H:MM:SS" 形式になる

Chronometer.OnChronometerTickListener     getOnChronometerTickListener() 
       クロノメーターの変更のリスナーを取得する
void   setOnChronometerTickListener(Chronometer.OnChronometerTickListener listener)
       クロノメーターの変更のリスナーを設定する
void   start()              開始する
void   stop()               停止する
---


・SystemClock クラス

間隔または経過時間の測定に使うシステム時計のクラス。

java.lang.Object
   +   android.os.SystemClock

メソッド
---
static long     currentThreadTimeMillis()    現在のスレッドの実行時間(ミリ秒)

static long     elapsedRealtime()    
                CPU停止時間なども含めたシステム・ブートからの時間(ミリ秒)

static boolean  setCurrentTimeMillis(long millis)    現在の時間(ミリ秒)

static void     sleep(long ms)    指定されたミリ秒スリープ(例外をスローしない)

static long     uptimeMillis()    
                CPU停止時間などを含めないシステム・ブートからの時間(ミリ秒)
---


(3) クロノメーターを停止する

    chronometer.stop();    // (3)


(4) 経過時間をダイアログで表示する

chronometer.setBase() を行ったのが、SystemClock.elapsedRealtime() なので、経過時
間は停止時点の SystemClock.elapsedRealtime() から 開始時点の 
SystemClock.elapsedRealtime() を引く。開始時点の SystemClock.elapsedRealtime() 
は、setBase() で、クロノメーター・オブジェクトに設定済み。

    new AlertDialog.Builder(this)
        .setMessage(
            String.format(getString(R.string.finishMessage),
                    (SystemClock.elapsedRealtime() - chronometer
                            .getBase()) / 1000))    // (4)
        .setPositiveButton("OK", null)
        .show();

(5)(6) クロノメーターを開始する

経過時間を後で取得するために、setBase() で、現在の時刻を設定しておく。

    chronometer.setBase(SystemClock.elapsedRealtime());    // (5)
    chronometer.start();    // (6)


■ ボード

数字ブロックが移動するたびに、パズルの完成を判定する。
判定のメソッドとして、isComplete() を設ける。

変更・追加は、つぎのメソッドのみ。
    public boolean onClickNumberBlock(NumberButton button)  完成すれば true を返す
    private boolean isComplete() 完成したかどうか(完成すれば true を返す)


□ FifteenPuzzleBoard.java
---
package jp.marunomaruno.android.fifteenpuzzle;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import jp.marunomaruno.android.util.ArrayCollectionUtil;
import jp.marunomaruno.android.util.SymmetricGroupUtil;
import android.app.Activity;
import android.content.Context;

public class FifteenPuzzleBoard {

    public Context context;
    
    public static final int ORDER = 4; // 4次の正方行列

    private NumberButton[] numberButtons;
    private int emptyBlockIndex = ORDER * ORDER;
    
    public FifteenPuzzleBoard(Context context) {
        this.context = context;

        Activity activity = (Activity) context;
        
        numberButtons = new NumberButton[] {
                null, // ボード上の番号と合わせるため、インデックス0をダミーとする
                (NumberButton) activity.findViewById(R.id.numberButton1),
                (NumberButton) activity.findViewById(R.id.numberButton2),
                (NumberButton) activity.findViewById(R.id.numberButton3),
                (NumberButton) activity.findViewById(R.id.numberButton4),
                (NumberButton) activity.findViewById(R.id.numberButton5),
                (NumberButton) activity.findViewById(R.id.numberButton6),
                (NumberButton) activity.findViewById(R.id.numberButton7),
                (NumberButton) activity.findViewById(R.id.numberButton8),
                (NumberButton) activity.findViewById(R.id.numberButton9),
                (NumberButton) activity.findViewById(R.id.numberButton10),
                (NumberButton) activity.findViewById(R.id.numberButton11),
                (NumberButton) activity.findViewById(R.id.numberButton12),
                (NumberButton) activity.findViewById(R.id.numberButton13),
                (NumberButton) activity.findViewById(R.id.numberButton14),
                (NumberButton) activity.findViewById(R.id.numberButton15),
                (NumberButton) activity.findViewById(R.id.numberButton16), 
            };

        // 最後のブロックを空とする
        numberButtons[emptyBlockIndex].setText(R.string.emptyText);

        log("initialize() ");
    }

    /**
     * 空ブロックが指定されたインデックスに入っているかどうか判定する。
     * @param indexes
     * @return 空ブロックが指定されたインデックスに入っていればtrue
     */
    private boolean isEmptyBlockIndexIn(int... indexes) {
        assert indexes.length > 0;

        return ArrayCollectionUtil.isKeyIn(emptyBlockIndex, indexes);
    }

    /**
     * 数字ボタン・クリック時のハンドラー
     * @param v
     * @return パズル終了時は true
     */
    public boolean onClickNumberBlock(NumberButton button) {
        // このブロックに対して、水平方向に有効な空ブロックの位置
        final int[][] HORIZONTAL_INDEXES = new int[][]{
                null,
                
                {2, 3, 4},
                {1, 3, 4},
                {1, 2, 4},
                {1, 2, 3},
                
                {6, 7, 8},
                {5, 7, 8},
                {5, 6, 8},
                {5, 6, 7},
                
                {10, 11, 12},
                {9, 11, 12},
                {9, 10, 12},
                {9, 10, 11},
                
                {14, 15, 16},
                {13, 15, 16},
                {13, 14, 16},
                {13, 14, 15},
        };

        // このブロックに対して、垂直方向に有効な空ブロックの位置
        final int[][] VERTICAL_INDEXES = new int[][]{
                null, 
                
                {5, 9, 13},
                {6, 10, 14},
                {7, 11, 15},
                {8, 12, 16},

                {1, 9, 13},
                {2, 10, 14},
                {3, 11, 15},
                {4, 12, 16},

                {1, 5, 13},
                {2, 6, 14},
                {3, 7, 15},
                {4, 8, 16},

                {1, 5, 9},
                {2, 6, 10},
                {3, 7, 11},
                {4, 8, 12},
        };

        System.out.printf("onClick() %s", button.toString());
        
        // 空の場合、何もしない
        if (isEmptyBlock(button)) {
            System.out.println(": empty");
            return false;    // (1)
        }
        
        int blockIndex = button.getBlockIndex();
        
        if (isEmptyBlockIndexIn(HORIZONTAL_INDEXES[blockIndex])) {    
            rotateHorizontal(blockIndex);

            System.out.println();    // 最後にlogを出すための改行
            return isComplete();    // (2)
        }

        if (isEmptyBlockIndexIn(VERTICAL_INDEXES[blockIndex])) {
            rotateVertical(blockIndex);
            
            System.out.println();    // 最後にlogを出すための改行
            return isComplete();
        }
        
        System.out.println();    // 最後にlogを出すための改行
        
        return false;
    }
    
    /**
     * パズルが完成かどうか
     * @return 完成のとき true
     */
    private boolean isComplete() {    // (3)
        // インデックス 1~15 の中に空ブロックがあればまだ完成していない
        for (int i = 1; i < numberButtons.length - 1; i++) {
            if (numberButtons[i].getText().toString().equals(
                    context.getString(R.string.emptyText))) {
                return false;
            }
        }

        // インデックス 1~15 の中で、数字の順番が逆なのがあればまだ完成していない
        for (int i = 1; i < numberButtons.length - 2; i++) {
            if (Integer.parseInt(numberButtons[i].getText().toString()) >= Integer
                    .parseInt(numberButtons[i + 1].getText().toString())) {
                return false;
            }
        }

        return true;
    }

    /**
     * 垂直にローテートする。
     * @param index 空ブロックを置く位置
     */
    private void rotateVertical(int index) {
        if (index < emptyBlockIndex) {
            rotateDown(index);
        } else {
            rotateUp(index);
        }            
    }

    /**
     * 水平にローテートする。
     * @param index 空ブロックを置く位置
     */
    private void rotateHorizontal(int index) {
        if (index < emptyBlockIndex) {
            rotateRight(index);
        } else {
            rotateLeft(index);
        }            
    }

    /**
     * 左にローテートする。
     * @param index 空ブロックを置く位置
     */
    private void rotateLeft(int index) {
        System.out.printf("rotateLeft() %d %d%n", index, emptyBlockIndex);
        rotateAscending(index, 1);
    }

    /**
     * 右にローテートする。
     * @param index 空ブロックを置く位置
     */
    private void rotateRight(int index) {
        System.out.printf("rotateRight() %d %d%n", index, emptyBlockIndex);
        rotateDescending(index, 1);
    }

    /**
     * 下にローテートする。
     * @param index 空ブロックを置く位置
     */
    private void rotateDown(int index) {
        System.out.printf("rotateDown() %d %d%n", index, emptyBlockIndex);
        rotateDescending(index, ORDER);
    }

    /**
     * 上にローテートする。
     * @param index 空ブロックを置く位置
     */
    private void rotateUp(int index) {
        System.out.printf("rotateUp() %d %d%n", index, emptyBlockIndex);
        rotateAscending(index, ORDER);
    }

    /**
     * 降順にローテートする。
     * @param index 空ブロックを置く位置
     */
    private void rotateDescending(int index, int step) {
        for (int i = emptyBlockIndex; i > index; i -= step) {
            numberButtons[i].setText(numberButtons[i - step].getText());
        }

        numberButtons[index].setText(R.string.emptyText);
        emptyBlockIndex = index;
    }


    /**
     * 昇順にローテートする。
     * @param index 空ブロックを置く位置
     */
    private void rotateAscending(int index, int step) {
        for (int i = emptyBlockIndex; i < index; i += step) {
            numberButtons[i].setText(numberButtons[i + step].getText());
        }

        numberButtons[index].setText(R.string.emptyText);
        emptyBlockIndex = index;
    }

    /**
     * このブロックが空かどうかを判断する。
     * @return 空の場合true
     */
    public boolean isEmptyBlock(NumberButton button) {
        return button.getBlockIndex() == emptyBlockIndex;
    }
    
    /**
     * ボードをリセットする
     */
    public void reset() {
        // 1~15 までの乱数を生成する
        List<Integer> numberList = new ArrayList<Integer>();
        numberList.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
                14, 15));
        
        int[] numberArray;
        do {
            Collections.shuffle(numberList);
            numberArray = ArrayCollectionUtil.toArray(numberList);
        } while (SymmetricGroupUtil.sgn(numberArray) != 1);    
                                        // 解けない問題(遇置換でない)なら作り直し

        if (Boolean.parseBoolean(context.getResources().getString(R.string.debug))) {
            System.out.println("DEBUG MODE");
            numberArray = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
                    14, 15};
        }
        
        // 数字ブロックを設定する
        for (int i = 1; i < numberArray.length + 1; i++) {
            numberButtons[i].setText(String.valueOf(numberArray[i - 1]));
        }

        // 空ブロックを設定する
        numberButtons[ORDER * ORDER].setText(R.string.emptyText);
        emptyBlockIndex = ORDER * ORDER;

        log("onClickStartButton() ");
    }
    
    private void log(String message) {
        System.out.println(message);
        System.out.print(toString());
    }

    @Override
    public String toString() {
        return String.format(
                "[%s %s %s %s]%n[%s %s %s %s]%n[%s %s %s %s]%n[%s %s %s %s]%n",
                numberButtons[1].getText(), numberButtons[2].getText(),
                numberButtons[3].getText(), numberButtons[4].getText(),
                numberButtons[5].getText(), numberButtons[6].getText(),
                numberButtons[7].getText(), numberButtons[8].getText(),
                numberButtons[7].getText(), numberButtons[10].getText(),
                numberButtons[11].getText(), numberButtons[12].getText(),
                numberButtons[13].getText(), numberButtons[14].getText(),
                numberButtons[15].getText(), numberButtons[16].getText());
    }
}
---

(1)(2) onClickNumberBlock の戻り

ブロックの移動がない場合は、単純に false を返す。

    return false;    // (1)

ブロックの移動があった場合は、完成かどうか判断して、その結果を返す。

    return isComplete();    // (2)


(3) パズルが完成かどうか

    private boolean isComplete() {    // (3)

ロジックは、単に、ブロック 1 ~ 15 の間に空ブロックがなくて、番号の昇順に並んで
いれば、パズル完成、としている。


■レイアウト

クロノメーター (Chronometer) 要素を追加した。それ以外は前回からの変更なし。

□ res/layout/main.xml
---
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:orientation="vertical" >

    <Button
        android:id="@+id/start_button"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="onClickStartButton"
        android:text="@string/start" />

    <TableLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center" >

        <TableRow
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" >

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton1"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="1"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton2"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="2"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton3"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="3"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton4"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="4"
                android:textAppearance="?android:attr/textAppearanceLarge" />
        </TableRow>

        <TableRow
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" >

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton5"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="5"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton6"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="6"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton7"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="7"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton8"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="8"
                android:textAppearance="?android:attr/textAppearanceLarge" />
        </TableRow>

        <TableRow
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" >

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton9"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="9"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton10"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="10"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton11"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="11"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton12"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="12"
                android:textAppearance="?android:attr/textAppearanceLarge" />
        </TableRow>

        <TableRow
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" >

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton13"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="13"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton14"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="14"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton15"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="15"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <jp.marunomaruno.android.fifteenpuzzle.NumberButton
                android:id="@+id/numberButton16"
                android:layout_width="0dip"
                android:layout_height="90px"
                android:layout_margin="1px"
                android:layout_weight="1"
                android:onClick="onClickNumberButton"
                android:text="@string/emptyText"
                android:textAppearance="?android:attr/textAppearanceLarge" />
        </TableRow>
    </TableLayout>

    <Chronometer
        android:id="@+id/chronometer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:format="@string/chronometerFormat" />    <!-- (1) -->

</LinearLayout>
---

(1) クロノメーター

表示形式は、res/values/strings.xml で指定。

    <Chronometer
        android:id="@+id/chronometer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:format="@string/chronometerFormat" />    <!-- (1) -->


■ データ

□ res/values/strings.xml
---
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="debug">true</string>
    <string name="app_name">15パズル</string>
    <string name="start">スタート</string>
    <string name="emptyText">★</string>
    <string name="chronometerFormat">経過時間 %s</string>
    <string name="finishMessage">%d 秒で完成しました。</string>
        
</resources>
---

                                                                            以上



最新の画像もっと見る

コメントを投稿