web-dev-qa-db-ja.com

SwipeRefreshLayout + ViewPager、水平スクロールのみを制限しますか?

アプリにSwipeRefreshLayoutViewPagerを実装しましたが、大きな問題があります。ページを切り替えるために左右にスワイプするたびに、スクロールが敏感になりすぎます。少し下にスワイプすると、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>

どんな助けも感謝します、ありがとう

84
user3896501

何も拡張せずに非常に簡単に解決しました

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;
    }
});

魅力のように働く

34
user3896501

この問題が引き続き発生するかどうかはわかりませんが、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()を使用します。

141
nhasan

問題が発生しました。 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);
}

参照を参照してください: link

22
huu duy

私はこれを以前の答えに基づいていましたが、これは少し良くなることがわかりました。モーションは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;
    }
});
10
Sean Abraham

彼らだけが最もよく知っている何らかの理由で、サポートライブラリ開発チームは、強制的にSwipeRefreshLayoutの子レイアウトからすべての垂直ドラッグモーションイベントをインターセプトしました。イベント。彼らがチェックする唯一のことは、そのメインの子の垂直スクロール状態がゼロであることです(その子が垂直にスクロール可能である場合)。 requestDisallowInterceptTouchEvent()メソッドは空のボディで上書きされ、(そうではない)照明のコメント "Nope"があります。

この問題を解決する最も簡単な方法は、サポートライブラリからプロジェクトにクラスをコピーし、メソッドオーバーライドを削除することです。 ViewGroupの実装はonInterceptTouchEvent()の処理に内部状態を使用するため、メソッドを再度オーバーライドして複製することはできません。 オーバーライドサポートライブラリの実装を本当にしたい場合は、requestDisallowInterceptTouchEvent()の呼び出し時にカスタムフラグを設定し、onInterceptTouchEvent()およびonTouchEvent()(または場合によってはcanChildScrollUp()をハックする)それに基づく動作。

9
corsair992

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
}

それは私にとって魅力のようです。

1
Alex Shevelev

Nhasanの解決策には1つの問題があります。

SwipeRefreshLayoutOnPageChangeListenerが既にPull-to-Reloadを認識しているがまだ呼び出していないときに、SwipeRefreshLayoutsetEnabled(false)呼び出しをトリガーする水平スワイプが発生する場合通知コールバック、アニメーションは消えますが、SwipeRefreshLayoutの内部状態は、状態をリセットする可能性のある通知コールバックが呼び出されないため、「更新中」のままです。これは、ユーザーの観点からは、すべてのプルジェスチャが認識されないため、Pull-to-Reloadが機能しなくなったことを意味します。

ここでの問題は、disable(false)呼び出しがスピナーのアニメーションを削除し、通知コールバックがそのスピナーの内部AnimationListenerのonAnimationEndメソッドから呼び出されることです。

確かに、この状況を引き起こすには最速の指を持つテスターが必要でしたが、現実的なシナリオでも同様に発生することがあります。

これを解決する解決策は、次のようにonInterceptTouchEventSwipeRefreshLayoutメソッドをオーバーライドすることです。

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
Nantoka

ViewPagerがSwiprRefreshLayoutに配置されている垂直スクロール可能なコンテナーに配置されている場合、@ huu duy answerに問題がある可能性があります。コンテンツのスクロール可能なコンテナーが完全にスクロールアップされていない場合、同じスクロールアップジェスチャーでスワイプして更新を有効にします。実際、内側のコンテナのスクロールを開始し、意図せずに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 のプロジェクト例を参照してください。

0