Detectar quando o RecyclerView atinge a posição mais inferior durante a rolagem

106

Eu tenho este código para um RecyclerView.

    recyclerView = (RecyclerView)rootview.findViewById(R.id.fabric_recyclerView);
    recyclerView.setLayoutManager(layoutManager);
    recyclerView.addItemDecoration(new RV_Item_Spacing(5));
    FabricAdapter fabricAdapter=new FabricAdapter(ViewAdsCollection.getFabricAdsDetailsAsArray());
    recyclerView.setAdapter(fabricAdapter);

Preciso saber quando o RecyclerView atinge a posição mais inferior durante a rolagem. É possível ? Se sim, como?

Adrian
fonte
1
recyclerView.canScrollVertically (direção interna); Qual deve ser o parâmetro assim que temos que passar aqui?
Adrian
@Adrian, caso você ainda esteja interessado nesta questão :)))) stackoverflow.com/a/48514857/6674369
Andriy Antonov

Respostas:

186

também há uma maneira simples de fazer isso

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);

        if (!recyclerView.canScrollVertically(1)) {
            Toast.makeText(YourActivity.this, "Last", Toast.LENGTH_LONG).show();

        }
    }
});

inteiros de direção: -1 para cima, 1 para baixo, 0 sempre retornará falso.

Kaustubh Bhagwat
fonte
17
onScrolled () evita que a detecção aconteça duas vezes.
Ian Wambai
Não consigo adicionar ScrollListnerEvent ao My RecyclerView. O código em onScrollStateChanged não é executado.
Sandeep Yohans
1
se você quiser que apenas isso seja acionado uma vez (mostrar o Toast uma vez), adicione um sinalizador booleano (uma variável de membro de classe) à instrução if e defina-o como verdadeiro dentro de se: if (!recyclerView.canScrollVertically(1) && !mHasReachedBottomOnce) { mHasReachedBottomOnce = true Toast.makeText(YourActivity.this, "Last", Toast.LENGTH_LONG).show();}
bastami82
@ bastami82 eu uso o truque como você sugere para evitar duas impressões de torradas, mas não funciona
Vishwa Pratap
4
Adicionar newState == RecyclerView.SCROLL_STATE_IDLEà ifdeclaração funcionaria.
Lee Chun Hoe
73

Use este código para evitar chamadas repetidas

    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);

            if (!recyclerView.canScrollVertically(1) && newState==RecyclerView.SCROLL_STATE_IDLE) {
                Log.d("-----","end");
                
            }
        }
    });
Milind Chaudhary
fonte
2
Eu tentei pode responder, mas isso é simples e perfeito para mim. obrigado
Vishwa Pratap
@Venkatesh Bem-vindo.
Milind Chaudhary
A única resposta que não me deu nenhum outro bug
The Fluffy T Rex
Obrigado @TheFluffyTRex
Milind Chaudhary
Cuidado pessoal! Isso também será acionado quando você tentar deslizar para cima com poucos elementos no reciclador. Ele também irá disparar DUAS VEZES em relação a esse novo Estado ser negligenciado.
Ammar Mohammad
47

Basta implementar um addOnScrollListener () em sua reciclagem. Em seguida, dentro do ouvinte de rolagem, implemente o código abaixo.

RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            if (mIsLoading)
                return;
            int visibleItemCount = mLayoutManager.getChildCount();
            int totalItemCount = mLayoutManager.getItemCount();
            int pastVisibleItems = mLayoutManager.findFirstVisibleItemPosition();
            if (pastVisibleItems + visibleItemCount >= totalItemCount) {
                //End of list
            }
        }
    };
Febi M Felix
fonte
1
Você pode explicar um pouco sobre mIsLoading, por favor? onde você está configurando ou alterando o valor.
MiguelHincapieC
mLoading é apenas uma variável booleana que indica se a visualização está ativada ou não. Por exemplo; se o aplicativo estiver preenchendo a visualização de reciclagem, mLoading será verdadeiro e se tornará falso quando a lista for preenchida.
Febi M Felix
1
esta solução não funciona corretamente. dê uma olhada em stackoverflow.com/a/48514857/6674369
Andriy Antonov
3
Não é possível resolver o método 'findFirstVisibleItemPosition'emandroid.support.v7.widget.RecyclerView
Iman Marashi
2
@Iman Marashi, você precisa elenco: (recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition(). Este trabalho.
bitvale
17

A resposta está em Kotlin, funcionará em Java. O IntelliJ deve convertê-lo para você, se você copiar e colar.

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener(){

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {

        // 3 lines below are not needed.
        Log.d("TAG","Last visible item is: ${gridLayoutManager.findLastVisibleItemPosition()}")
        Log.d("TAG","Item count is: ${gridLayoutManager.itemCount}")
        Log.d("TAG","end? : ${gridLayoutManager.findLastVisibleItemPosition() == gridLayoutManager.itemCount-1}")

        if(gridLayoutManager.findLastVisibleItemPosition() == gridLayoutManager.itemCount-1){
            // We have reached the end of the recycler view.
        }

        super.onScrolled(recyclerView, dx, dy)
    }
})

Isso também funcionará LinearLayoutManagerporque tem os mesmos métodos usados ​​acima. Ou seja, findLastVisibleItemPosition()e getItemCount()( itemCountem Kotlin).

daka
fonte
7
Boa abordagem, mas um problema é que as instruções no loop if serão executadas várias vezes
Vishnu
você teve a solução para os mesmos problemas
Vishwa Pratap
1
@Vishnu Uma variável booleana simples pode consertar isso.
daka
11

Eu não estava obtendo uma solução perfeita com as respostas acima porque estava disparando duas vezes, mesmo em onScrolled

override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
           if( !recyclerView.canScrollVertically(RecyclerView.FOCUS_DOWN))
               context?.toast("Scroll end reached")
        }
Manoj Perumarath
fonte
2
Mas recyclerView.canScrollVertically(direction)direção é qualquer número inteiro + vs - então pode 4 se estiver na parte inferior ou -12 se estiver no topo.
Xenolion de
6

Tente isto

Eu usei as respostas acima que ele sempre roda quando você chega ao final da visualização do reciclador,

Se você quiser verificar apenas uma vez se está em um fundo ou não? Exemplo: - Se eu tiver a lista de 10 itens sempre que for no final ela me exibirá e novamente se eu rolar de cima para baixo ela não imprimirá novamente, e se você adicionar mais listas e for lá, ela será exibida novamente.

Nota: - Use este método ao lidar com compensação ao acertar API

  1. Crie uma classe chamada EndlessRecyclerViewScrollListener

        import android.support.v7.widget.GridLayoutManager;
        import android.support.v7.widget.LinearLayoutManager;
        import android.support.v7.widget.RecyclerView;
        import android.support.v7.widget.StaggeredGridLayoutManager;
    
        public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
            // The minimum amount of items to have below your current scroll position
            // before loading more.
            private int visibleThreshold = 5;
            // The current offset index of data you have loaded
            private int currentPage = 0;
            // The total number of items in the dataset after the last load
            private int previousTotalItemCount = 0;
            // True if we are still waiting for the last set of data to load.
            private boolean loading = true;
            // Sets the starting page index
            private int startingPageIndex = 0;
    
            RecyclerView.LayoutManager mLayoutManager;
    
            public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager) {
                this.mLayoutManager = layoutManager;
            }
    
        //    public EndlessRecyclerViewScrollListener() {
        //        this.mLayoutManager = layoutManager;
        //        visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
        //    }
    
            public EndlessRecyclerViewScrollListener(StaggeredGridLayoutManager layoutManager) {
                this.mLayoutManager = layoutManager;
                visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
            }
    
            public int getLastVisibleItem(int[] lastVisibleItemPositions) {
                int maxSize = 0;
                for (int i = 0; i < lastVisibleItemPositions.length; i++) {
                    if (i == 0) {
                        maxSize = lastVisibleItemPositions[i];
                    }
                    else if (lastVisibleItemPositions[i] > maxSize) {
                        maxSize = lastVisibleItemPositions[i];
                    }
                }
                return maxSize;
            }
    
            // This happens many times a second during a scroll, so be wary of the code you place here.
            // We are given a few useful parameters to help us work out if we need to load some more data,
            // but first we check if we are waiting for the previous load to finish.
            @Override
            public void onScrolled(RecyclerView view, int dx, int dy) {
                int lastVisibleItemPosition = 0;
                int totalItemCount = mLayoutManager.getItemCount();
    
                if (mLayoutManager instanceof StaggeredGridLayoutManager) {
                    int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null);
                    // get maximum element within the list
                    lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
                } else if (mLayoutManager instanceof GridLayoutManager) {
                    lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();
                } else if (mLayoutManager instanceof LinearLayoutManager) {
                    lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();
                }
    
                // If the total item count is zero and the previous isn't, assume the
                // list is invalidated and should be reset back to initial state
                if (totalItemCount < previousTotalItemCount) {
                    this.currentPage = this.startingPageIndex;
                    this.previousTotalItemCount = totalItemCount;
                    if (totalItemCount == 0) {
                        this.loading = true;
                    }
                }
                // If it’s still loading, we check to see if the dataset count has
                // changed, if so we conclude it has finished loading and update the current page
                // number and total item count.
                if (loading && (totalItemCount > previousTotalItemCount)) {
                    loading = false;
                    previousTotalItemCount = totalItemCount;
                }
    
                // If it isn’t currently loading, we check to see if we have breached
                // the visibleThreshold and need to reload more data.
                // If we do need to reload some more data, we execute onLoadMore to fetch the data.
                // threshold should reflect how many total columns there are too
                if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
                    currentPage++;
                    onLoadMore(currentPage, totalItemCount, view);
                    loading = true;
                }
            }
    
            // Call this method whenever performing new searches
            public void resetState() {
                this.currentPage = this.startingPageIndex;
                this.previousTotalItemCount = 0;
                this.loading = true;
            }
    
            // Defines the process for actually loading more data based on page
            public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view);
    
        }
    
  2. use esta classe assim

         LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity());
            recyclerView.setLayoutManager(linearLayoutManager);
            recyclerView.addOnScrollListener(new EndlessRecyclerViewScrollListener( linearLayoutManager) {
                @Override
                public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
                    Toast.makeText(getActivity(),"LAst",Toast.LENGTH_LONG).show();
                }
            });
    

Está funcionando perfeitamente na minha extremidade, avise-me se você estiver tendo algum problema

Sunil
fonte
Isso foi muito útil :)
Devrath
4

Aí está a minha implementação, é muito útil para StaggeredGridLayout.

Uso:

private EndlessScrollListener scrollListener =
        new EndlessScrollListener(new EndlessScrollListener.RefreshList() {
            @Override public void onRefresh(int pageNumber) {
                //end of the list
            }
        });

rvMain.addOnScrollListener(scrollListener);

Implementação do ouvinte:

class EndlessScrollListener extends RecyclerView.OnScrollListener {
private boolean isLoading;
private boolean hasMorePages;
private int pageNumber = 0;
private RefreshList refreshList;
private boolean isRefreshing;
private int pastVisibleItems;

EndlessScrollListener(RefreshList refreshList) {
    this.isLoading = false;
    this.hasMorePages = true;
    this.refreshList = refreshList;
}

@Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    super.onScrolled(recyclerView, dx, dy);
    StaggeredGridLayoutManager manager =
            (StaggeredGridLayoutManager) recyclerView.getLayoutManager();

    int visibleItemCount = manager.getChildCount();
    int totalItemCount = manager.getItemCount();
    int[] firstVisibleItems = manager.findFirstVisibleItemPositions(null);
    if (firstVisibleItems != null && firstVisibleItems.length > 0) {
        pastVisibleItems = firstVisibleItems[0];
    }

    if (visibleItemCount + pastVisibleItems >= totalItemCount && !isLoading) {
        isLoading = true;
        if (hasMorePages && !isRefreshing) {
            isRefreshing = true;
            new Handler().postDelayed(new Runnable() {
                @Override public void run() {
                    refreshList.onRefresh(pageNumber);
                }
            }, 200);
        }
    } else {
        isLoading = false;
    }
}

public void noMorePages() {
    this.hasMorePages = false;
}

void notifyMorePages() {
    isRefreshing = false;
    pageNumber = pageNumber + 1;
}

interface RefreshList {
    void onRefresh(int pageNumber);
}  }
Aleksey Timoshchenko
fonte
Por que adicionar 200 milissegundos de atraso ao retorno de chamada?
Mani
@Mani Não me lembro exatamente neste momento, mas talvez eu tenha feito isso apenas para um efeito suave ...
Aleksey Timoshchenko
4

Depois de não estar satisfeito com a maioria das outras respostas neste tópico, encontrei algo que acho melhor e não está em nenhum lugar aqui.

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        if (!recyclerView.canScrollVertically(1) && dy > 0)
        {
             //scrolled to bottom
        }else if (!recyclerView.canScrollVertically(-1) && dy < 0)
        {
            //scrolled to bottom
        }
    }
});

Isso é simples e atingirá exatamente uma vez em todas as condições quando você tiver rolado para cima ou para baixo.

noone392
fonte
3

Eu também estava procurando por essa pergunta, mas não encontrei a resposta que me satisfizesse, então criei minha própria realização do recyclerView.

outras soluções são menos precisas que a minha. por exemplo: se o último item for muito grande (muito texto), o retorno de chamada de outras soluções virá muito mais cedo, então o recyclerView realmente atingiu o fundo.

minha solução consertar esse problema.

class CustomRecyclerView: RecyclerView{

    abstract class TopAndBottomListener{
        open fun onBottomNow(onBottomNow:Boolean){}
        open fun onTopNow(onTopNow:Boolean){}
    }


    constructor(c:Context):this(c, null)
    constructor(c:Context, attr:AttributeSet?):super(c, attr, 0)
    constructor(c:Context, attr:AttributeSet?, defStyle:Int):super(c, attr, defStyle)


    private var linearLayoutManager:LinearLayoutManager? = null
    private var topAndBottomListener:TopAndBottomListener? = null
    private var onBottomNow = false
    private var onTopNow = false
    private var onBottomTopScrollListener:RecyclerView.OnScrollListener? = null


    fun setTopAndBottomListener(l:TopAndBottomListener?){
        if (l != null){
            checkLayoutManager()

            onBottomTopScrollListener = createBottomAndTopScrollListener()
            addOnScrollListener(onBottomTopScrollListener)
            topAndBottomListener = l
        } else {
            removeOnScrollListener(onBottomTopScrollListener)
            topAndBottomListener = null
        }
    }

    private fun createBottomAndTopScrollListener() = object :RecyclerView.OnScrollListener(){
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            checkOnTop()
            checkOnBottom()
        }
    }

    private fun checkOnTop(){
        val firstVisible = linearLayoutManager!!.findFirstCompletelyVisibleItemPosition()
        if(firstVisible == 0 || firstVisible == -1 && !canScrollToTop()){
            if (!onTopNow) {
                onTopNow = true
                topAndBottomListener?.onTopNow(true)
            }
        } else if (onTopNow){
            onTopNow = false
            topAndBottomListener?.onTopNow(false)
        }
    }

    private fun checkOnBottom(){
        var lastVisible = linearLayoutManager!!.findLastCompletelyVisibleItemPosition()
        val size = linearLayoutManager!!.itemCount - 1
        if(lastVisible == size || lastVisible == -1 && !canScrollToBottom()){
            if (!onBottomNow){
                onBottomNow = true
                topAndBottomListener?.onBottomNow(true)
            }
        } else if(onBottomNow){
            onBottomNow = false
            topAndBottomListener?.onBottomNow(false)
        }
    }


    private fun checkLayoutManager(){
        if (layoutManager is LinearLayoutManager)
            linearLayoutManager = layoutManager as LinearLayoutManager
        else
            throw Exception("for using this listener, please set LinearLayoutManager")
    }

    private fun canScrollToTop():Boolean = canScrollVertically(-1)
    private fun canScrollToBottom():Boolean = canScrollVertically(1)
}

então em sua atividade / fragmento:

override fun onCreate() {
    customRecyclerView.layoutManager = LinearLayoutManager(context)
}

override fun onResume() {
    super.onResume()
    customRecyclerView.setTopAndBottomListener(this)
}

override fun onStop() {
    super.onStop()
    customRecyclerView.setTopAndBottomListener(null)
}

espero que ajude alguém ;-)

Andriy Antonov
fonte
1
que parte da solução de outras pessoas não o satisfez? você deve explicar primeiro antes de sugerir sua resposta
Zam Sunk
@ZamSunk porque outras soluções são menos precisas que a minha. por exemplo, se o último item for bem pequeno (muito texto), o retorno de chamada de outras soluções virá muito mais cedo, então o recyclerView realmente alcançará o final. minha solução consertar esse problema.
Andriy Antonov
0

Esta é a minha solução:

    val onScrollListener = object : RecyclerView.OnScrollListener() {

    override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
        directionDown = dy > 0
    }

    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        if (recyclerView.canScrollVertically(1).not()
                && state != State.UPDATING
                && newState == RecyclerView.SCROLL_STATE_IDLE
                && directionDown) {
               state = State.UPDATING
            // TODO do what you want  when you reach bottom, direction
            // is down and flag for non-duplicate execution
        }
    }
}
Alex Zezekalo
fonte
onde você conseguiu state esse enum?
raditya gumay
State é um enum meu, este é um sinalizador para verificar os estados dessas telas (usei MVVM com ligação de dados em meu aplicativo)
Alex Zezekalo
como você implementaria o retorno de chamada se não houver conectividade com a Internet?
Aks4125
0

Podemos usar a interface para obter a posição

Interface: crie uma interface para o ouvinte

public interface OnTopReachListener { void onTopReached(int position);}

Atividade:

mediaRecycleAdapter = new MediaRecycleAdapter(Class.this, taskList); recycle.setAdapter(mediaRecycleAdapter); mediaRecycleAdapter.setOnSchrollPostionListener(new OnTopReachListener() {
@Override
public void onTopReached(int position) {
    Log.i("Position","onTopReached "+position);  
}
});

Adaptador:

public void setOnSchrollPostionListener(OnTopReachListener topReachListener) {
    this.topReachListener = topReachListener;}@Override public void onBindViewHolder(MyViewHolder holder, int position) {if(position == 0) {
  topReachListener.onTopReached(position);}}
Amuthan S
fonte