RecyclerView scroll horizontal snap no centro

89

Estou tentando fazer uma exibição semelhante a um carrossel aqui usando o RecyclerView, quero que o item se encaixe no meio da tela ao rolar, um item por vez. Eu tentei usarrecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

mas a exibição ainda está rolando suavemente, também tentei implementar minha própria lógica usando o ouvinte de rolagem assim:

recyclerView.setOnScrollListener(new OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    Log.v("Offset ", recyclerView.getWidth() + "");
                    if (newState == 0) {
                        try {
                               recyclerView.smoothScrollToPosition(layoutManager.findLastVisibleItemPosition());
                                recyclerView.scrollBy(20,0);
                            if (layoutManager.findLastVisibleItemPosition() >= recyclerView.getAdapter().getItemCount() - 1) {
                                Beam refresh = new Beam();
                                refresh.execute(createUrl());
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

Deslizar da direita para a esquerda está funcionando bem agora, mas não o contrário, o que estou perdendo aqui?

Ahmed Awad
fonte

Respostas:

165

Com LinearSnapHelper, isso agora é muito fácil.

Tudo que você precisa fazer é o seguinte:

SnapHelper helper = new LinearSnapHelper();
helper.attachToRecyclerView(recyclerView);

Atualizar

Disponível desde 25.1.0, PagerSnapHelperpode ajudar a obter o ViewPagermesmo efeito. Use-o como você usaria o LinearSnapHelper.

Antiga solução alternativa:

Se você deseja que ele se comporte da mesma forma que o ViewPager, tente o seguinte:

LinearSnapHelper snapHelper = new LinearSnapHelper() {
    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
        View centerView = findSnapView(layoutManager);
        if (centerView == null) 
            return RecyclerView.NO_POSITION;

        int position = layoutManager.getPosition(centerView);
        int targetPosition = -1;
        if (layoutManager.canScrollHorizontally()) {
            if (velocityX < 0) {
                targetPosition = position - 1;
            } else {
                targetPosition = position + 1;
            }
        }

        if (layoutManager.canScrollVertically()) {
            if (velocityY < 0) {
                targetPosition = position - 1;
            } else {
                targetPosition = position + 1;
            }
        }

        final int firstItem = 0;
        final int lastItem = layoutManager.getItemCount() - 1;
        targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
        return targetPosition;
    }
};
snapHelper.attachToRecyclerView(recyclerView);

A implementação acima apenas retorna a posição próxima ao item atual (centralizado) com base na direção da velocidade, independentemente da magnitude.

O primeiro é uma solução original incluída na Biblioteca de Suporte versão 24.2.0. O que significa que você tem que adicionar isso ao seu módulo de aplicativo build.gradleou atualizá-lo.

compile "com.android.support:recyclerview-v7:24.2.0"
Razzle Dazzle
fonte
Isso não funcionou para mim (mas a solução de @Albert Vila Calvo funcionou). Você o implementou? Devemos substituir métodos a partir dele? Eu acho que a classe deve fazer todo o trabalho fora da caixa
galaxigirl
Sim, daí a resposta. Eu não testei completamente, no entanto. Para ser claro, fiz isso com uma horizontal em LinearLayoutManagerque todas as visualizações eram de tamanho regular. Nada mais além do trecho acima é necessário.
razzledazzle
@galaxigirl desculpas, eu entendi o que você quis dizer e fiz algumas edições reconhecendo isso, por favor, comente. Esta é uma solução alternativa com o LinearSnapHelper.
razzledazzle
Isso funcionou perfeitamente para mim. @galaxigirl sobrescrever este método ajuda no sentido de que o auxiliar de snap linear irá, de outra forma, se ajustar ao item mais próximo quando a rolagem estiver se acomodando, não exatamente apenas o item seguinte / anterior. Muito obrigado por isso, razzledazzle
AA_PV
LinearSnapHelper foi a solução inicial que funcionou para mim, mas parece que PagerSnapHelper deu uma melhor animação ao deslizar.
LeonardoSibela
77

Atualização do Google I / O 2019

ViewPager2 está aqui!

O Google acaba de anunciar na palestra 'Novidades no Android' (também conhecido como 'The Android keynote') que estão trabalhando em um novo ViewPager baseado no RecyclerView!

Dos slides:

Como o ViewPager, mas melhor

  • Migração fácil do ViewPager
  • Baseado em RecyclerView
  • Suporte para modo da direita para a esquerda
  • Permite paginação vertical
  • Notificações de mudança de conjunto de dados aprimoradas

Você pode verificar a versão mais recente aqui e as notas de lançamento aqui . Também existe uma amostra oficial .

Opinião pessoal: Acho que é uma adição muito necessária. Recentemente, tive muitos problemas com a PagerSnapHelper oscilação da esquerda para a direita indefinidamente - veja o tíquete que abri.


Nova resposta (2016)

Agora você pode apenas usar um SnapHelper .

Se você deseja um comportamento de alinhamento centralizado semelhante ao ViewPager , use PagerSnapHelper :

SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

Também existe um LinearSnapHelper . Eu tentei e se você arremessar com energia, ele rola 2 itens com 1 arremesso. Pessoalmente, não gostei, mas decida por si mesmo - tentar leva apenas alguns segundos.


Resposta original (2016)

Depois de muitas horas tentando três soluções diferentes encontradas aqui no SO, finalmente construí uma solução que imita muito de perto o comportamento encontrado em a ViewPager.

A solução é baseada na solução @eDizzle , que acredito ter melhorado o suficiente para dizer que funciona quase como um ViewPager.

Importante: a RecyclerViewlargura dos meus itens é exatamente igual à da tela. Não tentei com outros tamanhos. Também uso com uma horizontal LinearLayoutManager. Acho que você precisará adaptar o código se quiser rolagem vertical.

Aqui você tem o código:

public class SnappyRecyclerView extends RecyclerView {

    // Use it with a horizontal LinearLayoutManager
    // Based on https://stackoverflow.com/a/29171652/4034572

    public SnappyRecyclerView(Context context) {
        super(context);
    }

    public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean fling(int velocityX, int velocityY) {

        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

        int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;

        // views on the screen
        int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
        View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
        int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
        View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);

        // distance we need to scroll
        int leftMargin = (screenWidth - lastView.getWidth()) / 2;
        int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
        int leftEdge = lastView.getLeft();
        int rightEdge = firstView.getRight();
        int scrollDistanceLeft = leftEdge - leftMargin;
        int scrollDistanceRight = rightMargin - rightEdge;

        if (Math.abs(velocityX) < 1000) {
            // The fling is slow -> stay at the current page if we are less than half through,
            // or go to the next page if more than half through

            if (leftEdge > screenWidth / 2) {
                // go to next page
                smoothScrollBy(-scrollDistanceRight, 0);
            } else if (rightEdge < screenWidth / 2) {
                // go to next page
                smoothScrollBy(scrollDistanceLeft, 0);
            } else {
                // stay at current page
                if (velocityX > 0) {
                    smoothScrollBy(-scrollDistanceRight, 0);
                } else {
                    smoothScrollBy(scrollDistanceLeft, 0);
                }
            }
            return true;

        } else {
            // The fling is fast -> go to next page

            if (velocityX > 0) {
                smoothScrollBy(scrollDistanceLeft, 0);
            } else {
                smoothScrollBy(-scrollDistanceRight, 0);
            }
            return true;

        }

    }

    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);

        // If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
        // This code fixes this. This code is not strictly necessary but it improves the behaviour.

        if (state == SCROLL_STATE_IDLE) {
            LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

            int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;

            // views on the screen
            int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
            View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
            int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
            View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);

            // distance we need to scroll
            int leftMargin = (screenWidth - lastView.getWidth()) / 2;
            int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
            int leftEdge = lastView.getLeft();
            int rightEdge = firstView.getRight();
            int scrollDistanceLeft = leftEdge - leftMargin;
            int scrollDistanceRight = rightMargin - rightEdge;

            if (leftEdge > screenWidth / 2) {
                smoothScrollBy(-scrollDistanceRight, 0);
            } else if (rightEdge < screenWidth / 2) {
                smoothScrollBy(scrollDistanceLeft, 0);
            }
        }
    }

}

Apreciar!

Albert Vila Calvo
fonte
Bom pegar o onScrollStateChanged (); caso contrário, um pequeno bug estará presente se deslizarmos lentamente o RecyclerView. Obrigado!
Mauricio Sartori
Para oferecer suporte às margens (android: layout_marginStart = "16dp"), tentei com int screenwidth = getWidth (); E parece que funciona.
Eigil
AWESOOOMMEEEEEEEEE
raditya gumay
1
@galaxigirl não, ainda não experimentei o LinearSnapHelper. Acabei de atualizar a resposta para que as pessoas conheçam a solução oficial. Minha solução funciona sem problemas no meu aplicativo.
Albert Vila Calvo
1
@AlbertVilaCalvo Eu tentei LinearSnapHelper. LinearSnapHelper funciona com um Scroller. Ele rola e se encaixa com base na gravidade e na velocidade de arremesso. Quanto maior a velocidade, mais páginas são roladas. Eu não o recomendaria para um comportamento semelhante ao do ViewPager.
mipreamble
47

Se o objetivo é RecyclerViewimitar o comportamento de, ViewPagerhá uma abordagem bastante fácil

RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);

LinearLayoutManager layoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
SnapHelper snapHelper = new PagerSnapHelper();
recyclerView.setLayoutManager(layoutManager);
snapHelper.attachToRecyclerView(mRecyclerView);

Ao usar PagerSnapHelpervocê pode obter o comportamento comoViewPager

Boonya Kitpitak
fonte
4
Estou usando em LinearSnapHelpervez de PagerSnapHelper e funciona para mim
fanjavaid de
1
Sim, não há efeito instantâneo emLinearSnapHelper
Boonya Kitpitak
1
isso fixa o item na visualização permite scrolable
Sam
@BoonyaKitpitak, tentei LinearSnapHelper, e ele se encaixa em um item do meio. PagerSnapHelperdificulta a rolagem fácil (pelo menos, uma lista de imagens).
CoolMind
@BoonyaKitpitak você pode me dizer como fazer isso, um item no centro totalmente visível e os itens laterais estão parcialmente visíveis?
Rucha Bhatt Joshi
30

Você precisa usar findFirstVisibleItemPosition para ir na direção oposta. E para detectar em qual direção o golpe estava, você precisará obter a velocidade de arremesso ou a mudança em x. Abordei esse problema de um ângulo ligeiramente diferente do seu.

Crie uma nova classe que estenda a classe RecyclerView e, em seguida, substitua o método fling de RecyclerView da seguinte maneira:

@Override
public boolean fling(int velocityX, int velocityY) {
    LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

//these four variables identify the views you see on screen.
    int lastVisibleView = linearLayoutManager.findLastVisibleItemPosition();
    int firstVisibleView = linearLayoutManager.findFirstVisibleItemPosition();
    View firstView = linearLayoutManager.findViewByPosition(firstVisibleView);
    View lastView = linearLayoutManager.findViewByPosition(lastVisibleView);

//these variables get the distance you need to scroll in order to center your views.
//my views have variable sizes, so I need to calculate side margins separately.     
//note the subtle difference in how right and left margins are calculated, as well as
//the resulting scroll distances.
    int leftMargin = (screenWidth - lastView.getWidth()) / 2;
    int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
    int leftEdge = lastView.getLeft();
    int rightEdge = firstView.getRight();
    int scrollDistanceLeft = leftEdge - leftMargin;
    int scrollDistanceRight = rightMargin - rightEdge;

//if(user swipes to the left) 
    if(velocityX > 0) smoothScrollBy(scrollDistanceLeft, 0);
    else smoothScrollBy(-scrollDistanceRight, 0);

    return true;
}
eDizzle
fonte
Onde screenWidth é definido?
Itsai
@Itsai: Oops! Boa pergunta. É uma variável que defini em outra parte da classe. É a largura da tela do dispositivo em pixels, não pixels de densidade, uma vez que o método smoothScrollBy requer o número de pixels que você deseja percorrer.
eDizzle
3
@ltsai Você pode alterar para getWidth () (recyclerview.getWidth ()) em vez de screenWidth. screenWidth não funciona corretamente quando o RecyclerView é menor que a largura da tela.
VAdaihiep
Tentei estender o RecyclerView, mas estou recebendo "Erro inflating class custom RecyclerView". Você pode ajudar? Obrigado
@bEtTyBarnes: Você pode querer pesquisar isso em outro feed de pergunta. Está muito fora do escopo da pergunta inicial aqui.
eDizzle
6

Basta adicionar paddinge margina recyclerViewe recyclerView item:

recyclerView item:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parentLayout"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_marginLeft="8dp" <!-- here -->
    android:layout_marginRight="8dp" <!-- here  -->
    android:layout_width="match_parent"
    android:layout_height="200dp">

   <!-- child views -->

</RelativeLayout>

recyclerView:

<androidx.recyclerview.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="8dp" <!-- here -->
    android:paddingRight="8dp" <!-- here -->
    android:clipToPadding="false" <!-- important!-->
    android:scrollbars="none" />

e definir PagerSnapHelper:

int displayWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
parentLayout.getLayoutParams().width = displayWidth - Utils.dpToPx(16) * 4;
SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

dp para px:

public static int dpToPx(int dp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}

resultado:

insira a descrição da imagem aqui

Farzad
fonte
Existe uma maneira de obter preenchimento variável entre os itens? como se a primeira e a última carta não precisassem ser centralizadas para mostrar mais das cartas seguinte / anterior versus as cartas intermediárias sendo centralizadas?
RamPrasadBismil
2

Minha solução:

/**
 * Horizontal linear layout manager whose smoothScrollToPosition() centers
 * on the target item
 */
class ItemLayoutManager extends LinearLayoutManager {

    private int centeredItemOffset;

    public ItemLayoutManager(Context context) {
        super(context, LinearLayoutManager.HORIZONTAL, false);
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
        LinearSmoothScroller linearSmoothScroller = new Scroller(recyclerView.getContext());
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

    public void setCenteredItemOffset(int centeredItemOffset) {
        this.centeredItemOffset = centeredItemOffset;
    }

    /**
     * ********** Inner Classes **********
     */

    private class Scroller extends LinearSmoothScroller {

        public Scroller(Context context) {
            super(context);
        }

        @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            return ItemLayoutManager.this.computeScrollVectorForPosition(targetPosition);
        }

        @Override
        public int calculateDxToMakeVisible(View view, int snapPreference) {
            return super.calculateDxToMakeVisible(view, SNAP_TO_START) + centeredItemOffset;
        }
    }
}

Passo este gerenciador de layout para o RecycledView e defino o deslocamento necessário para centralizar os itens. Todos os meus itens têm a mesma largura, então o deslocamento constante está ok

anagaf
fonte
0

PagerSnapHelpernão funciona com GridLayoutManagerspanCount> 1, então minha solução nesta circunstância é:

class GridPagerSnapHelper : PagerSnapHelper() {
    override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager?, velocityX: Int, velocityY: Int): Int {
        val forwardDirection = if (layoutManager?.canScrollHorizontally() == true) {
            velocityX > 0
        } else {
            velocityY > 0
        }
        val centerPosition = super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
        return centerPosition +
            if (forwardDirection) (layoutManager as GridLayoutManager).spanCount - 1 else 0
    }
}
Wenqing MA
fonte