社会人1年目エンジニアのブログ

if you can't explain it simply, you don't understand it well enough.

Androidのタッチイベントでつまずいた話

以下のようなイメージスライダーを実装するにあたり、タッチイベントの扱いでつまずいたのでメモ

f:id:chonesu:20161210215226g:plain

大まかな構成

ロジック

viewpagerにimageviewをセット。Handler#postDelayで3秒ごとにViewPager#setCurrentItem(position + 1)を実行
無限ループの仕組みに関しては本題ではないので下記参照

ViewPagerのループ : 時々、失業SEの開発日誌

なにをしたかったか

①ユーザーが画像をスワイプしているときは自動ループ停止
②ユーザーが画像スワイプをやめたら自動ループ再開

うまくいかなかった実装

imageviewにタッチリスナを登録する

imageview.setOnTouchListener(listener);
   @Override
    public boolean onTouch(View v, MotionEvent e) {
        int action = e.getAction();
        if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { // スワイプ中
            // 自動ループ停止の処理

        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { // スワイプ中止
            // 自動ループ再開の処理
        }
        return false;
    }

問題点

この実装だとDownしかよばれず、stackoverflow先生が「downの返り値でtrueを返せばいけるよ!」と教えてくださったので 以下のように変更

   @Override
    public boolean onTouch(View v, MotionEvent e) {
        int action = e.getAction();
        if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { // スワイプ中
            // 自動ループ停止の処理
            return true;

        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { // スワイプ中止
            // 自動ループ再開
        }
        return false;
    }

これでイケるっしょ!!
と思ってログを見ます

スワイプしたときになんでかtoucheventのcancelが呼ばれてしまいます。 諦めて腰を据えて勉強することにしました。

タッチイベントの挙動

Activity#dispatchTouchEvent()
・必ず一番最初に呼ばれる
・もしどのビューもイベントを消化していなかったら自身のonTouchEventを呼び出す

ViewGroup#dispatchTouchEvent()
・onInterceptTouchEvent()が呼ばれる(イベント伝搬を止めるかどうかの判定、trueだと伝搬を止める)
・どの子ビューもイベントを消化してい無い場合はリスナーが呼ばれるOnTouchListener.onTouch()
・リスナーも無い場合は、自身のonTouchEventを呼び出す

View#dispatchTouchEvent()
・もしリスナーが存在していたらそれを呼びだす(View.OnTouchListener.onTouch())
・もしイベントが消化されてなかったら自信のonTouchEventを呼び出す

という流れになっています。

また、ViewGroup# onInterceptTouchEvent()に関して、公式の「 Managing Touch Events in a ViewGroup | Android Developers」を見てみると

If you return true from onInterceptTouchEvent(), the child view that was previously handling touch events receives an ACTION_CANCEL, and the events from that point forward are sent to the parent's onTouchEvent() method for the usual handling
onInterceptTouchEventがtrueを返したら、子ビューのそれまでのタッチイベントはキャンセルされて自身のonTouchEventが呼ばれる

と書いてあります。

ViewPagerの構造

上記を踏まえて、ViewPager#onInterceptTouchEventのドキュメントを見てみます。

        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onMotionEvent will be called and we do the actual
         * scrolling there.
         */

「trueを返したら自身のonTouchEventが呼ばれそこでスクロール処理をする」

とかいてあります。
実際に、onTouchEventの中をみると、スクロールの処理がかいてあります。

※ドキュメントにはonMotionEventとありますが、これは昔の名前らしく、正しくはonTouchEventです。
Googleちゃんと直して。。笑

つまりViewpager(ViewGroup)がonInterceptEvent()でAction==Moveのときはtrueを返しているため、imageviewにセットしたタッチイベントはスワイプをしようとするとキャンセルが呼ばれるのでした。

改良版

viewgroup自体にリスナをセットしてそこでイベントをキャッチします。
子ビューがイベントをハンドリングしていない場合は
View groupの
①onTouchListener.onTouch
→ここでループ処理
②TouchEvent()
→ここでviewpagerのスワイプ処理(androidのコード)

が順番に呼ばれます。これにより、問題なく実装することができました。

めでたしめでたし。

ViewGroupのタッチイベントの挙動はActivityやViewとは違うということがわかり勉強になりました

参考:

Managing Touch Events in a ViewGroup | Android Developers
Android のタッチイベントを理解する(その1) - Unmotivated
http://files.cnblogs.com/files/sunzn/PRE_andevcon_mastering-the-android-touch-system.pdf
Mastering the Android Touch System - YouTube