ViewPager 안에 수평 스와이핑 View가 또 있다면?
728x90
반응형

수직 스크롤 안에 수직 스크롤 되는 뷰가 또 있다면 꽤 피곤하다. 보통 이럴 때 사용하는 것이 nestedscrollview이다. 이런건 수직 스크롤에서 발생하는 이슈. 그렇다면 수평인 경우에도 이렇게 골치 아픈 일이 있지 않을까?

아이고 두야....

 

문제는 다음과 같다.

  1. MainActivity 내부에 ViewPager가 있다. ViewPager의 스와이프 기능은 오버라이드된 canScroll()에서 막았다.
  2. ViewPager에는 여러개의 Fragment가 동적으로 붙는다.
  3. Fragment 중 일부는 WebContainer로 구성되어 있고 이 WebContainer는 WebView를 갖는다.
  4. WebView에는 수평 스크롤 View를 가질 수 있다.
  5. Fragment 중 일부는 완전한 네이티브로 구성되어 있고 수평형 RecyclerView와 ViewPager를 갖는다.

결국 1번의 커다란 스와이프 액션에 4번과 5번의 수평 스와이프 액션이 동시에 동작할 수 있는 상황이다. 유저는 1번 혹은 4번의 동작만 선택해서 스와이프를 할 수 없기 때문에 UX적으로 좋지 않은 형태가 된다. 정확히 다음과 같이 두가지 경우가 문제가 된다.

  • ViewPager 내부에 네이티브로 수평 스크롤 View가 있는 경우
  • ViewPager 내부의 웹뷰에 수평 스크롤 View가 있는 경우

이 두가지는 각각 다른 방식으로 해결을 해 보았다. 먼저 ViewPager가 스와이프가 되도록 만드는 것이 먼저다.

 

0. 스와이프가 되는 ViewPager

MainActivity에 ContainerFragment를 하나 두고 여기에 ViewPager를 만들었다. 스와이프를 끄고 켤 수 있도록 CustomViewPager로 만들었다.

public class SwipeableCustomViewPager extends ViewPager {

    public boolean canSwipe = false;
    float actionDownX = -1;

    // 일부 생략
    
    @Override
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        // return값이 true일 경우 해당 ViewPager의 자식뷰의 좌우 스크롤 혹은 스와이프가 가능하다.
        // ViewPager가 스와이프될 때 자식뷰도 좌우로 움직이면 안되기에 아래처럼 구현한다.
        boolean canChildViewsScroll = !canSwipe;
        return canChildViewsScroll;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // 자식 View에게 event가 전달되는 것을 가로챈다.
        // 자식의 touch event보다 우선시 된다.
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                actionDownX = event.getX();
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
            default:
                if (getFocusedChild() instanceof WebContainerView) {
                    if (actionDownX < 200 || actionDownX > (getMeasuredWidth() - 200)) {
                        return canSwipe ? super.onInterceptTouchEvent(event) : false;
                    } else {
                        return false;
                    }
                }
        }

        return canSwipe ? super.onInterceptTouchEvent(event) : false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return canSwipe ? super.onTouchEvent(event) : false;
    }

    public void setSwipeable(boolean canSwipe) {
        this.canSwipe = canSwipe;
    }
}

 

 

1. 일반적인 Fragment에 좌우 스크롤되는 View가 있는 경우

onInterceptTouchEvent가 자식뷰의 터치 이벤트를 막기 때문에 ViewPager의 스와이프가 터치 이벤트에 의해 동작할 수 있다. 하지만 좌우로 동작하는 RecyclerView 혹은 ViewPager가 Fragment 내부에 있다면? requestDisallowInterceptTouchEvent를 사용하면 편하다. 부모로부터 이벤트를 Intercept, 즉 가로채이지 않도록 요청하는 함수로서 true를 입력하면 부모 스크롤뷰 내부의 자식 List가 스크롤을 사용할 수 있다. 아래 코드로 이해가 가지 않는다면 mantdu 블로그 글을 참고해보자.

 

// Java, ViewPager 버전
public class DisallowInterceptViewPager extends ViewPager {

    // 일부 생략 
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 해당 뷰페이저에만 터치 이벤트 발생, 부모 뷰그룹은 이벤트 Block
        ViewParent parent = getParent();
        parent.requestDisallowInterceptTouchEvent(true);

        return super.dispatchTouchEvent(ev);
    }
}
// Kotlin, RecyclerView 버전
class DisallowInterceptRecyclerView : RecyclerView {

    // 일부 생략

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        // 해당 뷰페이저에만 터치 이벤트 발생, 부모 뷰그룹은 이벤트 Block
        parent.requestDisallowInterceptTouchEvent(true)
        return super.dispatchTouchEvent(ev)
    }
}

 

2. WebView가 Container 형태로 Fragment에 구현되어 있을 경우

WebView에는 좌우 스크롤 뷰가 있다는 가정을 한다. 즉 모바일 웹에서 좌우 스크롤 View가 있는데 그건 네이티브에서 제어하기 쉽지 않다. 그래서 약간의 꼼수(?)를 사용했다. 0번에 보여진 코드에서 getMeasuredWidth를 계산해서 return값을 달리하는 부분이 있다. 디바이스 전체 스크린의 좌우 일부분(여기서는 200px)만 ViewPager 스와이프가 되는 터치 이벤트를 발생시키고 그 안쪽으로는 웹뷰의 터치이벤트로 모바일웹 안 좌우 스크롤 View가 동작되도록 하였다.

 

3. ViewPager 내부의 WebContainer는 어떻게 구분했을까?

WebView를 감싸고 있는 Container가 존재하는데 ViewPager에서 내부 Fragment들이 무엇인지 알기란 쉽지 않다. 그래서 0번 코드에서와 같이 getFocusedChild를 사용하여 Focus된 Fragment 내부 전체를 감싸고 있는 WebContainer를 instanceOf로 구분하였다. 이렇게 웹뷰로 구성된 Fragment와 네이티브로만 구현된 Fragment를 구분하였다. getFocusedChild 사용은 StackOverFlow를 참고하였다.

 

참고자료
웹뷰, 전체뷰를 가로 스크롤할 수 있는 ViewPager를 구현하고자 하거나, 위 방식과 다른 네이티브 내부에서 웹뷰 가로 스크롤 구현이 알고 싶다면 아래 링크를 참고하길 바란다. (참고로 나에게는 도움이 되지 않았지만 공부는 되었다.)
- Zerolism의 생각하는 개발자 : https://zerolism.tistory.com/87
- Life in Hong Kong : https://nexters.me/Fc 

부모와 자식뷰 간 TouchEvent 흐름, 그리고 Touch event 종류, Event 막기
- 번데기 개발자 : https://jw910911.tistory.com/62
- 돼지왕 왕돼지 : https://aroundck.tistory.com/839
- 장범석님 개발일지 : http://dktfrmaster.blogspot.com/2016/09/blog-post_26.html
- Moka : https://moka.land/android/touch-event-transfer/
- StackOverFlow : https://nexters.me/Gc 
- 83년생 : https://nexters.me/Hc
728x90
반응형