SwipeRefreshLayout + ViewPager, limitar a rolagem horizontal apenas?

94

Eu implementei SwipeRefreshLayoute ViewPagerem meu aplicativo, mas há um grande problema: sempre que vou deslizar para a esquerda / direita para alternar entre as páginas, a rolagem é muito sensível. Um pequeno deslizar para baixo também acionará a SwipeRefreshLayoutatualização.

Quero definir um limite para o início do deslizamento horizontal e, em seguida, forçar na horizontal apenas até o final do deslizamento. Em outras palavras, desejo cancelar o deslizamento vertical quando o dedo estiver se movendo horizontalmente.

Esse problema ocorre apenas em ViewPager, se eu deslizar para baixo e a SwipeRefreshLayoutfunção de atualização for acionada (a barra é mostrada) e, em seguida, mover meu dedo horizontalmente, ainda permite apenas deslizamentos verticais.

Tentei estender a ViewPageraula, mas não está funcionando de jeito nenhum:

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

}

Layout 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>

qualquer ajuda seria apreciada, obrigado

user3896501
fonte
O mesmo cenário funciona se um de seus fragmentos dentro do viewpager tiver um SwipeRefreshLayout?
Zapnologica

Respostas:

160

Não tenho certeza se você ainda tem esse problema, mas o aplicativo Google I / O iosched resolve o problema da seguinte maneira:

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

Eu usei o mesmo e funciona muito bem.

EDIT: Use addOnPageChangeListener () em vez de setOnPageChangeListener ().

Nhasan
fonte
3
Esta é a melhor resposta porque leva em consideração o estado do ViewPager. Isso não evita um arrastar para baixo que se origina no ViewPager, o que demonstra claramente a intenção de atualizar.
Andrew Gallasch
4
Melhor resposta, no entanto, pode ser bom postar o código para enableDisableSwipeRefresh (sim, é óbvio pelo nome da função .. mas para ter certeza de que eu tive que pesquisar no Google ...)
Greg Ennis
5
Funciona perfeitamente, mas setOnPageChangeListener está depreciado agora. use addOnPageChangeListener em vez disso.
Yon Kornilov
@nhasan Em uma atualização recente, essa resposta não funciona mais. Definir o estado habilitado de swiperefresh como false remove o swiperefresh inteiramente, enquanto antes, se o estado de rolagem do viewpager mudasse enquanto o swiperefresh está atualizando, ele não removeria o layout, mas o desabilitaria enquanto o mantinha no mesmo estado de atualização que estava antes.
Michael Tedla,
2
Link para o código-fonte de 'enableDisableSwipeRefresh' no aplicativo google i / o: android.googlesource.com/platform/external/iosched/+/HEAD/…
jpardogo
37

Resolvido de forma muito simples, sem estender nada

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

trabalhe como um encanto

user3896501
fonte
Ok, mas lembre-se de que você terá o mesmo problema para qualquer rolável Viewque possa ter dentro de ViewPager, pois SwipeRefreshLayoutpermite até mesmo a rolagem vertical apenas para seu filho de nível superior (e em APIs inferiores a ICS apenas se for um ListView) .
corsair992
@ corsair992less Obrigado por suas dicas
user3896501
@ corsair992 enfrentando o problema. Eu tenho ViewPagerdentro SwipeRefrestLayoute o ViewPager tem Listview! SwipeRefreshLayoutdeixe-me rolar para baixo, mas ao rolar para cima isso aciona o progresso da atualização. Alguma sugestão?
Muhammad Babar
1
você pode desenvolver um pouco mais sobre o que é mLayout?
desgraci
2
viewPager.setOnTouchListener {_, event -> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false}
Axrorxo'ja Yodgorov
22

Eu conheci seu problema. Personalizar o SwipeRefreshLayout resolveria o problema.

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

Veja a referência: link

huu duy
fonte
Esta é a melhor solução para incluir a inclinação na detecção.
nafsaka
Ótima solução +1
Tram Nguyen
12

Eu baseei isso em uma resposta anterior, mas achei que funcionava um pouco melhor. O movimento começa com um evento ACTION_MOVE e termina em ACTION_UP ou ACTION_CANCEL na minha experiência.

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;
    }
});
Sean Abraham
fonte
Thnxx para a solução
Hitesh Kushwah
9

Por algum motivo mais conhecido apenas por eles, a equipe de desenvolvedores da biblioteca de suporte decidiu interceptar à força todos os eventos de movimento de arrastar vertical do SwipeRefreshLayoutlayout filho, mesmo quando uma criança solicita especificamente a propriedade do evento. A única coisa que eles verificam é se o estado de rolagem vertical de seu filho principal está em zero (no caso de seu filho ser rolável verticalmente). O requestDisallowInterceptTouchEvent()método foi substituído por um corpo vazio e o (nem tanto) comentário esclarecedor "Não".

A maneira mais fácil de resolver esse problema seria apenas copiar a classe da biblioteca de suporte em seu projeto e remover a substituição do método. ViewGroupA implementação de usa estado interno para manipulação onInterceptTouchEvent(), portanto, você não pode simplesmente sobrescrever o método novamente e duplicá-lo. Se você realmente deseja sobrescrever a implementação da biblioteca de suporte, então você terá que configurar um sinalizador customizado nas chamadas para requestDisallowInterceptTouchEvent(), e sobrescrever onInterceptTouchEvent()e onTouchEvent()(ou possivelmente hackear canChildScrollUp()) o comportamento baseado nisso.

corsair992
fonte
Cara, isso é difícil. Eu realmente gostaria que eles não tivessem feito isso. Eu tenho uma lista que desejo ativar para atualizar e puxar os itens da linha. A maneira como eles criaram o SwipeRefreshLayout torna isso quase impossível sem algumas soluções malucas.
Jessie A. Morris
3

Eu encontrei uma solução para ViewPager2. Eu uso reflexão para reduzir a sensibilidade ao arrasto assim:

/**
 * 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
}

Funciona como um encanto para mim.

Alex Shevelev
fonte
2

Existe um problema com a solução de nhasan:

Se o deslize horizontal que aciona a setEnabled(false)chamada no SwipeRefreshLayoutno OnPageChangeListeneracontece quando o SwipeRefreshLayoutjá reconheceu um Pull-to-Reload, mas ainda não chamou o retorno de chamada de notificação, a animação desaparece, mas o estado interno doSwipeRefreshLayout permanece em "atualizando" para sempre como não retornos de chamada de notificação são chamados para redefinir o estado. Da perspectiva do usuário, isso significa que Pull-to-Reload não está mais funcionando, pois todos os gestos de pull não são reconhecidos.

O problema aqui é que a disable(false)chamada remove a animação do botão giratório e o retorno de chamada de notificação é chamado doonAnimationEnd método de um AnimationListener interno para esse botão giratório que está configurado fora de ordem dessa maneira.

Admitimos que nosso testador com os dedos mais rápidos provocou essa situação, mas também pode acontecer de vez em quando em cenários realistas.

Uma solução para corrigir isso é substituir o onInterceptTouchEventmétodo da SwipeRefreshLayoutseguinte maneira:

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

Use MySwipeRefreshLayoutem seu Layout - Arquivo e altere o código na solução de mhasan para

...

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

...
Nantoka
fonte
1
Eu também tive o mesmo problema que o seu. Acabei de modificar a solução de nhasan com este pastebin.com/XmfNsDKQ Note layout de atualização de furto não cria problemas quando é atualizado, então é por isso que a verificação.
Amit Jayant
0

Pode haver um problema com a resposta @huu duy quando o ViewPager é colocado em um contêiner rolável verticalmente que, por sua vez, é colocado no SwiprRefreshLayout. Se o contêiner rolável de conteúdo não estiver totalmente rolado para cima, então pode não ser possível ative deslizar para atualizar no mesmo gesto de rolar para cima. Na verdade, quando você começa a rolar o contêiner interno e move o dedo horizontalmente mais do que mTouchSlop involuntariamente (que é 8dp por padrão), o CustomSwipeToRefresh proposto recusa esse gesto. Portanto, o usuário deve tentar mais uma vez para iniciar a atualização. Isso pode parecer estranho para o usuário. Extraí o código-fonte do SwipeRefreshLayout original da biblioteca de suporte para meu projeto e reescrevi o 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;
}

Veja meu projeto de exemplo no Github .

Stanislav Perchenko
fonte