fc2ブログ

戌印-INUJIRUSHI- (Androidあれこれ)

Androidのプログラミングをメインにしてます。記事に貼られたソースコードはダブルクリックすることで行番号をはずしてコピーすることができます。

 
1
2
3
4
5
6
8
9
10
11
12
13
14
15
16
17
18
19
21
22
23
24
25
26
28
29
30
04

Android でのボタン連打制御 (onClick)

作成したアプリのテストでボタンを連打したとき、イベントが2回走ってしまったりすることはありませんか?

例えば、次のアクティビティに遷移するボタン。
クリック時には本来1つのアクティビティしか開かないはずです。
しかし、ボタンを連打することで二重(多重)に開いてしまうことがあります。

これを制御するには以下のような方法があります。

①クリックから一定時間、次のイベントを禁止する
②フラグ管理によりイベントが完了するまで次のイベントを禁止する

とりあえず思いつくのは2つ。他にもあれば教えてください。

①クリックから一定時間、次のイベントを禁止する
// クリックイベント
@Override
public void onClick(View v) {
// 前回クリックから一定時間経過していなければクリックイベントを実行しない
if (!CommonUtils.isClickEvent()) return;

// 次のアクティビティを開く
startActivity(new Intent(this, NextActivity.class));
}

/** クリック連打制御時間(ミリ秒) */
private static final long CLICK_DELAY = 1000;
/** 前回のクリックイベント実行時間 */
private static long mOldClickTime;

/**
* クリックイベントが実行可能か判断する。
* @return クリックイベントの実行可否 (true:可, false:否)
*/
public static boolean isClickEvent() {
// 現在時間を取得する
long time = System.currentTimeMillis();

// 一定時間経過していなければクリックイベント実行不可
if (time - mOldClickTime < CLICK_DELAY) {
return false;
}

// 一定時間経過したらクリックイベント実行可能
mOldClickTime = time;
return true;
}
クリック時に前回クリックイベントを実行してから一定時間経過したかチェックします。
とりあえずの1秒なので禁止時間は各々調整してください。
1秒では短い処理もあるかもしれませんが、長時間の処理は ProgressDialog を表示すると思うので問題ないと思います。

フラグ管理によりイベントが完了するまで次のイベントを禁止する
View#setClickable(false) でクリックを制御する方法もありますが、ボタン等のクリックイベントを持つ View が複数あると制御が面倒になるのでフラグ管理しています。
/** クリックイベント実行可否フラグ */
private boolean mIsClickEvent;

/**
* 再開処理
*/
@Override
protected void onResume() {
super.onResume();

// クリックイベントを許可する
mIsClickEvent = false;
}

/**
* クリックイベント
*/
@Override
public void onClick(View v) {
// クリックイベント実行されていれば実行しない
if (mIsClickEvent) return;

// クリックイベントを禁止する
mIsClickEvent = true;

// 次のアクティビティを開く
startActivity(new Intent(this, NextActivity.class));
}
onClick が実行されたタイミングでクリックイベントを禁止します。
しかし、このままだと次のアクティビティから戻ったときにクリックできないままとなってしまいます。
そこでライフサイクルで必ず実行される onResume の中でフラグを元に戻します。

なぜこのような実装が必要なのか?

Android では UI の処理は全てメインスレッド(UI スレッド) で行われます。
ここで注意するのは、メインスレッドはボタンがクリックされた等のイベントをすぐに処理するわけではないということです。
イベントは一度メッセージキューに登録され、メインスレッドはこのメッセージキューからイベントを取り出して実行します。
この登録から実行までにはほんのわずかの間があるため、メインスレッドがイベントを実行する前にメッセージキューに複数のイベントが登録されてしまうことがあります。

そのため処理側で今回の①②のような対応が必要になります。
スポンサーサイト



タッチイベントを取得 : onTouchEvent

Android のタッチパネル操作には複数のタッチイベントがあります。

① タップやスライドといった単純なタッチイベントを処理する
  Activity#onTouchEvent / View#onTouchEvent
② ダブルタップやロングタッチなどの複雑なタッチイベントを処理する
  GestureDetector / ScaleGestureDetector

ここでは①の単純なタッチイベントの取得について解説します。
解説の最後には HTC 端末での問題点を少々…。

【参考サイト】
TechBooster - タッチイベントを取得する(onTouchEventとMotionEvent)
TechBooster - MotionEventでマルチタッチを検出する
Androidプログラマへの道 ~ Moonlight 明日香 ~ - タッチイベントを取得する
Androidプログラマへの道 ~ Moonlight 明日香 ~ - マルチタッチイベントを取得する

【タッチイベントの取得】
・Activity でタッチイベントを取得するには onTouchEvent(MotionEvent event) をオーバーライドする (※)
・タッチアクションは MotionEvent#getAction() から取得する。
・タッチアクションには何点目かを示すポインタも含まれているため、イベントを取得するには MotionEvent.ACTION_MASK でマスクする必要がある。

※ onTouchEvent(MotionEvent event) は Activity / View それぞれでオーバーライド可能。
※ View は View#OnTouchListener でタッチイベントを設定できる。
※ Activity / View 共にタッチイベントが取得可能な場合、View が優先される。

【タッチアクション】
ACTION_DOWN
  タッチイベント開始(1本目の指がタッチパネルに触れた)
ACTION_UP
  タッチイベント終了 (最後の指をタッチパネルから離した)
ACTION_MOVE
  タッチ中の指をスライドした
ACTION_CANCEL
  タッチイベントがキャンセルされた
以下はマルチタッチイベント。API 5 (Android 2.0) より追加。
ACTION_POINTER_UP
  2本目以降の指がタッチパネルに触れた
ACTION_POINTER_DOWN
  2本以上の指がタッチパネルに触れている時に指をタッチパネルから離した

【サンプル】
■ 画面イメージ
Android_onTouchEvent.png


■ TouchActivity.java
package jp.inujirushi.android.sample;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;

public class TouchActivity extends Activity {

TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_touch);
mTextView = (TextView) findViewById(R.id.text);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
// --------------------------------------------------
// 最後に発生したタッチイベントをログ表示
// --------------------------------------------------
// タッチ情報を取得
int action = event.getAction();
int actionEvent = (action & MotionEvent.ACTION_MASK);
int pointerId = (action & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;

// タッチイベント処理を実行
switch (actionEvent) {
case MotionEvent.ACTION_DOWN:
Log.i("onTouchEvent", "ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
Log.i("onTouchEvent", "ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i("onTouchEvent", "ACTION_MOVE");
break;
case MotionEvent.ACTION_CANCEL:
Log.i("onTouchEvent", "ACTION_CANCEL");
break;
case MotionEvent.ACTION_POINTER_UP:
Log.i("onTouchEvent", "ACTION_POINTER_UP:ID=" + pointerId);
break;
case MotionEvent.ACTION_POINTER_DOWN:
Log.i("onTouchEvent", "ACTION_POINTER_DOWN:ID=" + pointerId);
break;
}

// --------------------------------------------------
// 現在のタッチ状態を画面に表示
// --------------------------------------------------
StringBuilder sb = new StringBuilder();
for (int i = 0; i < event.getPointerCount(); i++) {
sb.append("ID:" + event.getPointerId(i)
+ ", x=" + event.getX(i)
+ ", y=" + event.getY(i)
+ "\n");
}
mTextView.setText(sb.toString());

return super.onTouchEvent(event);
}
}



タッチイベントを行うには HTC 端末で注意が必要です。
HTC の一部端末では『HTCジェスチャー』という3本指操作時に『HTCメディアコンパニオン』を起動する機能があり、デフォルトでオンになっています。
これにより、マルチタッチでは2点までしか認識せず、3本指以上でタッチすると onTouchEvent で MotionEvent.ACTION_CANCEL が発生します。

確認端末:HTC J ISW13HT
[設定] - [ディスプレイ、ジェスチャ、ボタン]
Android_HTC_Settings.png

このチェックをはずすとマルチタッチで5点まで認識するようになり、MotionEvent.ACTION_CANCEL も発生しなくなります。
なぜこの特別な設定をデフォルトにしちゃうんでしょうね…

位置情報を取得 : LocationManager

位置情報を取得するには LocationManager を使います。

API Reference: LocationManager, LocationListener, Location
ソフトウェア技術ドキュメントを勝手に翻訳 - f.1 ユーザロケーションの取得

【参考】
逆引きAndroid入門 - 位置情報を取得するには/GPSを使用するには
A Day In The Life - Android の位置情報を確実に取る方法

【使い方】
・LocationManager は Context.getSystemService(Context.LOCATION_SERVICE) で取得する。
・位置情報の更新を開始するには、LocationManager#requestLocationUpdates を呼ぶ。
・位置情報の更新を停止するには、LocationManager#removeUpdates を呼ぶ。
・更新した位置情報の取得は LocationListener インターフェースを実装して行う。
・位置情報が更新されると、LocationListener#onLocationChanged(Location location) が呼ばれる。
・位置情報の取得には AndroidManifest.xml に <uses-permission> を追加する必要がある。
 android.permission.ACCESS_FINE_LOCATION (GPS) 
 android.permission.ACCESS_COARSE_LOCATION (3G / Wi-Fi)
 android.permission.ACCESS_MOCK_LOCATION (DDMS等のテスト用)
 android.permission.INTERNET (ネットワークをベースにする場合に通信許可が必要)

【メモ】
・Android での位置情報は GPS, 3G, Wi-Fi の3種類。
 LocationManager.GPS_PROVIDER (GPS)
 LocationManager.NETWORK_PROVIDER (3G / Wi-Fi)
・Android 2.2 (API 8) から LocationManager.PASSIVE_PROVIDER 追加。
 他のアプリやサービスが位置情報を取得した際に位置情報を流用する。
LocationManager#removeUpdates メソッドを呼ばずにアクティビティを終了すると挙動がおかしくなる
・HTC Desire(Android 2.1) で LocationManager#getBestProvider の不具合があるらしい。

以下、サンプルアプリ。

■ サンプル画面イメージ
Android_LocationManager.png

■ activity_location.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/txtProvider"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/txtLatitude"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/txtLongitude"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/txtAltitude"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/txtAccuracy"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/txtBearing"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/txtSpeed"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/txtTime"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>


■ MyLocationActivity.java
package jp.inujirushi.android.sample;

import android.app.Activity;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.widget.TextView;

public class MyLocationActivity extends Activity implements LocationListener {

private LocationManager mLocationManager;
private TextView mTextProvider; // プロバイダ
private TextView mTextLatitude; // 緯度
private TextView mTextLongitude; // 経度
private TextView mTextAltitude; // 標高
private TextView mTextBearing; // 方位
private TextView mTextSpeed; // 速度
private TextView mTextAccuracy; // 精度
private TextView mTextTime; // 時間

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_location);
mTextProvider = (TextView) findViewById(R.id.txtProvider);
mTextLatitude = (TextView) findViewById(R.id.txtLatitude);
mTextLongitude = (TextView) findViewById(R.id.txtLongitude);
mTextAltitude = (TextView) findViewById(R.id.txtAltitude);
mTextBearing = (TextView) findViewById(R.id.txtBearing);
mTextSpeed = (TextView) findViewById(R.id.txtSpeed);
mTextAccuracy = (TextView) findViewById(R.id.txtAccuracy);
mTextTime = (TextView) findViewById(R.id.txtTime);

// LocationManager を取得する
mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
}

@Override
protected void onResume() {
super.onResume();
if (mLocationManager != null) {
// 位置情報のリクエストを開始する
mLocationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 0, 0, this);
}
}

@Override
protected void onPause() {
super.onPause();
if (mLocationManager != null) {
// 更新が不要であればリクエストを破棄する
mLocationManager.removeUpdates(this);
}
}

// 位置情報が更新されたときに呼ばれる
@Override
public void onLocationChanged(Location location) {
mTextProvider.setText("プロバイダ:" + location.getProvider());
mTextLatitude.setText("緯度:" + location.getLatitude());
mTextLongitude.setText("経度:" + location.getLongitude());
mTextAltitude.setText("標高:" + location.getAltitude());
mTextAccuracy.setText("精度:" + location.getAccuracy());
mTextBearing.setText("方位:" + location.getBearing());
mTextSpeed.setText("速度:" + location.getSpeed());
mTextTime.setText("時間:" + location.getTime());
}

// 位置情報のステータスが変更されたときに呼ばれる
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}

// プロバイダが有効になったときに呼ばれる
@Override
public void onProviderEnabled(String provider) {
}

// プロバイダが無効になったときに呼ばれる
@Override
public void onProviderDisabled(String provider) {
}
}


■ AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.inujirushi.android.sample"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="16" />

<!-- ネットワーク接続 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- GPSからの位置情報取得 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- ネットワークからの位置情報取得 -->
<!-- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> -->
<!-- テスト用の位置情報取得(DDMS等) -->
<!-- <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/> -->

<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="jp.inujirushi.android.sample.MyLocationActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

プロフィール

とむ・やむくん

Author:とむ・やむくん
管理人について

Windows 7 / 64bit
Eclipse 4.2 Juno (日本語パッチ済)

スポンサーサイト
最新トラックバック
検索フォーム
ブロとも申請フォーム
QRコード
QR
Twitter
2013/01/04 19:00 カウント開始