SwipeRefreshLayout + ViewPager, 가로 스크롤 만 제한 하시겠습니까?


94

내가 구현 한 SwipeRefreshLayout및 ViewPager내 응용 프로그램에서하지만 큰 문제가 있습니다 : 내가 페이지 사이를 잘 스위치 / 왼쪽으로 스 와이프 갈거야 때마다 스크롤이 너무 민감하다. 아래로 약간 스 와이프하면 SwipeRefreshLayout새로 고침도 트리거됩니다 .

수평 스 와이프가 시작될 때로 제한을 설정 한 다음 스 와이프가 끝날 때까지만 수평으로 강제로 설정하고 싶습니다. 즉, 손가락이 수평으로 움직일 때 수직 스 와이프를 취소하고 싶습니다.

이 문제는에서만 발생합니다. ViewPager아래로 스 와이프하고 SwipeRefreshLayout새로 고침 기능이 트리거 된 다음 (바가 표시됨) 손가락을 가로로 움직여도 세로 스 와이프 만 허용됩니다.

ViewPager클래스 를 확장하려고했지만 전혀 작동하지 않습니다.

public class CustomViewPager extends ViewPager {

    public CustomViewPager(Context ctx, AttributeSet attrs) {
        super(ctx, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean in = super.onInterceptTouchEvent(ev);
        if (in) {
            getParent().requestDisallowInterceptTouchEvent(true);
            this.requestDisallowInterceptTouchEvent(true);
        }
        return false;
    }

}

레이아웃 xml :

<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/viewTopic"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.myapp.listloader.foundation.CustomViewPager
        android:id="@+id/topicViewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>

어떤 도움을 주시면 감사하겠습니다.


뷰 페이지 내의 조각 중 하나에 SwipeRefreshLayout? 가있는 경우 동일한 시나리오가 작동 합니까?
— Zapnologica

답변:


160

여전히이 문제가 있는지 확실하지 않지만 Google I / O 앱 iosched는이 문제를 따라서 해결합니다.

    viewPager.addOnPageChangeListener( new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled( int position, float v, int i1 ) {
        }

        @Override
        public void onPageSelected( int position ) {
        }

        @Override
        public void onPageScrollStateChanged( int state ) {
            enableDisableSwipeRefresh( state == ViewPager.SCROLL_STATE_IDLE );
        }
    } );


private void enableDisableSwipeRefresh(boolean enable) {
    if (swipeContainer != null) {
            swipeContainer.setEnabled(enable);
    }
}

나는 똑같은 것을 사용했고 아주 잘 작동합니다.

편집 : setOnPageChangeListener () 대신 addOnPageChangeListener ()를 사용하십시오.


3
ViewPager의 상태를 고려하므로 이것이 가장 좋은 대답입니다. ViewPager에서 시작되는 아래로 드래그하는 것을 방지하지 않으며, 이는 새로 고침 의도를 분명히 보여줍니다.
— 앤드류 Gallasch

4
그러나 가장 좋은 대답은 enableDisableSwipeRefresh에 대한 코드를 게시하는 것이 좋을 수 있습니다 (예, 함수 이름에서 분명하지만 Google에서 확인해야합니다 ...)
— Greg Ennis

5
완벽하게 작동하지만 setOnPageChangeListener는 이제 감가 상각됩니다. 대신 addOnPageChangeListener를 사용하십시오.
— Yon Kornilov

@nhasan 최근 업데이트 에서이 답변은 더 이상 작동하지 않습니다. swiperefresh의 enabled 상태를 false로 설정하면 swiperefresh가 완전히 제거되지만 이전에는 swiperefresh가 새로 고쳐지는 동안 viewpager의 스크롤 상태가 변경되면 레이아웃이 제거되지 않지만 이전과 동일한 새로 고침 상태를 유지하면서 비활성화됩니다.
— Michael Tedla

2
Google i / o 앱에서 'enableDisableSwipeRefresh'의 소스 코드 링크 : android.googlesource.com/platform/external/iosched/+/HEAD/…
— jpardogo

37

아무것도 확장하지 않고 매우 간단하게 해결

mPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        mLayout.setEnabled(false);
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                mLayout.setEnabled(true);
                break;
        }
        return false;
    }
});

매력처럼 일하다


좋습니다.하지만 최상위 하위 항목에 대해서만 세로 스크롤 을 허용하므로 내부에있을 수있는 모든 스크롤 가능 항목에 대해 이와 동일한 문제 View가 발생할 수 있습니다 (ICS보다 낮은 API에서는 ICS보다 낮은 경우에만 ). . ViewPagerSwipeRefreshLayoutListView
— corsair992 2014 년

당신의 팁 @ corsair992less 감사
— user3896501

@ corsair992가 문제에 직면했습니다. 나는 ViewPager안에 SwipeRefrestLayout있고 ViewPager가 있습니다 Listview! SwipeRefreshLayout아래로 스크롤 해 보겠습니다 만 위로 스크롤하면 새로 고침 진행률이 트리거됩니다. 어떠한 제안?
— Muhammad Babar 2015 년

1
mLayout에 대해 조금 더 개발할 수 있습니까?
— desgraci

2
viewPager.setOnTouchListener {_, event-> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false}
— Axrorxo'ja Yodgorov

22

나는 당신의 문제를 만났습니다. SwipeRefreshLayout을 사용자 지정하면 문제가 해결됩니다.

public class CustomSwipeToRefresh extends SwipeRefreshLayout {

private int mTouchSlop;
private float mPrevX;

public CustomSwipeToRefresh(Context context, AttributeSet attrs) {
    super(context, attrs);

    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mPrevX = MotionEvent.obtain(event).getX();
            break;

        case MotionEvent.ACTION_MOVE:
            final float eventX = event.getX();
            float xDiff = Math.abs(eventX - mPrevX);

            if (xDiff > mTouchSlop) {
                return false;
            }
    }

    return super.onInterceptTouchEvent(event);
}

참조 참조 : 링크


감지에 기울기를 포함하는 것이 가장 좋은 솔루션입니다.
— nafsaka

훌륭한 솔루션 +1
— Tram Nguyen

12

나는 이것을 이전 답변을 기반으로했지만 이것이 조금 더 잘 작동한다는 것을 알았습니다. 모션은 ACTION_MOVE 이벤트로 시작하여 내 경험상 ACTION_UP 또는 ACTION_CANCEL로 끝납니다.

mViewPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mSwipeRefreshLayout.setEnabled(false);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mSwipeRefreshLayout.setEnabled(true);
                break;
        }
        return false;
    }
});

솔루션에 대한 Thnxx
— Hitesh Kushwah

9

그들에게만 가장 잘 알려진 어떤 이유로 지원 라이브러리 개발자 팀은 어린이가 이벤트의 소유권을 구체적으로 요청하더라도의 하위 레이아웃 에서 모든 수직 드래그 모션 이벤트 를 강제로 가로채는 데 적합하다고 생각했습니다 SwipeRefreshLayout. 그들이 확인하는 유일한 것은 기본 자식의 세로 스크롤 상태가 0이라는 것입니다 (자식의 경우 세로로 스크롤 할 수 있음). 이 requestDisallowInterceptTouchEvent()메서드는 빈 본문과 조명 주석 "Nope"로 재정의되었습니다.

이 문제를 해결하는 가장 쉬운 방법은 지원 라이브러리에서 프로젝트로 클래스를 복사하고 메서드 재정의를 제거하는 것입니다. ViewGroup의 구현은 처리를 위해 내부 상태를 사용 onInterceptTouchEvent()하므로 단순히 메서드를 다시 재정의하고 복제 할 수 없습니다. 실제로 지원 라이브러리 구현 을 재정의 하려면를 호출 할 때 사용자 지정 플래그를 설정하고이를 기반으로 동작을 requestDisallowInterceptTouchEvent()재정의 onInterceptTouchEvent()및 onTouchEvent()(또는 해킹 canChildScrollUp())해야합니다.


야, 그건 거칠다. 나는 그들이 그렇게하지 않았 으면 좋았을 것이다. 풀을 새로 고칠 수있는 목록과 행 항목을 스 와이프하는 기능이 있습니다. 그들이 SwipeRefreshLayout을 만든 방식은 약간의 미친 해결 방법 없이는 거의 불가능하게 만듭니다.
— 제시 A. 모리스

3

ViewPager2에 대한 솔루션을 찾았습니다. 다음과 같이 드래그 감도를 줄이기 위해 반사를 사용합니다.

/**
 * Reduces drag sensitivity of [ViewPager2] widget
 */
fun ViewPager2.reduceDragSensitivity() {
    val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
    recyclerViewField.isAccessible = true
    val recyclerView = recyclerViewField.get(this) as RecyclerView

    val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
    touchSlopField.isAccessible = true
    val touchSlop = touchSlopField.get(recyclerView) as Int
    touchSlopField.set(recyclerView, touchSlop*8)       // "8" was obtained experimentally
}

제게는 매력처럼 작동합니다.


2

nhasan 솔루션에는 한 가지 문제가 있습니다.

만약 트리거하는 수평 슬쩍 setEnabled(false)온 전화 SwipeRefreshLayout에서이 OnPageChangeListener일이 (가) SwipeRefreshLayout이미 인식하고 끌어 오기 - 다시로드 투하지만 아직의 알림 콜백, 애니메이션 사라하지만 내부 상태라는되지 SwipeRefreshLayout는 어떠한 영원히 "새로 고침"에 숙박을 상태를 재설정 할 수있는 알림 콜백이 호출됩니다. 사용자 관점에서 이것은 모든 당기기 제스처가 인식되지 않기 때문에 Pull-to-Reload가 더 이상 작동하지 않음을 의미합니다.

여기서 문제는 disable(false)호출이 스피너의 애니메이션을 제거하고 알림 콜백이 해당 onAnimationEnd방식으로 설정된 해당 스피너에 대한 내부 AnimationListener 의 메소드 에서 호출 된다는 것입니다.

이 상황을 유발하는 데 가장 빠른 손가락을 가진 테스터가 필요했지만 현실적인 시나리오에서도 가끔 발생할 수 있습니다.

이 문제를 해결하는 onInterceptTouchEvent방법은 SwipeRefreshLayout다음과 같이 메서드 를 재정의하는 것입니다 .

public class MySwipeRefreshLayout extends SwipeRefreshLayout {

    private boolean paused;

    public MySwipeRefreshLayout(Context context) {
        super(context);
        setColorScheme();
    }

    public MySwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setColorScheme();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (paused) {
            return false;
        } else {
            return super.onInterceptTouchEvent(ev);
        }
    }

    public void setPaused(boolean paused) {
        this.paused = paused;
    }
}

MySwipeRefreshLayout레이아웃에서 사용 -파일 및 mhasan 솔루션의 코드를 다음과 같이 변경하십시오.

...

@Override
public void onPageScrollStateChanged(int state) {
    swipeRefreshLayout.setPaused(state != ViewPager.SCROLL_STATE_IDLE);
}

...

1
나는 또한 당신과 같은 문제가있었습니다. 이 pastebin.com/XmfNsDKQ로 방금 수정 한 nhasan의 솔루션은 새로 고침 할 때 레이아웃을 스 와이프해도 문제가 발생하지 않으므로 확인해야합니다.
— Amit Jayant

0

ViewPager가 SwiprRefreshLayout에 차례로 배치되는 수직 스크롤 가능 컨테이너에 배치 될 때 @huu duy 응답에 문제가있을 수 있습니다. 컨텐츠 스크롤 가능 컨테이너가 완전히 스크롤되지 않은 경우에는 불가능할 수 있습니다. 동일한 스크롤 업 제스처에서 스 와이프하여 새로 고침을 활성화합니다. 실제로 내부 컨테이너를 스크롤하기 시작하고 의도하지 않게 mTouchSlop보다 손가락을 수평으로 움직이면 (기본적으로 8dp) 제안 된 CustomSwipeToRefresh가이 제스처를 거부합니다. 따라서 사용자는 새로 고침을 시작하기 위해 한 번 더 시도해야합니다. 이것은 사용자에게 이상하게 보일 수 있습니다. 지원 라이브러리에서 원본 SwipeRefreshLayout의 소스 코드를 내 프로젝트로 추출하고 onInterceptTouchEvent ()를 다시 작성했습니다.

private float mInitialDownY;
private float mInitialDownX;
private boolean mGestureDeclined;
private boolean mPendingActionDown;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ensureTarget();
    final int action = ev.getActionMasked();
    int pointerIndex;

    if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
        mReturningToStart = false;
    }

    if (!isEnabled() || mReturningToStart || mRefreshing ) {
        // Fail fast if we're not in a state where a swipe is possible
        if (D) Log.e(LOG_TAG, "Fail because of not enabled OR refreshing OR returning to start. "+motionEventToShortText(ev));
        return false;
    }

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());
            mActivePointerId = ev.getPointerId(0);

            if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) >= 0) {

                if (mNestedScrollInProgress || canChildScrollUp()) {
                    if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. Set pending DOWN=true. "+motionEventToShortText(ev));
                    mPendingActionDown = true;
                } else {
                    mInitialDownX = ev.getX(pointerIndex);
                    mInitialDownY = ev.getY(pointerIndex);
                }
            }
            return false;

        case MotionEvent.ACTION_MOVE:
            if (mActivePointerId == INVALID_POINTER) {
                if (D) Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                return false;
            } else if (mGestureDeclined) {
                if (D) Log.e(LOG_TAG, "Gesture was declined previously because of horizontal swipe");
                return false;
            } else if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) < 0) {
                return false;
            } else if (mNestedScrollInProgress || canChildScrollUp()) {
                if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. "+motionEventToShortText(ev));
                return false;
            } else if (mPendingActionDown) {
                // This is the 1-st Move after content stops scrolling.
                // Consider this Move as Down (a start of new gesture)
                if (D) Log.e(LOG_TAG, "Consider this move as down - setup initial X/Y."+motionEventToShortText(ev));
                mPendingActionDown = false;
                mInitialDownX = ev.getX(pointerIndex);
                mInitialDownY = ev.getY(pointerIndex);
                return false;
            } else if (Math.abs(ev.getX(pointerIndex) - mInitialDownX) > mTouchSlop) {
                mGestureDeclined = true;
                if (D) Log.e(LOG_TAG, "Decline gesture because of horizontal swipe");
                return false;
            }

            final float y = ev.getY(pointerIndex);
            startDragging(y);
            if (!mIsBeingDragged) {
                if (D) Log.d(LOG_TAG, "Waiting for dY to start dragging. "+motionEventToShortText(ev));
            } else {
                if (D) Log.d(LOG_TAG, "Dragging started! "+motionEventToShortText(ev));
            }
            break;

        case MotionEvent.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged = false;
            mGestureDeclined = false;
            mPendingActionDown = false;
            mActivePointerId = INVALID_POINTER;
            break;
    }

    return mIsBeingDragged;
}

Github의 예제 프로젝트를 참조하십시오 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.