Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

findLastVisibleItemPosition is returning wrong value if RecyclerView inside NestedScrollView

I have RecyclerView inside NestedScrollView

<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</android.support.v4.widget.NestedScrollView>

I have this problem in big project and in order to find solution for this problem I have created new project without other views.

This is full code of MainActivity

class MainActivity : AppCompatActivity() {

    var mItems = mutableListOf<String>()
    var mAdapter = MyAdapter(mItems)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        recycler.layoutManager = LinearLayoutManager(this)
        recycler.adapter = mAdapter

        delayedLoadDataIfPossible(100)

        recycler.viewTreeObserver.addOnScrollChangedListener {
            delayedLoadDataIfPossible(100)
        }

    }

    private fun delayedLoadDataIfPossible(delay: Long) {
        Observable.timer(delay, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe {
                    var scrollingReachedEnd = isScrollingReachedEnd()
                    if (scrollingReachedEnd) {
                        loadData()
                    }
                }
    }

    private fun isScrollingReachedEnd(): Boolean {
        val layoutManager = LinearLayoutManager::class.java.cast(recycler.layoutManager)
        val totalItemCount = layoutManager.itemCount
        val lastVisible = layoutManager.findLastVisibleItemPosition()
        return lastVisible + 5 >= totalItemCount
    }

    private fun loadData() {
        Observable.timer(5, TimeUnit.SECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { progress.visibility = View.VISIBLE }
                .doFinally { progress.visibility = View.GONE }
                .subscribe {
                    for (i in 1..10) {
                        mItems.add(i.toString())
                    }
                    mAdapter.notifyDataSetChanged()
                    delayedLoadDataIfPossible(100)
                }

    }

}

I am using isScrollingReachedEnd method to identify is scrolling reaching end of list. If there are less than 5 visible items in the end, I am trying to load new data.

loadData simulates loading data. It adds 10 items to list and notifies adapter about change.

delayedLoadDataIfPossible method should work after some delay because findLastVisibleItemPosition is returning value before items are added to list. In result it is returning wrong value. For example -1 after adding first 10 items.

My problem: when RecyclerView inside NestedScrollView findLastVisibleItemPosition returning wrong value and data loading can not be stopped even there are enough items. There is no such problem when RecyclerView not inside NestedScrollView.

My question: how to get last visible item position from RecyclerView when it is inside NestedScrollView?

like image 864
Joe Rakhimov Avatar asked Sep 07 '18 08:09

Joe Rakhimov


2 Answers

the problem is when recyclerView parent is a scrollable ViewGroup like nestedScroll , all items in recyclerView is laid out even its not shown , so the findLastVisibleItemPosition() will always return last item in array even if it not visible , so you have to use scroll listener of parent scrollView

package com.example.myApp;

import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.widget.NestedScrollView;
import androidx.recyclerview.widget.LinearLayoutManager;

public class MyNestedScroll extends NestedScrollView {
int SCREEN_HEIGHT ; 
private IsBottomOfList isBottomOfList ;
private LinearLayoutManager linearLayoutManager ;
private String TAG = "MyNestedScroll";

public MyNestedScroll(@NonNull Context context) {
    super(context);
init();
}

public MyNestedScroll(@NonNull Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
 init();
}

public MyNestedScroll(@NonNull Context context, @Nullable AttributeSet attrs, int 
defStyleAttr) {
    super(context, attrs, defStyleAttr);
init();
}
private void init(){
    SCREEN_HEIGHT = getContext().getResources().getDisplayMetrics().heightPixels;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    super.onScrollChanged(l, t, oldl, oldt);
    if (isBottomOfList != null){
        isBottomOfList.isBottomOfList(isVisible());
    }
}

public  boolean isVisible() {
    View view = null;
    int childCount ;
    if (linearLayoutManager != null) {
        childCount = linearLayoutManager.getChildCount();
        view = linearLayoutManager.getChildAt(childCount-1);
    }else {
        Log.v(TAG , "linearLayoutManager == null");

    }
    if (view == null) {
        Log.v(TAG , "view == null");
        return false;
    }
    if (!view.isShown()) {
        Log.v(TAG , "!view.isShown()");
        return false;
    }
    Rect actualPosition = new Rect();
    view.getGlobalVisibleRect(actualPosition);
    int height1 = view.getHeight();
    int height2 = actualPosition.bottom- actualPosition.top;
    Log.v(TAG , "actualPosition.bottom = "+actualPosition.bottom+"/ HomePage.SCREEN_HEIGHT ="+
            HomePage.SCREEN_HEIGHT+" / height1 = "+height1+"/ height2 = "+height2);
    return actualPosition.bottom<SCREEN_HEIGHT&&height1==height2;
}

public void setIsBottomOfList(IsBottomOfList isBottomOfList) {
    this.isBottomOfList = isBottomOfList;
}

public void setLinearLayoutManager(LinearLayoutManager linearLayoutManager) {
    this.linearLayoutManager = linearLayoutManager;
    Log.v(TAG , linearLayoutManager == null?"LM == NULL":"LM != NULL");
}

public interface IsBottomOfList {
    void isBottomOfList(boolean isBottom);
}
}

and in your activity use only this lines

// call this function after set layoutmanager to your recyclerView    
private void initScrollListener() {
nestedScroll.setLinearLayoutManager((LinearLayoutManager)bookingRecyclerView.getLayoutManager());

    nestedScroll.setIsBottomOfList(isBottom -> {
        if (isBottom){
           // here the last item of recyclerView is reached 
           // do some stuff to load more data
        }
    });
}

It works fine for me , if not please let me know

like image 177
Sherif farid Avatar answered Nov 08 '22 08:11

Sherif farid


You could try this approach:

  1. Find RecyclerView inside NestedScrollingView with getChildAt() method.
  2. Get LayoutManager from RecyclerView.
  3. Find lastVisiblePosition().

This is my code for a ScrollingListener for a NestedScrollingView:

@Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {

    int lastVisibleItemPosition = 0;
    int totalItemCount = layoutManager.getItemCount();

    if(v.getChildAt(v.getChildCount() - 1) != null) {
        if (scrollY >= (v.getChildAt(v.getChildCount()-1).getMeasuredHeight() - v.getMeasuredHeight())
                && scrollY > oldScrollY) {
            if (layoutManager instanceof LinearLayoutManager) {
                lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
            }

            if (totalItemCount < previousTotalItemCount) {
                this.currentPage = this.startingPageIndex;
                this.previousTotalItemCount = totalItemCount;
                if (totalItemCount == 0) {
                    this.loading = true;
                }
            }

            if (loading && (totalItemCount > previousTotalItemCount)) {
                loading = false;
                previousTotalItemCount = totalItemCount;
            }

            if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
                currentPage++;
                onLoadMore();
                loading = true;
            }
        }
    }
}

Good luck!

like image 2
Juanje Avatar answered Nov 08 '22 09:11

Juanje