RxJava上のAndroidのShake Detector





エントリー



すべては、デバイスを振るときにアプリケーションの最後のアクションをキャンセルすることがタスクであるという事実から始まりました。 しかし、何が起こったかを理解する方法は同じ揺れですか? 問題を数分調べた後、加速度計からのイベントをサブスクライブし、デバイスが揺れていることを何らかの方法で判断する必要があることが明らかになりました。

既製のソリューションが登場しました。 それらはすべて非常に似ていましたが、それらは純粋な形では私に合わず、私は自分の「自転車」を書きました。 これは、センサーからのイベントをサブスクライブし、到着時にその状態を変更したクラスでした。 それから数回、同僚と私はこの自転車のギアをねじって、その結果、マッドマックスの何かに似始めました。 私は、自由時間が際立つように、この不名誉を整理することを約束しました。



したがって、RxJavaに関する最近の記事を読んでいるときに、このタスクを思い出しました。 「うーん」と思った、「RxJavaはこの種の問題に非常に適したツールのように見える。」 遅滞なく、RxJavaでソリューションを作成しました。 結果は私を驚かせました:すべてのロジックは8(8!)行かかりました! 私の経験を他の開発者と共有することにしました。 それで、この記事は生まれました。



この簡単な例が、プロジェクトでRxJavaを使用することを考えている人が決定を下すのに役立つことを願っています。



この記事は、基本的なAndroid開発経験のある読者を対象としています。 完成したアプリケーションのソースコードは、 GitHubで表示できます。



さあ始めましょう!





プロジェクトのセットアップ



RxJavaをプロジェクトに接続します



RxJavaを接続するには、build.gradleに追加するだけです



dependencies { ... compile 'io.reactivex:rxjava:1.1.3' compile 'io.reactivex:rxandroid:1.1.0' }
      
      







注:RxAndroidは、UIスレッドに関連付けられたスケジューラを提供します。



ラムダサポートを有効にする



RxJavaはラムダと一緒に使用すると最適です。ラムダがないと、コードが読みにくくなります。 現時点では、Androidプロジェクトでlambdサポートを有効にする2つのオプションがあります。AndroidN Developer PreviewのJackコンパイラを使用するか、Retrolambdaライブラリを使用します。

どちらの場合も、最初にJDK 8がインストールされていることを確認する必要があります。



Android N Developer Preview



Android N Developer PreviewからJackを使用するには、こちらの手順に従ってください



build.gradleに次の行を追加します。

 android { ... defaultConfig { ... jackOptions { enabled true } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }
      
      







レトロラムダ



レトロラムダを接続するには、Evan Tatarkaの指示に従ってください



 buildscript { ... dependencies { classpath 'me.tatarka:gradle-retrolambda:3.2.5' } } apply plugin: 'com.android.application' apply plugin: 'me.tatarka.retrolambda' android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }
      
      







元の手順では、Maven Centralリポジトリを接続することを推奨しています。 ほとんどの場合、jcenterはプロジェクトですでに使用されています。AndroidStudioでプロジェクトを作成するときにデフォルトで指定されるのはこのリポジトリであるためです。 すでに必要な依存関係が含まれているため、Maven Centralを追加で接続する必要はありません。



観測可能



したがって、私たちのプロジェクトでは、必要なすべてのツールが接続されており、開発を開始できます。



RxJavaを使用する場合、すべてはObservableを取得することから始まります。

Observable.createメソッドを使用して、送信されたセンサーのイベントにサブスクライブするObservableを作成するファクトリーを作成しましょう。

 public class SensorEventObservableFactory { public static Observable<SensorEvent> createSensorEventObservable(@NonNull Sensor sensor, @NonNull SensorManager sensorManager) { return Observable.create(subscriber -> { MainThreadSubscription.verifyMainThread(); SensorEventListener listener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { if (subscriber.isUnsubscribed()) { return; } subscriber.onNext(event); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // NO-OP } }; sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME); // unregister listener in main thread when being unsubscribed subscriber.add(new MainThreadSubscription() { @Override protected void onUnsubscribe() { sensorManager.unregisterListener(listener); } }); }); } }
      
      







これで、イベントを任意のセンサーからObservableに変換するツールができました。 しかし、どのセンサーが私たちの目的に最適ですか? 以下のスクリーンショットでは、最初のグラフにはTYPE_GRAVITYセンサーの測定値が 表示され 、2番目のグラフにはTYPE_ACCELEROMETERが表示され 、3番目のグラフにはTYPE_LINEAR_ACCELERATIONが表示されます。 最初は、デバイスがスムーズに回転し、その後、激しく振られたことがわかります。











Sensor.TYPE_LINEAR_ACCELERATIONタイプのセンサーイベントに関心があります。 これには、地球の重力の成分がすでに差し引かれている加速度値が含まれています。



多くのソリューションがSensor.TYPE_ACCELEROMETERを使用し、重力成分を除去するためにハイパスフィルタリングを使用することは興味深いです。 理由がわからない場合は、コメントであなたの知識を共有してください。



 @NonNull private static Observable<SensorEvent> createAccelerationObservable(@NonNull Context context) { SensorManager mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); List<Sensor> sensorList = mSensorManager.getSensorList(Sensor.TYPE_LINEAR_ACCELERATION); if (sensorList == null || sensorList.isEmpty()) { throw new IllegalStateException("Device has no linear acceleration sensor"); } return SensorEventObservableFactory.createSensorEventObservable(sensorList.get(0), mSensorManager); }
      
      







リアクティブマジック



加速度計からのイベントを含むObservableができたので、RxJavaオペレーターのフルパワーを使用できます。



イベントの生のシーケンスがどのように見えるか見てみましょう:

 createAccelerationObservable(context) .subscribe(event -> Log.d(TAG, formatTime(event) + " " + Arrays.toString(event.values))); 29.398 [0.0016835928, 0.014868498, 0.0038280487] 29.418 [-0.026405454, -0.017675579, 0.024353027] 29.438 [-0.032944083, -0.0029007196, 0.011956215] 29.458 [0.03226435, 0.022876084, 0.032211304] 29.478 [-0.0011371374, 0.022291958, -0.054023743]
      
      







イベントがセンサーから20ミリ秒ごとに到着することがわかります。 この頻度は、SensorEventListenerを登録するときに、samplingPeriodUsパラメーターとして渡されたSensorManager.SENSOR_DELAY_GAME値に対応します。



ペイロードとして、3つの軸すべての加速度の値があります。

X軸に沿った値にのみ興味があり、追跡したい動きに対応します。 一部のソリューションでは、3つの軸すべてで加速度値を使用するため、たとえば、デバイスがテーブルに置かれている場合(テーブルに接触しているときにZ軸に沿って大きな加速度)に機能します。

興味のある値を持つデータクラスを作成しましょう。

 private static class XEvent { public final long timestamp; public final float x; private XEvent(long timestamp, float x) { this.timestamp = timestamp; this.x = x; } }
      
      







SensorEventをXEventに変換し、モジュロモジュロの大きさが特定のしきい値を超えるイベントをフィルター処理します。

 createAccelerationObservable(context) .map(sensorEvent -> new XEvent(sensorEvent.timestamp, sensorEvent.values[0])) .filter(xEvent -> Math.abs(xEvent.x) > THRESHOLD) .subscribe(xEvent -> Log.d(TAG, formatMsg(xEvent)));
      
      







ログにイベントを表示するには、デバイスを初めて振る必要があります。



一般に、外部からのシェイク検出デバッグプロセスはかなりおかしく見えます。開発者は常に座って電話を揺らしています。 他の人が同時に何を考えているのかわかりません:)

 55.347 19.030302 55.367 13.084376 55.388 -15.775546 55.408 -14.443999
      
      







X軸に沿って大幅に加速されたイベントのみがログに残りました。



楽しい部分として、加速度が符号を変更するタイミングを追跡します。 この瞬間が何であるかを理解してみましょう。 たとえば、最初に電話を左に向けて手を加速しますが、加速度はX軸に負の投影を持ち、次に手を停止します-この瞬間、X軸の投影は符号を正に変更します。 つまり、1つのストロークに対して、投影の符号に1つの変化があります。

これを行うには、最初に各イベントとその前のイベントを含むスライドウィンドウを作成します。

 createAccelerationObservable(context) .map(sensorEvent -> new XEvent(sensorEvent.timestamp, sensorEvent.values[0])) .filter(xEvent -> Math.abs(xEvent.x) > THRESHOLD) .buffer(2, 1) .subscribe(buf -> Log.d(TAG, getLogMsg(buf)));
      
      







ログを確認します。

 [43.977 -15.497713; 44.017 21.000145] [44.017 21.000145; 44.037 19.947767] [44.037 19.947767; 44.057 19.836182] [44.057 19.836182; 44.077 20.659754] [44.077 20.659754; 44.098 -16.811298] [44.098 -16.811298; 44.118 -15.6345
      
      







いいね! 各イベントは前のイベントとグループ化されていることがわかります。これで、異なる加速サインを持つイベントのペアを簡単にフィルタリングできます。

  createAccelerationObservable(context) .map(sensorEvent -> new XEvent(sensorEvent.timestamp, sensorEvent.values[0])) .filter(xEvent -> Math.abs(xEvent.x) > THRESHOLD) .buffer(2, 1) .filter(buf -> buf.get(0).x * buf.get(1).x < 0) .subscribe(buf -> Log.d(TAG, getLogMsg(buf)));
      
      







 [53.888 -16.762777; 53.928 20.83315] [53.988 19.87952; 54.028 -16.735554] [54.089 -16.46596; 54.109 21.682497] [54.169 20.355597; 54.209 -16.634022] [54.269 -16.122211; 54.309 21.806463]
      
      







各イベントは1つのウェーブに対応するようになりました。 たった4人のオペレーターで、私たちはすでに鋭い動きを追跡することができます! しかし、検出器が1つの波によってトリガーされた場合、誤警報が発生する可能性があるため、停止しません。 たとえば、ユーザーはデバイスを振るつもりはありませんでしたが、単に反対側に移動しました。 解決策は簡単です。ユーザーに短時間でデバイスを数回振らせる必要があります。 パラメーターSHAKES_COUNT =ストローク数およびSHAKES_PERIOD =必要なストローク数を作成するために時間が必要な時間間隔を入力します。 実験的に、快適なパラメーターは1秒間に3ストロークであることが判明しました。 それ以外の場合は、ランダムにトリップする可能性があります、または非常に激しく電話を振る必要があります。



したがって、1秒間に3つのスイングが発生した瞬間を追跡したいと思います。

加速度の値はもう必要ないことに注意してください。イベントが発生した時間だけを残し、同時に時間をナノ秒から秒に変換します。

 .map(buf -> buf.get(1).timestamp / 1000000000f)
      
      





次に、スライディングウィンドウを使用して、使い慣れたトリックを適用します。 イベントごとに、このイベントと前の2つのイベントを含む配列を返します。

 .buffer(SHAKES_COUNT, 1)
      
      





最後に、1秒以内に収まるイベントのトリプルのみを残します。

 .filter(buf -> buf.get(SHAKES_COUNT - 1) - buf.get(0) < SHAKES_PERIOD)
      
      





イベントが最後のフィルターを通過した場合、最後の1秒間にユーザーがデバイスを3回振ったことを意味します。

しかし、ユーザーが気を失い、引き続き携帯電話を注意深く振るとします。 その後、次のスイングごとにイベントを受け取り、3ウェーブごとにイベントを受け取りたいと考えています。 簡単な解決策は、ジェスチャーが定義されてから1秒間イベントを無視することです。

 .throttleFirst(SHAKES_PERIOD, TimeUnit.SECONDS)
      
      





できた! これで、Observableの結果は、イベントのシェイクを待つ場所で使用できます。



Observableを作成するための最終コードは次のとおりです。

 public class ShakeDetector { public static final int THRESHOLD = 13; public static final int SHAKES_COUNT = 3; public static final int SHAKES_PERIOD = 1; @NonNull public static Observable<?> create(@NonNull Context context) { return createAccelerationObservable(context) .map(sensorEvent -> new XEvent(sensorEvent.timestamp, sensorEvent.values[0])) .filter(xEvent -> Math.abs(xEvent.x) > THRESHOLD) .buffer(2, 1) .filter(buf -> buf.get(0).x * buf.get(1).x < 0) .map(buf -> buf.get(1).timestamp / 1000000000f) .buffer(SHAKES_COUNT, 1) .filter(buf -> buf.get(SHAKES_COUNT - 1) - buf.get(0) < SHAKES_PERIOD) .throttleFirst(SHAKES_PERIOD, TimeUnit.SECONDS); } @NonNull private static Observable<SensorEvent> createAccelerationObservable(@NonNull Context context) { SensorManager mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); List<Sensor> sensorList = mSensorManager.getSensorList(Sensor.TYPE_LINEAR_ACCELERATION); if (sensorList == null || sensorList.isEmpty()) { throw new IllegalStateException("Device has no linear acceleration sensor"); } return SensorEventObservableFactory.createSensorEventObservable(sensorList.get(0), mSensorManager); } private static class XEvent { public final long timestamp; public final float x; private XEvent(long timestamp, float x) { this.timestamp = timestamp; this.x = x; } } }
      
      







使用する



この例では、イベントが発生したときにサウンドを再生します。

アクティビティで、シェイクを聞きたい場合は、フィールドを追加します。

 private Observable<?> mShakeObservable;
      
      







onCreateで初期化します。



 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mShakeObservable = ShakeDetector.create(this); }
      
      







onResumeのイベントをサブスクライブします。



 @Override protected void onResume() { super.onResume(); mShakeSubscription = mShakeObservable.subscribe((object) -> Utils.beep()); }
      
      







また、onPauseで登録解除することを忘れないでください。

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







おわりに



ご覧のとおり、指定したジェスチャを確実に決定するソリューションを数行で記述できました。 コードはコンパクトで、読みやすく、保守が容易です。 Jx WhartonのSeismicなど 、RxJavaを使用しないソリューションと比較してください。 RxJavaは優れたツールであり、RxJavaを巧みにビジネスに適用すると、素晴らしい結果を得ることができます。 この記事が、あなたがRxJavaを学び、あなたのプロジェクトにリアクティブプログラミングアプローチを適用することを奨励することを願っています。



stackoverflow.comがあなたと共にありますように!



Arkady Gamza、Android開発者。



All Articles