LoginSignup
6
3

More than 5 years have passed since last update.

スクロールビュー外のジェスチャーをスクロールビューに反映させる (Android/iOS)

Last updated at Posted at 2018-12-31

動機

あまり頻度のあるユースケースではないかもしれませんが、時折、下図のように画面よりもかなり小さいスクロールビューを見せたい時があります(個人的に何度かそういうデザインを実装したことがありました)。しかしスワイプやパンと言った動きは横方向に大きな動きなので、スクロールのジェスチャー自体は画面全体から取りたかったりします。

Japanese.png

Android の場合

GestureDetectoronScroll(MotionEvent, MotionEvent, Float, Float) を実装したものに、外側の view で発生した touch イベントを捕捉させます。そして onScroll(...) 内で得られた横方向の distance をスクロールビューに反映します。

コード例では、スクロールビューというより、ViewPager を利用した場合を用いますが、ScrollView でも RecyclerView でも基本的な考え方は使えると思います。

以下、実装例は Kotlin コードです。

GestureDetector を初期化

    private var isScrolling = false

    private val panGestureDetector = GestureDetectorCompat(activity, object: GestureDetector.SimpleOnGestureListener() {
        override fun onScroll(
            e1: MotionEvent, e2: MotionEvent,
            distanceX: Float, distanceY: Float
        ): Boolean {
            // 1. ここに来るということは間違いなくスクロール中であるので、カスタムフラグを立てる。
            isScrolling = true
            // 2. もしまだ ViewPager に ドラッグを指示していなければ、ドラッグ開始を指示
            if (view?.viewPager?.isFakeDragging() == false) {
                view?.viewPager?.beginFakeDrag()
            }
            // 3. スクロールイベントと反対方向に ViewPager をドラッグ
            view?.viewPager?.fakeDragBy(-distanceX)

            return true
        }

        override fun onDown(e: MotionEvent?): Boolean {
            // 4. onDown イベントをオーバーライドしておかないと、スクロールなど onDown から開始されるイベントを捕捉できないっぽい。
            return true
        }
    })

View.OnTouchListener を作って、外側の view の touch イベントをキャッチ

    private val touchListner = object: View.OnTouchListener {
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            // 1. panGestureDetector が処理できるイベントに関しては panGestureDetector に任せる
            if (panGestureDetector.onTouchEvent(event)) {
                return true
            }

            // 2. ドラッグの後処理。ACTION_UP を受理したということはドラッグが終了したということ。
            if (event?.getAction() == MotionEvent.ACTION_UP) {
                if (isScrolling) {
                    if (view?.viewPager?.isFakeDragging() == true) {
                        // 2.1. ViewPager にドラッグの終了を指示して
                        view?.viewPager?.endFakeDrag()
                    }
                    // 2.2. カスタムフラグをリセット
                    isScrolling = false
                }
            }

            return true
        }
    }

あとは、touchListener をスクロールイベントを捕捉したい外側の view に設定してやるだけです。

    view.setOnTouchListener(touchListner)

結果

final_android.gif

スクロールビューは青い額の中ですが、額の外をスクロールしてもちゃんと額の中がスクロールします。残念ながら 100% というわけではなく、キリンのところで一度空振りしています。

iOS の場合

この記事は、総じてこれが書きたかった感じですが ^^; iOS はこの挙動を非常に簡単に書けます。UIScrollView のスクロールは、それに乗っている UIPanGestureRecognizerUISwipeGestureRecognizer によって管理されています。なので、これらの GestureRecognizer を外側の view に乗せてやれば、あら不思議、まるでスクロールビューのタッチエリアが外側まで拡張されているかのように動きます。

以下、コード例は Swift で書かれています。

        if let gestureRecognizers = scrollView.gestureRecognizers {
            for gestureRecognizer in gestureRecognizers.makeIterator() {
                if gestureRecognizer is UIPanGestureRecognizer || gestureRecognizer is UISwipeGestureRecognizer {
                    // 外側の view に GestureRecognizer を乗せる。
                    view.addGestureRecognizer(gestureRecognizer as UIGestureRecognizer)
                }
            }
        }

結果

final_ios.gif

構造的に、外側のジェスチャーをそのまま中に伝えられるので、挙動自体も iOS の方がスムーズです。

ちなみに、この方法は WWDC 2014 Session 235 で紹介されています。

まとめ

画面中に小さなスクロールビューがあり、外側でスワイプジェスチャーをした場合にもスクロールをさせたい場合、iOS でも Android でも外側のジェスチャーをスクロールビューに送る形で実現できます。

ただし構造的に、iOS の方が圧倒的に簡単に実装できます。また、iOS の場合、UICollectionViewUIScrollView を継承しているため、そちらを採用しても完全に同じコードを使える安心感もあります。Android の場合、ここでは ViewPager を用いましたが、ScrollViewRecyclerView を用いた場合は、ViewPagerfakeDrag() メソッドが使えず、その部分で少し違うコードを書かないといけないでしょう。

Android をディスるつもりは全くないんですが、iOS におけるUIScrollView は画面コンポーネントの中核として、とてもよくデザインされているなあ、という印象を強くしました。(いや、お前の Android の理解が乏しいだけだ、というご指摘があれば是非ご教授ください)

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3