【初心者】センサーを使ってボールを転がすandroidアプリ【Java】

# Udemyのandroid開発コースが速過ぎてですね

https://www.udemy.com/androidjavapro/

Java知識ゼロOK！プロのAndroid開発者になるためのマスターコース
【最新Android Studio 3に対応！】分かりやすく丁寧に解説！豊富なサンプルを通して、Javaの基礎・Android技術者として必要な知識・リストビュー・データベースの作成・魅力的なデザインをマスターしよう！
3.9 (182件の評価)
1449人の受講生が登録
作成者 金田浩明 (Hiroaki Kaneda)
最終更新 1/2019


このコースを受講していて、ペースが速すぎるのと、
ここ詳しい解説がないとわからないなという部分がいくつかあったので、
コメントを付け加えて自分が分かるようにしたものを掲載しておきます。

```MainActivity.java:
package com.example.accball;

import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/* イベントリスナーはイベントを監視し続け、イベントがあったらそれを受け取る */
public class MainActivity extends AppCompatActivity implements SensorEventListener, SurfaceHolder.Callback {

    // メンバ変数の宣言(=先頭に小文字のmをつけよう)
    SensorManager mSensorManager;
    Sensor mAccSensor;

    // SurfaceHolderクラスのmHolderというメンバ変数の宣言
    // 一時的に画面を格納するためのホルダーだと思っておけば良い
    SurfaceHolder mHolder;
    int mSurfaceWidth;      // surfaceViewの幅
    int mSurfaceHeight;     // surfaceViewの高さ

    // float型の数値の末尾にはfをつけないとコンパイルエラーになる
    static final float RADIUS = 150.0f;     // ボール描画の際の半径を表す定数(単位はpixel)
    static final int DIA = (int)RADIUS * 2; // ボールの直径を表す定数(int型へキャスト)
    static final float COEF = 1000.0f;      // ボールの移動量を調整するための係数

    float mBallX;           // ボールの現在のX座標(位置）
    float mBallY;           // ボールの現在のY座標(位置）
    float mVX;              // ボールのX軸方向への加速度
    float mVY;              // ボールのY軸方向への加速度

    // longは時間を表すためのクラス
    long mT0;               // 前回センサーから加速度を取得した時間

    // Bitmapはイメージを扱うためのクラス
    Bitmap mBallBitmap;     // ボールの画像

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 画面を縦表示に固定
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        setContentView(R.layout.activity_main);

        /* getSystemService=>androidシステムを使う。
        センサーを使うために、その引数にSENSOR_SERVICEを用いる。
        戻り値のSensorManagerをメンバ変数mSensorManagerに格納 */
        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);

        /* getDefaultSensorメソッドの引数に加速度センサーを表すTYPE_ACCELEROMETERを指定
        メンバ変数mAccSensorに格納 */
        mAccSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

        // 画面レイアウトで配置したsurfaceViewを取得し、メンバ変数mHolderへ格納
        SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
        mHolder = surfaceView.getHolder();

        // surfaceHolderが変更・破棄された時のイベントリスナー
        mHolder.addCallback(this);

        // 背景画像を用意しているため、canvas自体は透明にする
        mHolder.setFormat(PixelFormat.TRANSLUCENT);
        surfaceView.setZOrderOnTop(true);

        // ボールの画像を表示
        // decodeResource()メソッドでリソースを取得
        // 引数にgetResources()メソッドとリソースのidを指定することで取得ができる
        Bitmap ball = BitmapFactory.decodeResource(getResources(), R.drawable.ball);
        // 描画したボールのリサイズ
        // 上の行で取得しballに格納したリソースを、createScaledBitmapで上下の高さをDIA(直径)、
        // 左右の長さをDIAに指定
        // 最後の引数はフィルター(ピクセルのギザギザに対する補正)をかけるか否か
        mBallBitmap = Bitmap.createScaledBitmap(ball, DIA, DIA, false);
    }

    // 加速度センサーの値に変化があった時に呼ばれるメソッド
    @Override
    public void onSensorChanged(SensorEvent sensorEvent) {

        // センサーが加速度センサーだったら
        if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {

            /* センサーから取得した加速度をx, yに代入
            x軸は画面の描画方向に合わせるために反転させる
            なお、それぞれの加速度はx軸方向への加速度、y軸方向への加速度と理解され、
            反対方向に加速する場合は値にマイナスをかけるものと思われる */
            float x = -sensorEvent.values[0];
            float y = sensorEvent.values[1];

            /* 時間tを求める
            ループの一番最初では前回時間は取れず、sensorEventのtimestampをmT0に代入 */
            if (mT0 == 0) {
                mT0 = sensorEvent.timestamp;
                // return;で終了
                return;
            }

            // 新たなsensorEventのtimestampから、前回時間が入っているmT0を引くことで時間がわかる
            float t = sensorEvent.timestamp -mT0;
            mT0 = sensorEvent.timestamp;
            t = t/ 1000000000.0f;               // ナノ秒を秒に単位変換

            // x方向、y方向への移動距離を求める
            // 以下は物理の公式であり無理に覚える必要はない
            // (X軸方向への加速度*時間)+(センサーから取得したxの加速度*時間の2乗を2で割る)と移動距離を算出
            float dx = (mVX * t) + (x * t * t/2.0f);
            // (Y軸方向への加速度*時間)+(センサーから取得したyの加速度*時間の2乗を2で割る)と移動距離を算出
            float dy = (mVY * t) + (y * t * t/2.0f);

            // 移動距離から今のボールの位置を更新
            // COEFは画面上で自然な移動量となるための係数
            mBallX = mBallX + dx * COEF;
            mBallY = mBallY + dy * COEF;

            // 現在のボールの移動速度を更新(等加速度運動の公式<=無理して覚えることはない)
            // 速度 = 初速度 + 加速度 * 時間
            mVX = mVX + (x * t);
            mVY = mVY + (y * t);

            // ボールが画面の外に出ないようにする処理(まずは横軸)
            // 現在のボールの位置のx座標からボールの半径を引いた時に、マイナスになる(=壁に当たっている)
            // かつ速度がマイナスになっている場合に、速度を反転しプラスにしてやる
            if(mBallX - RADIUS < 0 && mVX < 0){
                // 壁に当たって減速(速度を1.5で割ると遅くなる)
                mVX = -mVX / 1.5f;
                // ボールの位置座標は半径の値に更新する
                mBallX = RADIUS;
                // surfaceViewの横幅よりもボールのx座標と半径の合計が大きく、かつ速度が0以上だったら
                // =先ほどと逆方向に画面の外にはみ出してしまったら
            } else if (mBallX + RADIUS > mSurfaceWidth && mVX > 0){
                // 壁に当たって減速
                mVX = -mVX / 1.5f;
                // surfaceVIewの横幅から半径を引いた値へ、ボールの位置(のx座標)を更新
                mBallX = mSurfaceWidth - RADIUS;
            }

            // ボールが画面の外に出ないようにする処理(次は縦軸)
            if(mBallY - RADIUS < 0 && mVY < 0){
                mVY = - mVY / 1.5f;
                mBallY = RADIUS;
            } else if (mBallY + RADIUS > mSurfaceHeight && mVY > 0){
                mVY = -mVY / 1.5f;
                mBallY = mSurfaceHeight - RADIUS;
            }

            // 加速度から算出したボールの現在位置でボールをキャンバスに描画し直す
            drawCanvas();
        }
    }

    // 画面にボールを表示する処理
    private void drawCanvas() {
        // lockCanvasを呼び出しcanvasを取得するとついでにsurfaceがロックされ描画可能な状態に
        Canvas c = mHolder.lockCanvas();
        // drawColorで画面のボールをクリア(前回の描画をクリアしないと画面がボールだらけに)
        // つまりdrawColor()は何かを何色かで塗りつぶすメソッドであり,
        // 今回は透明色でcanvasを塗りつぶすという意味合い。
        // 第二引数はどうやら必須
        c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        // Paintクラスは描画色、線の太さ、文字の大きさやフォントなど実際に描画を行う際の要素を設定するために必要
        // 今回はとりあえずインスタンスを作成するだけ
        Paint paint = new Paint();
        // bitmapをcanvasの座標(left, top)の位置を拠点にして描画する
        c.drawBitmap(mBallBitmap, mBallX - RADIUS, mBallY - RADIUS, paint);

        // surfaceViewが格納されているメンバ変数、mHolder(ホルダー)に画面への反映とcanvasのunlock
        mHolder.unlockCanvasAndPost(c);
    }

    // 加速度センサーの精度が変更された時に呼ばれるメソッド
    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }

    // 画面が表示された時に呼ばれるメソッド
    @Override
    protected void onResume() {
        super.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();
    }

    // Surfaceが作成された時に呼ばれる
    @Override
    public void surfaceCreated(SurfaceHolder holder) {

        /* registerListenerは加速度センサーの監視を始めるメソッド
        第１引数：誰が監視をするか（thisはMainActivity自身を指す）
        第２引数：監視を行いたいセンサー
        第３引数：監視を行いたい頻度 */
        mSensorManager.registerListener(this, mAccSensor, SensorManager.SENSOR_DELAY_GAME);
    }

    // Surfaceに変更があった時に呼ばれる
    // 同時にボールの位置、速度、時間の初期化を行う
    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
        mSurfaceWidth = i1;
        mSurfaceHeight = i2;

        // ボールの初期位置を指定する
        // surfaceViewの横幅、縦幅の半分を指定すれば中央に配置できる
        mBallX = mSurfaceWidth / 2;
        mBallY = mSurfaceHeight / 2;

        // 最初の速度と時間の初期化
        mVX = 0;
        mVY = 0;
        mT0 = 0;
    }

    // Surfaceが削除される時に呼ばれる
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

        // 画面を閉じた時に監視を終了する //
        mSensorManager.unregisterListener(this);
    }
}

```

# 実行してみる

<img width="519" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391581/11c1759f-e3a6-e574-a1a0-4bf8f6a23289.png">

実行は、右上の>実行ボタンから

<img width="617" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391581/2c337df0-9e01-edb2-1191-0f9695d1baa0.png">
エミュレータを選択。
実機で試す場合は実機をUSBで接続する前に、実機側の設定が必要になります(割愛)。

<img width="443" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391581/2d7a63be-2982-58af-b89d-ce77263b9e5e.png">

右下の「…」を選択し、さらにVirtual sensorsを選択すると
<img width="812" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391581/ac51ecab-88e6-4d9b-d19d-95c820300ff5.png">
こんな感じでエミュレータを傾けて遊べます

# 小刻みなバウンドについて

エミュレータを動かしてみるとセンサーに従って動くことがわかります。
しかし、バウンド処理はしていますが停止処理をしていないので、エミュレータを固定してもボールは小刻みなバウンドを続けます。
バグではなく仕様です。

完全に止めるには減速処理のところにしきい値を設定し、加速度を0にする方法があるようです。

もう少し理解が深まったら試してみようと思います。