Android勉強会 vol.3 センサーの値をリアルタイムでグラフ表示する

この記事では Android Studio 2.1.2 を使用しています。
大学の研究室で開催している勉強会の資料共有とアーカイブのためにQiitaに投稿しています。
まずはプロジェクトを作成する

今回はminSDKをAndroid4.1以上に設定して開発を進めます。
初期に生成されるActivityはEmptyActivityを使用します。

# センサーを使うには
Androidでセンサーの値を使う場合には様々な手順が必要となります。

1. AndroidOSの中でセンサーを管理しているSensorManagerを呼ぶ
2. 使いたいセンサーをSensorManagerに確認する
3. センサーを監視するようにSensorManagerに登録する
4. センサーの値を受け取る

簡単に説明するとこのような流れになります。

# 画面の回転を縦で固定する
加速度センサーなどを使うときには画面を固定しておく方が無難ですね。
`android:screenOrientation="portrait"`をActivityに追記する。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.daiki.sensorgraph">

    <application
        ...>
        <activity
            ...
            android:screenOrientation="portrait">
            ...
        </activity>
    </application>

</manifest>
```

# そのままの値を表示する
それでは加速度を例に実際にデータを取ってみます。
## 表示する画面の作成
まずは表示するためのTextViewを用意します。activity_main.xmlを開きTextViewを追加します。
![無題.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/a3509c00-aa59-bb98-6a64-47a1536f4320.jpeg)
x, y, z軸の加速度を表示するTextViewにはidをつけておきます。
## 必要な変数の定義
センサーの値を表示するためにSensorManagerとSensorとTextViewを用意します。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity {
    SensorManager manager;
    Sensor sensor;
    TextView xTextView;
    TextView yTextView;
    TextView zTextView;

    ...
}
```

## 変数の初期化
Androidからセンサーを管理しているサービスを呼び出し加速度センサーを保持します。同時にTextViewも取得します。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        xTextView = (TextView)findViewById(R.id.xValue);
        yTextView = (TextView)findViewById(R.id.yValue);
        zTextView = (TextView)findViewById(R.id.zValue);

        manager = (SensorManager)getSystemService(SENSOR_SERVICE);
        sensor = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
    }
}
```

システムからセンサーに関するサービスを取得し、センサーには標準の加速度を登録しています。
`getDefaultSensor`に渡す引数を変えることで違う種類のセンサーの値も取得することができます。

## SensorEventListener
センサーの値を受け取るためにSensorEventListenerを実装します。
これでMainActivity.java内でセンサーのデータを受け取れるようにします。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity implements SensorEventListener{
    ...

    @Override
    public void onSensorChanged(SensorEvent event) {
        
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }
}
```

2つのメソッドを用意する必要があります。AndroidStudioの場合

1. `alt + Enter`を押す
2. Implements methodsを選択する
3. OKをクリックする

これにより自動的に追加することができます。
![無題.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/9cf24c55-ff20-29f7-59ba-f8510ec7a315.jpeg)

## センサーデータの取得を開始、停止する
SensorManagerにセンサーのデータを取り始める命令と停止する命令を追記します。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity implements SensorEventListener{
    ...

    @Override
    protected void onResume() {
        super.onResume();
        manager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI);
    }

    @Override
    protected void onPause() {
        super.onPause();
        manager.unregisterListener(this);
    }
}
```

`registerListener`という関数を用いて監視を開始させるものの第3引数の`SENSOR_DELAY_UI`の部分を変更することで更新頻度を変更することができる。
なお、onResumeなどのすでにある関数を上書きする場合はその関数名を書くだけでsuperへの実行とともに生成してくれる。
![無題.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/9884ce3a-706e-f9f3-8177-d2570dafdc9c.jpeg)

![無題.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/74caddf7-8959-3131-4216-d72e96c45e59.jpeg)

## センサーデータを表示する
TextViewにセンサーのデータを表示していきます。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity implements SensorEventListener{
    ...

    @Override
    public void onSensorChanged(SensorEvent event) {
        xTextView.setText(String.valueOf(event.values[0]));
        yTextView.setText(String.valueOf(event.values[1]));
        zTextView.setText(String.valueOf(event.values[2]));
    }

    ...
}
```

![device-2016-07-07-152355.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/1210a3d8-483a-28c1-68e4-dda239be4e08.jpeg)

センサーの値が表示されました。端末を傾けて加速度が変化するか確認してください。

# センサーの種類
端末によっては搭載されていない場合もありますが、Androidが用意しているセンサーの種類はこのようなものがあります。

- TYPE_ACCELEROMETER
- TYPE_AMBIENT_TEMPERATURE
- TYPE_GAME_ROTATION_VECTOR
- TYPE_GEOMAGNETIC_ROTATION_VECTOR
- TYPE_GRAVITY
- TYPE_GYROSCOPE
- TYPE_GYROSCOPE_UNCALIBRATED
- TYPE_HEART_RATE
- TYPE_LIGHT
- TYPE_LINEAR_ACCELERATION
- TYPE_MAGNETIC_FIELD
- TYPE_MAGNETIC_FIELD_UNCALIBRATED
- TYPE_ORIENTATION
- TYPE_PRESSURE
- TYPE_PROXIMITY
- TYPE_RELATIVE_HUMIDITY
- TYPE_ROTATION_VECTOR
- TYPE_SIGNIFICANT_MOTION
- TYPE_STEP_COUNTER
- TYPE_STEP_DETECTOR
- TYPE_TEMPERATURE

加速度やジャイロ、地磁気あたりはほぼすべての端末で取得できると思われますが使うセンサーがあるか確認してからSensorEventListenerに登録するほうが安心ですね。

# センサーをグラフで表示する
Androidで簡単にグラフが表示できるライブラリ[MPAndroidChart](https://github.com/PhilJay/MPAndroidChart)を用いてグラフ表示を行います。

## ライブラリを使うための準備
ライブラリを使用するためにGradleファイルを書き換えていきます。

まずGradle Scriptsのドロップダウン`build.gradle (Project: プロジェクト名)`と表示されているものを開き、以下の2行を追加する。

```:build.gradle
buildscript {
    repositories {
        ...
        maven { url "https://jitpack.io" }
    }
    ...
}

allprojects {
    repositories {
        ...
        maven { url "https://jitpack.io" }
    }
}
```

次にGradle Scriptsのドロップダウン`build.gradle (Module: app)`と表示されているものを開き、以下の2行を追加する。

```:build.gradle
apply plugin: 'com.android.application'

android {
    ...
}

dependencies {
    ...
    compile 'com.github.PhilJay:MPAndroidChart:v3.0.0-beta1'
}
```

Gradleはbuildする際にGradleファイルを元に依存関係を参照するのでここにライブラリの参照元を追加しています。

## グラフの表示エリアを追加する
activity_main.xmlにグラフ表示用のMPAndroidChartのViewを追加します。
Paletteの一番したにあるCustomView ![無題2.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/18e8e9ac-7222-fef2-fe0f-a30d1b93e56f.jpeg) を選択しLineChartを選択します。
![無題.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/0ceee3cc-b9d8-cbd7-a5a0-93b23f83ec10.jpeg)
layout:widthとlayout:heightはmatch_parentにして全体に広がるようにします。あとで呼び出すためにidも振っておきます。

コードを直接編集する際はactivity_main.xmlを開いた際に表示される下部のDesignタグをTextタグに切り替えます。
![無題.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/afc0abbd-2fa4-f901-b2ed-8dabb528ba8c.jpeg)

Textタブでの編集では以下のようになります。

```xml:activity_main.xml
...
    <com.github.mikephil.charting.charts.LineChart
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/lineChart"/>
...
```

AndroidのLayoutファイルでは後に書いたものが上のレイヤーに重ねていかれるので今回は他のTextViewより前に上記のコードを追記します。
![無題.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/2b243aa4-62f8-443b-e0ee-5dff3bd0dd07.jpeg)
このようになっていればグラフ表示のViewが一番下になりますので先ほど作成したTextViewも表示され無駄になりません。
![device-2016-07-12-102423.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/d066fb80-1329-0448-6317-f38295ccc386.jpeg)


## グラフを表示するプログラムを書く
MPAndroidChartのLineChartを使うためには

1. LineChartビューにLineData型のインスタンスを渡す(ただしまだデータは追加されていない)
2. データを追加するためにビューに割り当てられたLineData型インスタンスを取得する
3. LineData型インスタンスからILineDataSet型インスタンスを取得する(ないときは作成する)
4. ILineDataSet型インスタンスにEntry型インスタンス(これがグラフのデータ)を追加する
5. データの変更を通知して表示に反映させる
6. 表示位置を最新データにする

という流れが必要になります。

### MPAndroidChartのクラス構成について
クラス構成を理解しやすいように図にしてみました。

- LineChartは1つのLineData
- LineDataは複数のILineDataSet
- ILineDataSetは複数のEntry

を持つことができます。
![図1.jpg](https://qiita-image-store.s3.amazonaws.com/0/113969/eba7b50d-89c7-ebd2-0976-04cc893c6262.jpeg)

### onCreate内でビューの初期化
まずはonCreate内でビューを取得しいくつか初期化を行います。
namesとcolorsは3軸の加速度のための配列です

```java:MainActivity.java
public class MainActivity extends AppCompatActivity implements SensorEventListener {
    ...
    LineChart mChart;

    String[] names = new String[]{"x-value", "y-value", "z-value"};
    int[] colors = new int[]{Color.RED, Color.GREEN, Color.BLUE};

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

        mChart = (LineChart) findViewById(R.id.lineChart);

        mChart.setDescription(""); // 表のタイトルを空にする
        mChart.setData(new LineData()); // 空のLineData型インスタンスを追加
    }
}
```

### onSensorChangedにグラフへのデータ追加のためのコードを追加する
センサーの値を取得するたびにグラフにデータを追加して表示を更新していきます。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity implements SensorEventListener {
    ...

    @Override
    public void onSensorChanged(SensorEvent event) {
        ...

        LineData data = mChart.getLineData();
        if (data != null) {
            for (int i = 0; i < 3; i++) { // 3軸なのでそれぞれ処理します
                ILineDataSet set = data.getDataSetByIndex(i);
                if (set == null) {
                    set = createSet(names[i], colors[i]); // ILineDataSetの初期化は別メソッドにまとめました
                    data.addDataSet(set);
                }

                data.addEntry(new Entry(set.getEntryCount(), event.values[i]), i); // 実際にデータを追加する
                data.notifyDataChanged();
            }

            mChart.notifyDataSetChanged(); // 表示の更新のために変更を通知する
            mChart.setVisibleXRangeMaximum(50); // 表示の幅を決定する
            mChart.moveViewToX(data.getEntryCount()); // 最新のデータまで表示を移動させる
        }
    }
}
```

### createSetメソッドを作る
3軸ということで色分けもするため引数に凡例に表示されるデータの名前と色を取ります。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity implements SensorEventListener {
    ...

    private LineDataSet createSet(String label, int color) {
        LineDataSet set = new LineDataSet(null, label);
        set.setLineWidth(2.5f); // 線の幅を指定
        set.setColor(color); // 線の色を指定
        set.setDrawCircles(false); // ポイントごとの円を表示しない
        set.setDrawValues(false); // 値を表示しない

        return set;
    }
}
```

実際に実行して確認してみましょう。

# チャレンジ

- センサーの種類を変えてみる(ジャイロや気圧など)
- ボタンでグラフの更新を止めてみる
- ローパスフィルターをかける