はじめに (対象読者・この記事でわかること)
この記事は、Android アプリの UI/UX を Java で実装する中級者以上の開発者を対象にしています。
特に「ボタンを押したつもりが、指をずらしてしまったらタップをキャンセルしたい」という要望に悩んでいる方に最適です。
この記事を読むことで、以下のことがわかります。
- onTouchEvent() を使ってタッチ開始・移動・離したときの処理を分岐する方法
- 移動距離が閾値を超えたらイベントを無効化(キャンセル)する実装
- 複数の View で同じロジックを共通化するための簡単な戦略
前提知識
- Android Studio でプロジェクトをビルドできる
- Java の基本文法(匿名クラス、フィールド、メソッド)を理解している
View.OnTouchListenerインタフェースの存在を知っている
なぜ「範囲外ドラッグでキャンセル」が必要なのか
スマホアプリで「タップしたつもりが、わずかに指がズレてしまう」という経験は誰しもあります。
特にリスト項目や小さなボタンが密集している画面では、意図しないタップが発生しやすく、結果として「押したのに反応しない」「押していないのに反応した」とユーザが混乱します。
iOS の標準 UI では「タップ開始後、ある程度指をずらすとタップがキャンセルされる」仕様が組み込まれていますが、Android 標準の Button や TextView にはその機能が存在しません。
そのため Java コードで自前で「移動距離が一定以上ならキャンセル」とする処理を実装する必要があります。
タッチイベントをフックしてキャンセル判定を実装する
ステップ1:カスタム TouchListener を作成する
まず、タッチ座標を記録し、移動距離が閾値を超えたらフラグを立てる CancelableTouchListener を作ります。
Javapublic class CancelableTouchListener implements View.OnTouchListener { private static final int CANCEL_DISTANCE_PX = 80; // キャンセルと判定するピクセル数 private float downX, downY; // タップ開始座標 private boolean canceled = false; // キャンセル済みフラグ private final Runnable onTap; // タップ確定時の処理 public CancelableTouchListener(Runnable onTap) { this.onTap = onTap; } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: downX = event.getRawX(); downY = event.getRawY(); canceled = false; return true; // イベント消費 case MotionEvent.ACTION_MOVE: if (!canceled) { float dx = event.getRawX() - downX; float dy = event.getRawY() - downY; float dist = (float) Math.hypot(dx, dy); if (dist > CANCEL_DISTANCE_PX) { canceled = true; } } return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (!canceled) onTap.run(); // キャンセルされていれば何もしない return true; } return false; } }
ステップ2:View に適用する
Activity や Fragment 内で対象の View にリスナをセットします。
JavaButton button = findViewById(R.id.my_button); button.setOnTouchListener( new CancelableTouchListener(() -> { // タップ確定後の処理 Toast.makeText(this, "タップされました", Toast.LENGTH_SHORT).show(); }));
これだけで「80 px 以上指をずらしたらタップをキャンセル」が実現できます。
ステップ3:複数の View で共通利用する
同じ Activity 内に複数のボタンがある場合、毎回匿名クラスを書くと冗長です。
CancelableTouchListener をフィールドに保持し、各 View でリスナを共有できます。
Javaprivate final View.OnTouchListener sharedListener = new CancelableTouchListener(() -> handleTap()); private void handleTap() { // 共通処理 } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button1).setOnTouchListener(sharedListener); findViewById(R.id.button2).setOnTouchListener(sharedListener); }
ハマった点やエラー解決
-
onTouchでreturn falseにすると、以降のイベントが View 自身に渡ってしまい、リスナ側でキャンセルできない
→ACTION_DOWN時点でreturn trueを必ず返すことで以降のイベントを確実に受け取る -
複数指(マルチタッチ)で
ACTION_POINTER_DOWNが発生した際、event.getX()ではなくevent.getRawX()を使わないと、指ごとの座標がずれる
→ 本記事では簡易のため 1 本指想定だが、本格対応する際はevent.getActionIndex()も考慮する -
キャンセルと判定した後、
ACTION_UPが呼ばれないケースがある(電話着信やシステムダイアログ表示など)
→ACTION_CANCELも拾っておけば安全
解決策
上記のサンプルコードでは、これらの点を踏まえて
- getRawX/Y() で絶対座標を取得
- ACTION_CANCEL も拾ってフラグクリア
- 常に true を返してイベント消費
を実装済みです。
まとめ
本記事では、Android/Java で「タップ開始後、指をずらしてキャンセルする」処理を自前で実装する方法を解説しました。
OnTouchListenerを使えば標準 View でも自由にタッチ処理をカスタマイズできる- 移動距離を閾値と比較してフラグを切り替えるだけで、シンプルにキャンセル判定が作れる
- 複数 View で使い回す際は、同一リスナインスタンスを共有するとコードがスッキリする
この記事を通して、意図しないタップを減らし、より自然なタッチ操作をアプリに組み込めるようになりました。
次回は、Kotlin 拡張関数を使って同機能をより簡潔に書く方法や、ジェスチャー判定に VelocityTracker を活用する高度なテクニックを紹介する予定です。
参考資料
- Android 公式 | Input events overview
- Android Developers | onTouchEvent
- StackOverflow | How to detect a tap or cancel in Android
