2015年11月23日
Android端末では、タッチパネルがマルチタッチに対応しており、一度に複数のボタンを押すことができますが、うっかりすると、複数のアクティビティーを同時に起動するような、やっかいな状態遷移をしてしまうこともあるようです。
Androidでは、ユーザインターフェースを実現する画面一枚一枚を、Activity(アクティビティー)と呼んでいます。
アプリは、複数のActivityを移動しながら操作する構成となる場合が多いです。
例えば、メイン画面があり、データをロードする場合は「ロード画面」、データをセーブする場合は「セーブ画面」に移動する構成のアプリを考えます。
ところで、メイン画面で、「ロード」と「セーブ」のボタンを同時に押すと、何が起こるのでしょうか?
ほとんどの場合は、どちらか早く押した一方のボタンだけクリックされたと判定して、「セーブ」「ロード」のいずれか一方にだけ遷移します。
ところがごくまれに、ボタンを押すタイミングによっては、「ロード」と「セーブ」の両方が押されたと判断されてしまうこともあります。
このときはまず、「ロード」と「セーブ」の一方のリンク先に、先にジャンプし、次に、もう一方のリンク先にジャンプします。
例えば、「セーブ」→「ロード」の順に押されたと判定された場合は、以下のように動作します。
メイン画面から、ロード画面に遷移します。ロードの操作を行うことができます。
ロード画面を閉じると、メイン画面には戻らずに、セーブ画面に遷移します。セーブの操作を行うことができます。
セーブ画面を閉じると、メイン画面に戻ります。
このような動作は、選択肢の「はい」と「いいえ」を両方選んでしまうような不都合な動作につながりかねず、バグや意図しない裏技を作ってしまう原因となりえます。
ちなみに、ほぼ同時に2つのボタンが押された場合に、後に押されたリンク先のほうが先に表示されるのは、アクティビティーを積み上げるスタックの都合によります。詳しくは、後で説明します。
メイン画面(MainActivity)には、「ロード」「セーブ」の2つのボタンがあります。
ソースコード上は、それぞれのボタンがクリックされると、startActivityForResult()を実行して、それぞれ「ロード画面」「セーブ画面」のアクティビティーを開始するようにしています。
public void button1Clicked(View v){ Log.d("main", "button1 clicked."); Intent i = new Intent(this, LoadActivity.class); startActivityForResult(i, 1); } public void button2Clicked(View v){ Log.d("main", "button2 clicked."); Intent i = new Intent(this, SaveActivity.class); i.putExtra("text", edit1.getText().toString()); startActivityForResult(i, 2); } |
「ロード」のボタンをクリックすると、button1Clicked()が実行され、「ロード画面」のアクティビティーを起動します。
「ロード実行」を押せば、ロード画面のアクティビティーは終了(finish())します。その結果、メイン画面に戻ることを意図しています。
「セーブ」のボタンをクリックすると、button2Clicked()が実行され、「セーブ画面」のアクティビティーを起動します。
「セーブ実行」を押せば、セーブ画面のアクティビティーは終了(finish())します。その結果、メイン画面に戻ることを意図しています。
ところが、メイン画面で「ロード」「セーブ」をほぼ同時に押すと、違った動作となってしまいます。
ポイントとなるのは、startActivityForResult()を実行したからといって、表示中のアクティビティーで、イベントの受付をすぐにやめてしまうとは限らない点です。
startActivity()やstartActivityForResult()を実行すると、たいていは、現在のアクティビティーの上に、新しく起動したアクティビティーが表示されるので、現在のアクティビティーに対するイベントの受付は、すぐに終わります。従って、アクティビティーを起動するためのボタンは、1つしか押すことができません。
しかし、マルチタッチ機能等を活用して、立て続けにボタンを押した場合などで、ボタン操作のイベントが蓄積しているときは、startActivity()やstartActivityForResult()を実行したからといって、現在のアクティビティーに対するイベント処理の受付を終えるような仕組みにはなっていない様子なので、現在のアクティビティーでボタンのクリックなどのイベントが残っていれば、引き続きイベントの処理が行われてしまうようです。
従って、1つのアクティビティーが、startActivity()やstartActivityForResult()を立て続けに複数回実行してしまうこともありえます。
ボタンを複数個配置して、次に遷移する画面を選択するデザインを行うと、操作のタイミングによっては、両方の選択肢にジャンプしてしまう可能性があることを、知っておくべきでしょう。
メイン画面で「セーブ」→「ロード」の順に立て続けに操作すると、画面の遷移は「ロード画面」→「セーブ画面」→「メイン画面」の順となります。
操作した順序と、表示される順序が逆になっているのは、アクティビティーのスタックを積む仕組みにあるようです。
まず「セーブ」をクリックすると、アクティビティーのスタックで、「メイン画面」の上に「セーブ画面」が積まれます。
次に「ロード」をクリックすると、「セーブ画面」の上にさらに「ロード画面」が積まれます。
画面に表示されるのは、スタックで最も上位となるアクティビティーなので、ジャンプ先として先に表示されるのは「ロード画面」となります。
「ロード画面」のアクティビティーをfinish()させて終了させると、スタックから「ロード画面」が取り除かれるので、次は「セーブ画面」が表示されます。
さらに、「セーブ画面」のアクティビティーを終了させる(finish()させる)と、その下位にあるメイン画面に戻ります。
メイン画面から「ロード」「セーブ」のアクティビティーを起動するときは、startActivityForResult()を使っています。
「ロード」「セーブ」のアクティビティーを次々と起動した場合、対応するonActivityResult()は、どちらもメイン画面のアクティビティーで実行されます。
ロード画面→セーブ画面の順に表示された場合は、onActivityResult()は、表示がメイン画面に戻ったタイミングで、ロード画面のonActivityResult()→セーブ画面のonActivityResult()の順に、たてつづけに実行されるようです。
メイン画面から立ち上げたアクティビティーがそれぞれ、終了した順番に従って、onActivityResult()が実行されている様子です。
先に表示される「ロード画面」のonActivityResult()は、ロード画面を閉じたタイミングではなく、その次のセーブ画面を閉じ、表示がメイン画面に戻ってから実行される点に注意する必要があります。
1つのアクティビティーに、他のアクティビティーを起動するボタンを複数配置すると、両方のボタンが同時に押されると、両方のアクティビティーに遷移してしまう場合がある。
両方のアクティビティーに遷移した場合、startActivity()やstartActivityForResult()を実行した順に、アクティビティーのスタックが積まれ、スタックで最上位となるアクティビティーを表示する様子だ。
両方のアクティビティーに遷移した場合、onActivitiResult()は、起動元のアクティビティーに、表示が戻ってきたタイミングで実行される。遷移先のアクティビティーが終了した順に、それぞれのアクティビティーに対するonActivityResult()が実行される様子だ。
この例を説明するために作成したAndroidアプリのソース等をダウンロードできるようにしておきます。Android Studio 1.5で作ったプロジェクトを圧縮したものになっています。Android 4.0.4以降で実行できます。
インストーラに相当するAPKファイルは、圧縮されたファイルを展開すると出てくる app/build/outputs/apk 以下にあります。
ソフトに保証はありません。端末が壊れたりしても筆者は責任を負いかねますので、お使いになる場合は自己責任にてお願いします。
なお、この文章は、このアプリケーションをAndroid 6.0をターゲットにビルドしたものを、Android 5.1.1が入ったNexus 7(2012)で実行した場合に発生した現象を示しています。
ゲームソフトなどでは、きわどい操作が裏技をまねくことがあり、対策が重要になることがあります。OSの仕組みから、思わぬ状態遷移をしないかについて、実機で念入りにテストを行ったほうがよいでしょう。
杉原俊雄のホームページ→ Androidアプリ開発メモ(もくじ)
(c) 2015 Toshio Sugihara. All rights reserved.