Androidアプリ開発奮闘記 漢字ゲームを作る①

こんにちはKoheiです。
最近、Androidアプリ開発の勉強をしてます。とりあえず、以下の参考書を一通りやってみて、本を見ながら簡単なアプリが作れるようになってきました。
さらに理解を深めるには、色んなアプリを自分で作ってみるのが一番ですよね。ということで、今回は、僕の子供が漢字が苦手なので、
自分自身のAndroid開発のノウハウ整理のために、制作のノウハウ等をこのブログにまとめていくことにします。
目次
1.仕様
1-1.ゲーム完成形の仕様
まずは、アプリ制作に入る前に、漢字ゲームの仕様をざっくり決めました。
最初は、漢字の読みを答えていくゲームにしようかと思いましたが、子供が漢字の書き取りが苦手だということで、書き取りゲームになりました。(個人的に制作の難易度がかなり上がりました。。)
画面は、トップ画面と文字入力画面の2画面構成です。

文字入力画面がゲームのメイン画面になります。

簡単に仕様を説明すると、①のエリアに問題が表示されるので、②の手書きエリアに漢字を入力し、完了ボタンを押すと、文字判定処理が動き、正解の場合は、お金(報酬)がもらえるというゲームです。
あとは、手書きの文字は、「戻る」、「進む」、「クリア」ボタンでやり直しができるようにしました。
また、「スキップ」ボタンを押すと、次の問題へ移るようになっています。
1-2.今回の目標
完成形まで一気に作ると大変なので少しずつアプリを完成させることにしました。
本記事では、まずは文字ゲームの基本部分である、トップ画面から文字入力画面への遷移部分と文字入力画面の基本部分を作ることにします。

2.レイアウトファイル、アクティビティクラスについて
開発に入るまえに、まずは、Androidアプリ開発で重要なレイアウトファイルとクラス構成について説明します。
Androidアプリ開発では、画面構成を記載するレイアウトファイル(.xmlファイル)とその画面処理を記載するアクティビティクラス(Javaクラス)があります。
画面ごとにレイアウトファイルとアクティビティクラスを作っていくイメージです。

今回、ゲームのトップ画面と文字入力画面2つの画面を作ったので、以下のようなレイアウトファイルとアクティビティクラスの構成になっています。

3.Androidアプリ開発の流れ
Androidアプリ開発のパターンとしては以下のような流れで作ります。
開発ツールは、AndroidStudioを使います。※AndroidStudioのインストール方法などは今回は省略します。(ネットで調べればいっぱい紹介されていますので、ググってみてください)
1.レイアウトファイル(.xml)で画面レイアウトを作成する
2.アクティビティクラスなどのJavaファイルに処理を記述する
3.アプリを起動して動作確認
それでは、早速順に作っていきたいと思います。
4.レイアウトを作る
4-1.トップ画面のレイアウト

まずは、トップ画面用のレイアウトから作ります。
まずは、AndroidStudioから新規プロジェクトを作成し、空のActivityを作りましょう。(僕は、MainActivityという名前にしました)
activity_main.xmlを開き、textViewとButtonを追加します。
AndroidStudioは、とても便利で、Designタブからボタンやテキストビューを直接ドラッグすることで、直感的にパーツを配置できます。
まずは、activity_main.xmlを選択し、[Design]タブを選択します。

パーツ(Button,TextView)を好みの位置に配置したら、「Infer Constraints」を押して制約をつけます。(レイアウト上に固定する)

あとは、Attributes画面から、パーツのIDやtextを入力しましょう。

・Button ID:button_start/ text:ゲームをはじめる
・TextView ID:textView/ text:かんじゲーム
[Text]タブからは、直接Xmlでコードを書いてパーツを配置できます。
一応、私が作ったレイアウトのコードを以下に載せてます。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.inomacreate.mojigame.MainActivity">
<Button
android:id="@+id/button_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ゲームをはじめる"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="かんじゲーム"
android:textSize="30sp"
app:layout_constraintBottom_toTopOf="@+id/button_start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
4-2.文字入力画面のレイアウトを作る

次に、文字入力画面レイアウトを作りましょう。javaフォルダを選択した状態で、右クリック→New→Activity→Empty Activity(空のActivity)を選択します。

名称は、DrawActivityとしました。

先程のトップ画面と同じように、activity_draw.xmlを開き、以下のようにパーツを追加しましょう。※文字入力のエリアは、SurfaceViewを配置します。

・Button ID:button_undo/ text:戻る
・Button ID:button_redo/ text:進む
・Button ID:button_clear/ text:クリア
・SurfaceView ID:surfaceView
一応、私が作ったレイアウトのコードを以下に載せてます。
[Text]タブから、直接Xmlでコードを記載するときに参考にしてください。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/draw_activity"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.inomacreate.mojigame1.DrawActivity">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="60dp"
android:layout_marginEnd="32dp"
android:layout_marginStart="32dp"
android:layout_marginTop="100dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.611" />
<Button
android:id="@+id/button_undo"
android:layout_width="0dp"
android:layout_height="45dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="戻る"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/button_redo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/surfaceView"
app:layout_constraintVertical_bias="1.0" />
<Button
android:id="@+id/button_redo"
android:layout_width="0dp"
android:layout_height="45dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="2dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="進む"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/button_clear"
app:layout_constraintStart_toEndOf="@+id/button_undo"
app:layout_constraintTop_toBottomOf="@+id/surfaceView"
app:layout_constraintVertical_bias="1.0" />
<Button
android:id="@+id/button_clear"
android:layout_width="0dp"
android:layout_height="45dp"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="クリア"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/button_redo"
app:layout_constraintTop_toBottomOf="@+id/surfaceView"
app:layout_constraintVertical_bias="1.0" />
</android.support.constraint.ConstraintLayout>
これで、とりあえず、2つの画面のレイアウトが完成しました。
次は、アクティビティに処理を実装していきます。
5.トップ画面の処理を実装する

5-1.ボタン押下処理
ボタンが押されたというイベントを検出するためには、
MainActivity.javaに以下の処理を実装します。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button startbt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Buttonリスナー登録
startbt = (Button)findViewById(R.id.button_start);
startbt.setOnClickListener(MainActivity.this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.button_start:
break;
default:
break;
}
}
}
まず、1行目で、View.OnClickListenerインターフェースを実装宣言をします。
15行目〜25行目で、onClickメソッドを実装します。
また、onCreateメソッド内に、リスナー登録します。(10行目〜12行目)
これで、「ゲームをはじめる」ボタンを押すと、onClickメソッドがコールされ、switch文の[R.id.button_start]処理に飛んできます。
5-2.画面遷移
次に「ゲームをはじめる」ボタンを押すと、ゲーム画面に遷移させます。
別のActivityを起動するには、Intentクラスを使います。具体的には以下のようにやります。
1.Intentクラスをnewする
2.起動先のアクティビティへ渡すデータを設定する
3.アクティビティを起動する
今回、特に渡すデータはないので、1,2の処理だけ入れます。
onClickメソッドに以下の処理を実装します。
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.button_start:
// Start ボタン押下→DrawActivity起動
Intent intent = new Intent(MainActivity.this,DrawActivity.class);
startActivity(intent);
break;
default:
break;
}
}
・Intentクラスをnewして、startActivityを実行することで、DrawActivityへ遷移させています。
6.文字入力画面の処理を実装する

6-1.surfaceViewで文字入力
今回手書きの文字入力処理は、surfaceViewを使います。
[Androidアプリ開発]タッチでお絵かきしてみた
DrawActivity.javaを開き、以下を追加しましょう。
public class DrawActivity extends AppCompatActivity {
private SurfaceView mSurfaceView;
private SurfaceHolder mHolder;
private Paint mPaint;
private Path mPath;
private Bitmap mLastDrawBitmap;
private Canvas mLastDrawCanvas;
private int offset_x = 0;
private int offset_y = 0;
private final SurfaceHolder.Callback mCallBack = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
clearLastDrawBitmap();
// 描画位置オフセットの算出
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mSurfaceView.getLayoutParams();
Rect rect = new Rect();
Window window = getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rect);
int statusBarHeight = rect.top;
int contentViewTop = window.findViewById(Window.ID_ANDROID_CONTENT).getTop();
int titleBarHeight= contentViewTop - statusBarHeight;
offset_x = lp.getMarginStart();
offset_y = lp.topMargin + statusBarHeight + titleBarHeight+40;
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
mLastDrawBitmap.recycle();
}
};
private void clearLastDrawBitmap() {
if (mLastDrawBitmap == null) {
mLastDrawBitmap = Bitmap.createBitmap(mSurfaceView.getWidth(), mSurfaceView.getHeight(),
Bitmap.Config.ARGB_8888);
}
if (mLastDrawCanvas == null) {
mLastDrawCanvas = new Canvas(mLastDrawBitmap);
}
mLastDrawCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float touchedX = event.getX() - offset_x;
float touchedY = event.getY() - offset_y;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
onTouchDown(touchedX, touchedY);
break;
case MotionEvent.ACTION_MOVE:
onTouchMove(touchedX, touchedY);
break;
case MotionEvent.ACTION_UP:
onTouchUp(touchedX, touchedY);
break;
default:
}
return true;
}
private void onTouchDown(float x, float y) {
mPath = new Path();
mPath.moveTo(x, y);
}
private void onTouchMove(float x, float y) {
mPath.lineTo(x, y);
drawLine(mPath);
}
private void onTouchUp(float x, float y) {
mPath.lineTo(x, y);
drawLine(mPath);
mLastDrawCanvas.drawPath(mPath, mPaint);
mUndoStack.addLast(mPath);
mRedoStack.clear();
}
private void drawLine(Path path) {
// ロックしてキャンバスを取得します。
Canvas canvas = mHolder.lockCanvas();
// キャンバスをクリアします。
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
// 前回描画したビットマップをキャンバスに描画します。
canvas.drawBitmap(mLastDrawBitmap, 0, 0, null);
// パスを描画します。
canvas.drawPath(path, mPaint);
// ロックを外します。
mHolder.unlockCanvasAndPost(canvas);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_draw);
// SurfaceView取得
mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView);
// SurfaceHolder取得
mHolder = mSurfaceView.getHolder();
// コールバックを設定
mHolder.addCallback(mCallBack);
// ペイントを設定
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(30);
}
}
・onCreateメソッド内で、SurfaceViewの初期設定を実装しています。134行目は手書き線の太さで30に設定しています。(119行目〜134行目)
・SurfaceHolderのコールバックメソッドを実装しています。surfaceCreatedは、surfaceViewの描画準備が整ったらコールされます。その中で、描画位置のオフセットを計算しています。(11行目〜38行目)
・onTouchEventメソッドで実際に文字エリア内でタッチされたときの描画処理等を実装しています。(56行目〜78行目)
6-2.文字操作ボタン処理追加
続いて、文字操作系のボタン(戻る、進む、クリア)処理を追加します。
DrawActivity.javaを開き、以下を追加しましょう。
public class DrawActivity extends AppCompatActivity implements View.OnClickListener{
private SurfaceView mSurfaceView;
private SurfaceHolder mHolder;
private Paint mPaint;
private Path mPath;
private Bitmap mLastDrawBitmap;
private Canvas mLastDrawCanvas;
private int offset_x = 0;
private int offset_y = 0;
private Button undobt;
private Button redobt;
private Button clearbt;
private Deque<Path> mUndoStack = new ArrayDeque<Path>();
private Deque<Path> mRedoStack = new ArrayDeque<Path>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_draw);
// ボタンリスナ登録
undobt = (Button) findViewById(R.id.button_undo);
undobt.setOnClickListener(this);
redobt = (Button) findViewById(R.id.button_redo);
redobt.setOnClickListener(this);
clearbt = (Button) findViewById(R.id.button_clear);
clearbt.setOnClickListener(this);
// SurfaceView取得
mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView);
// SurfaceHolder取得
mHolder = mSurfaceView.getHolder();
// コールバックを設定
mHolder.addCallback(mCallBack);
// ペイントを設定
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(30);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.button_undo:
undo();
break;
case R.id.button_redo:
redo();
break;
case R.id.button_clear:
reset();
break;
default:
break;
}
}
public void undo() {
if (mUndoStack.isEmpty()) {
return;
}
// undoスタックからパスを取り出し、redoスタックに格納します。
Path lastUndoPath = mUndoStack.removeLast();
mRedoStack.addLast(lastUndoPath);
// ロックしてキャンバスを取得します。
Canvas canvas = mHolder.lockCanvas();
// キャンバスをクリアします。
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
// 描画状態を保持するBitmapをクリアします。
clearLastDrawBitmap();
// パスを描画します。
for (Path path : mUndoStack) {
canvas.drawPath(path, mPaint);
mLastDrawCanvas.drawPath(path, mPaint);
}
// ロックを外します。
mHolder.unlockCanvasAndPost(canvas);
}
public void redo() {
if (mRedoStack.isEmpty()) {
return;
}
// redoスタックからパスを取り出し、undoスタックに格納します。
Path lastRedoPath = mRedoStack.removeLast();
mUndoStack.addLast(lastRedoPath);
// パスを描画します。
drawLine(lastRedoPath);
mLastDrawCanvas.drawPath(lastRedoPath, mPaint);
}
public void reset() {
mUndoStack.clear();
mRedoStack.clear();
clearLastDrawBitmap();
Canvas canvas = mHolder.lockCanvas();
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
mHolder.unlockCanvasAndPost(canvas);
}
}
・1行目で、View.OnClickListenerインターフェースを実装宣言をします。
・onCreateメソッド内で、各種ボタンのリスナーを登録しています。(24行目〜30行目)
・「戻る」、「進む」、「クリア」キーをクリックすると、onClickメソッドが実行され、それぞれ処理が実行されます。(52行目〜126行目)
7.動作確認
さて、ここまで実装できたら、早速動作確認してみましょう。
Androidの実機を使って動作確認してもいいのですが、せっかくAndroidStudioを使ってるので、エミュレータを使ってみます。
まずは、AndroidStudioの上部にある三角マーク(Run)をクリックします。

ここで、「Create New Virtual Device」をクリックすると、仮想デバイスの種類等を選択する画面になりますので、デバッグしたい機種を選択していってください。

私は、すでに登録済みなので、Nexus5Xを選択し、OKを押します。
特にビルドエラー等がなければ、以下のようにエミュレーター上で作成したアプリを動作させることができます。

無事に文字入力まで動作させることができました!
8.最後に
以上、今回は、漢字ゲームの基本部分を作成しました。
次回はゲーム完成に向けて、入力した文字の判定処理などを実装していこうと思います。
少しでも参考になるところがあれば幸いです。
それでは!

