스크롤 뷰 외부의 제스처를 스크롤 뷰에 반영(Android/iOS)

동기



빈번한 유스 케이스가 아닐지도 모르지만, 때때로 아래 그림과 같이 화면보다 상당히 작은 스크롤 뷰를 보여주고 싶을 때가 있습니다 (개인적으로 여러 번 그러한 디자인을 구현 한 적이있었습니다) . 그러나 스와이프나 빵이라고 하는 움직임은 가로 방향으로 큰 움직임이므로, 스크롤의 제스처 자체는 화면 전체로부터 취하고 싶거나 합니다.



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를 만들고 외부 뷰의 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)

결과





스크롤 뷰는 파란 이마 속입니다만, 이마 밖을 스크롤해도 제대로 이마 안이 스크롤 합니다. 불행히도 100%라는 것은 아니고, 기린의 곳에서 한 번 공 흔들고 있습니다.

iOS의 경우



이 기사는, 일반적으로 이것이 쓰고 싶었던 느낌입니다만 ^^; iOS 는 이 거동을 매우 간단하게 쓸 수 있습니다. UIScrollView 의 스크롤은 UIPanGestureRecognizerUISwipeGestureRecognizer 에 의해 관리됩니다. 그래서 이러한 GestureRecognizer 를 바깥 뷰에 올려 놓으면, 마치 이상하게, 마치 스크롤 뷰의 터치 영역이 바깥쪽까지 확장되어 있는 것처럼 움직입니다.

아래 코드 예제는 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)
                }
            }
        }

결과





구조적으로 외부 제스처를 그대로 안에 전해지므로 거동 자체도 iOS 쪽이 원활합니다.

그건 그렇고,이 방법은 WWDC 2014 세션 235에서 소개되었습니다.

요약



화면에 작은 스크롤 뷰가 있고 외부에서 스와이프 제스처를 한 경우에도 스크롤을 하고 싶은 경우, iOS에서도 Android에서도 외부 제스처를 스크롤 뷰로 보내는 형태로 실현할 수 있습니다.

그러나 구조적으로 iOS는 압도적으로 쉽게 구현할 수 있습니다. 또, iOS 의 경우, UICollectionViewUIScrollView 를 계승하고 있기 때문에, 그쪽을 채용해도 완전히 같은 코드를 사용할 수 있는 안심감도 있습니다. Android의 경우 여기에서는 ViewPager를 사용했지만 ScrollView 또는 RecyclerView 하지 마세요.

안드로이드를 할 생각은 전혀 없습니다만, iOS 에서 ViewPager 는 화면 컴퍼넌트의 핵심으로서, 매우 잘 디자인되고 있다고 하는 인상을 강하게 했습니다. (아니, 안드로이드의 이해가 부족할 뿐이라는 지적이 있으면 꼭 교수해 주세요)

좋은 웹페이지 즐겨찾기