I have a problem regarding RecyclerView duplicating its items on scroll to top.
My RecyclerView populates its View by taking values from an online database on scroll down to a certain threshold. I am well aware of RecyclerView's behavior on reusing the Views, but I don't want that to happen as it'll get confusing due to having Views with different items inside.
I've searched around SO for the solution. Some said that I have to override getItemId like :
#Override
public long getItemId(int id) {
return id;
}
But they don't elaborate more on that.
Tried using setHasStableIds(true); but it's not working. When I scroll down to populate the RecyclerView, then scroll quickly back up, the first item still shows the last item I scrolled to, or any other random item.
I have this in onBindViewHolder :
if(loading)
{
// Do nothing
}
else {
((ObjectViewHolder) holder).progressBar.setVisibility(View.GONE);
((ObjectViewHolder) holder).postListWrapper.setVisibility(View.VISIBLE);
Uri userImageUri = Uri.parse(mDataset.get(position).author_avatar);
...
// The rest of the code
}
Does it have to do with the error I'm getting? The loading is changed to false when the Fragment containing RecyclerView finished getting value from the database.
Here's the RecyclerView onScrollListener :
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstVisibleItem = manager.findFirstCompletelyVisibleItemPosition();
if(firstVisibleItem < 1 )
{
swipeRefreshLayout.setEnabled(true);
}else
{
swipeRefreshLayout.setEnabled(false);
}
totalItemCount = manager.getItemCount();
lastVisibleItem = manager.findLastVisibleItemPosition();
int visibleThreshold = 2;
if(isLoading == false)
{
if (totalItemCount <= lastVisibleItem + visibleThreshold) {
if(lobiAdapter.getItemCount() > 0)
{
if (lobiAdapter.getItemCount() < 5)
{
setIsLoaded();
}else{
// End has been reached
// Do something
PostListAPI postListAPI = new PostListAPI();
postListAPI.query.user_id = userId;
postListAPI.query.post_count = String.valueOf(counter);
postListAPI.query.flag = "load";
NewsFeedsAPIFunc newsFeedsAPIFunc = new NewsFeedsAPIFunc(BottomLobiFragment.this.getActivity());
newsFeedsAPIFunc.delegate = BottomLobiFragment.this;
setIsLoading();
newsFeedsAPIFunc.execute(postListAPI);
}
}
else {
setIsLoaded();
}
}
}
}
});
Related
The problem I am having is that the recycler view is not displaying all item list. In my particular case I have 38 items and it is missing some of them. I had 34 and it was working just fine.
I have a search bar and when I type in the ids of the missing items, it shows them. I can't find where the problem could be. Any suggestions on what to look for or changes I could make, are very welcome. Feel free to ask for any code you think is needed.
This is how I call the adapter
public void buildCollectionSalesAdapter(){
final String filter = edtFindSale.getText().toString().trim();
final SaleHeader saleHeaderModel = new SaleHeader();
List<SaleHeader> saleHeaderList1 = new SaleHeader().getSaleHeadersAll();
int limit = saleHeaderList1.size();
saleHeaderList = saleHeaderModel.getSaleHeaderCollectPagination(limit, 0, filter,
config.showVerified, config.allCustomers);
final SalesAdapter salesAdapter = new SalesAdapter(saleHeaderList, null, context,
CollectionFragment.this, null, getActivity());
rcvCollection.setAdapter(salesAdapter);
registerForContextMenu(rcvCollection);
rcvCollection.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy)
{
if(dy > 0) //check for scroll down
{
totalItemCount = linearLayoutManager.getItemCount();
visibleItemCount = linearLayoutManager.getChildCount();
firstVisibleItem = linearLayoutManager.findFirstVisibleItemPosition();
if (loading) {
Log.i(TAG, "scrolled loadin "+ loading+" "+saleHeaderList.size());
if (totalItemCount > previousTotal) {
loading = false;
previousTotal = totalItemCount;
}
}
if (!loading && (totalItemCount - visibleItemCount) <= (firstVisibleItem + VISIBLE_THRESHOLD)) {
Log.i(TAG, "scrolled !loadin "+ loading+" "+saleHeaderList.size());
// Hemos llegado al final
if(salesAdapter.getItemCount()>1) {
offset = offset + limit;
saleHeaderList.addAll(saleHeaderModel.getSaleHeaderCollectPagination(limit,
offset, filter, config.showVerified, config.allCustomers));
recyclerView.post(new Runnable() {
public void run() {
// There is no need to use notifyDataSetChanged()
salesAdapter.notifyItemRangeInserted(offset, saleHeaderList.size());
}
});
loading = true;
}
}
}
}
});
}
I found out that when I change the limit of the pagination from 10 to the list size it shows all the data.
I made it work by changing the pagination form 10 to 15, but still do not know waht is causng the error
I made it work by changing the pagination form 10 to 15, but still do not know what was causing the error.
I using GridLayoutManager with 3 span count in recyclerView on my app, the problem is when I run the app first time it's showing only first 10 items due to the the restriction / pagiation of the items from server, when user refreshing with SwipeRefreshLayout or scrolling it's continue loading other items, but it's look ugly on first load of items so I want fill these gaps without necessary for refreshing to load other items
here's the gridlayout with scroll listener
gridLayoutManager = new GridLayoutManager(MainActivity.this, 3);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
isScrolling = true;
progressBar.setVisibility(View.GONE);
}
}
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
currentItems = gridLayoutManager.getChildCount();
totalItems = gridLayoutManager.getItemCount();
scrollOutItems = gridLayoutManager.findFirstVisibleItemPosition();
if (isScrolling && (currentItems + scrollOutItems == totalItems)) {
isScrolling = false;
if (getItemsByLabelCalled) {
for (int i = 1; i < 7; i++) {
if (navigationView.getMenu().getItem(i).isChecked()) {
getItemsByLabel(navigationView.getMenu().getItem(i).getTitle().toString());
}
}
} else {
getData();
}
}
}
}
});
SwipeRefreshLayout
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
if (navigationView.getMenu().getItem(0).isChecked()) {
if (Utils.hasNetworkAccess(MainActivity.this)) {
getData();
} else {
Toast.makeText
(MainActivity.this, R.string.SwipeRefreshLayout_connect_to_update
, Toast.LENGTH_LONG).show();
}
} else {
for (int i = 1; i < 7; i++) {
if (navigationView.getMenu().getItem(i).isChecked()) {
getItemsByLabel(navigationView.getMenu().getItem(i).getTitle().toString());
}
}
}
new Handler().postDelayed(() -> swipeRefreshLayout.setRefreshing(false), 2000);
}
});
The problem described with images
The problem is that your items count is not enough to fill the entire page. There are two ways to overcome this issue.
You can call your getData method one more time after loading the first batch to get more data which is not a clean solution since you might need more items on larger screens like tablets.
Put your load more procedure in onBindViewHolder method of your recycler view adapter instead of its on scroll listener. You can do the load more if position == getItemCount()-1 for instance which requests server for more items if your last item gets in view (or needs binding). Remember that you have to hold a boolean in order to prevent repetitive calls on your getData.
I have a recycleView that I need to observe when the last item is reached but I have notice the it always indicate that I reached the last item even if I haven't scrolled yet.
My code for setting up the recycler:
newsRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
newsRecyclerView.setFocusable(false);
newsRecyclerView.setNestedScrollingEnabled(false);
newsAdapter = new NewsAdapter(getContext(), newsDetails, categoryNumber);
newsRecyclerView.setAdapter(newsAdapter);
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
My xml code is:
<android.support.v7.widget.RecyclerView
android:id="#+id/news_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/news_top_stories_title_text_view" />
This is my code that I put in my Util, and use anywhere.
Util.setRecyclerViewLastPositionListner(rv, linearLayoutManager , new UtilitiesV2.OnLastPositionReached() {
#Override
public void onReached() {
// last position reached
}
});
Put this in Util.
private boolean userScrolled = true;
int pastVisiblesItems, visibleItemCount, totalItemCount;
public interface OnLastPositionReached {
void onReached();
}
public void setRecyclerViewLastPositionListner(RecyclerView rvBooksMockTest, final LinearLayoutManager mLayoutManager, final OnLastPositionReached onLastPositionReached) {
rvBooksMockTest.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
userScrolled = true;
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// Here get the child count, item count and visibleitems
// from layout manager
visibleItemCount = mLayoutManager.getChildCount();
totalItemCount = mLayoutManager.getItemCount();
pastVisiblesItems = mLayoutManager.findFirstVisibleItemPosition();
// Now check if userScrolled is true and also check if
// the item is end then update recycler view and set
// userScrolled to false
if (userScrolled && (visibleItemCount + pastVisiblesItems) == totalItemCount) {
userScrolled = false;
if (onLastPositionReached != null) onLastPositionReached.onReached();
}
}
});
}
Update
According to your requirement, here is NestedScrollView bottom reach listener.
nestedScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
if (nestedScrollView != null) {
if (nestedScrollView.getChildAt(0).getBottom() <= (nestedScrollView.getHeight() + nestedScrollView.getScrollY())) {
//scroll view is at bottom
} else {
//scroll view is not at bottom
}
}
}
});
Thanks to Khemraj who give the tip for solution
because I have a recyclerview inside NestedScrollView and coordinator layout as parent for them
I have solved my problem like this:
public Disposable observeNestedScroll(LoadMoreListener listener) {
return RxNestedScrollView.scrollChangeEvents(nestedScrollView)
.subscribe(
viewScrollChangeEvent -> {
NestedScrollView nestedScrollView =(NestedScrollView) viewScrollChangeEvent.view();
if(nestedScrollView.getChildAt(nestedScrollView.getChildCount() - 1) != null) {
if ((viewScrollChangeEvent.scrollY() >= (nestedScrollView.getChildAt(nestedScrollView.getChildCount() - 1).getMeasuredHeight() - nestedScrollView.getMeasuredHeight())) &&
viewScrollChangeEvent.scrollY() > viewScrollChangeEvent.oldScrollY()) {
listener.onLoadMore();
}
}
});
}
I've seen to many responses for this question and I stand that all of them don't give accurate behavior as an outcome. However if you follow this approach I'm positive you'll get the best behavior.
rvCategories is your RecyclerView
categoriesList is the list passed to your adapter
binding.rvCategories.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val position = (recyclerView.layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition()
if (position + 1 == categoriesList.size) {
// END OF RECYCLERVIEW IS REACHED
} else {
// END OF RECYCLERVIEW IS NOT REACHED
}
}
})
I'm trying to implement endless scrolling with recyclerview and it works fine. However, I noticed when the content size is smaller than the screen height, onScrolled is never called. I want to load more when scrolling down but I can't detect a scroll down gesture as onScrolled is never called and thus I can't get the dy value. I was wondering:
1) How can I get the scroll direction in this case
2) What's the best practice for situations like this? I am getting a set number of items per service call. What happens if the number of items returned does not fill the screen?
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener()
{
int lastVisibleItemPosition, visibleItemCount, totalItemCount;
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy)
{
super.onScrolled(recyclerView, dx, dy);
if(dy>0)
{
visibleItemCount = recyclerLayoutManager.getChildCount();
totalItemCount = recyclerLayoutManager.getItemCount();
lastVisibleItemPosition = recyclerLayoutManager.findLastVisibleItemPosition();
if (lastVisibleItemPosition >= totalItemCount)
{
if (!loading && dy>0 && moreToload)
{
loadMore();
}
}
}
}
});
Thanks!
onScrolled callback will also be called if visible item range changes after a layout calculation. please check link here.
This mean its always called once after finished load the items in to the recycler view
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
visibleItemCount = recyclerLayoutManager.getChildCount();
totalItemCount = recyclerLayoutManager.getItemCount();
lastVisibleItemPosition = recyclerLayoutManager.findLastVisibleItemPosition();
if ((totalItemCount == visibleItemCount) || //This mean you still have space in your screen
(dy > 0 && !loading && lastVisibleItemPosition >= totalItemCount)) {
// Call load more here
}
}
If server does not return any items, the recyclerView will not update and by default onScroll method will not called again until you reach the end of the recyclerView.
You can try the same but with RecyclerView.LayoutManager customization and handling overscroll
#Override
public int scrollVerticallyBy ( int dx, RecyclerView.Recycler recycler,
RecyclerView.State state ) {
int scrollRange = super.scrollVerticallyBy(dx, recycler, state);
int overscroll = dx - scrollRange;
if (overscroll > 0) {
// bottom overscroll
} else if (overscroll < 0) {
// top overscroll
}
return scrollRange;
}
Create a subclass of LinearLayoutManager which lets you listen to the onLayoutCompleted() event:
/**
* This class calls [mCallback] (instance of [OnLayoutCompleteCallback]) when all layout
* calculations are complete, e.g. following a call to
* [RecyclerView.Adapter.notifyDataSetChanged()] (or related methods).
*
* In a paginated listing, we will decide if load more needs to be called in the said callback.
*/
class NotifyingLinearLayoutManager(context: Context) : LinearLayoutManager(context, VERTICAL, false) {
var mCallback: OnLayoutCompleteCallback? = null
override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
mCallback?.onLayoutComplete()
}
fun isLastItemCompletelyVisible() = findLastCompletelyVisibleItemPosition() == itemCount - 1
interface OnLayoutCompleteCallback {
fun onLayoutComplete()
}
}
In you listener, you can decide if a call to load more is needed or not:
mLayoutManager.mCallback = object : NotifyingLinearLayoutManager.OnLayoutCompleteCallback {
override fun onLayoutComplete() {
// here we know that the view has been updated.
loadMoreIfNeeded()
}
}
My loadMoreIfNeeded() looks like following:
private fun loadMoreIfNeeded() {
if (isAppropriateToLoadMoreItems() && !mLoadMoreRequestFired.getAndSet(true)) {
mListener.onLoadMore()
}
}
private fun isAppropriateToLoadMoreItems() =
mLoadMoreEnabled && itemCount > 1 && mLayoutManager.isLastItemCompletelyVisible()
These are following fields used in the previous step:
private var mLoadMoreRequestFired = AtomicBoolean(false)
// makes sure that we fire a single load more call at a time
private var mLoadMoreEnabled = false
// this flag is used by the footer (containing the ProgressBar) to show or hide it.
Just remove the if(dy > 0) on your code.
Final snippet
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener()
{
int lastVisibleItemPosition, visibleItemCount, totalItemCount;
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy)
{
super.onScrolled(recyclerView, dx, dy);
visibleItemCount = recyclerLayoutManager.getChildCount();
totalItemCount = recyclerLayoutManager.getItemCount();
lastVisibleItemPosition = recyclerLayoutManager.findLastVisibleItemPosition();
if (lastVisibleItemPosition >= totalItemCount)
{
if (!loading && dy>0 && moreToload)
{
loadMore();
}
}
}
});
I have included RecyclerView inside NestedScrollView and set
mRecyclerView.setNestedScrollingEnabled(false);
Since the scrolling of RecyclerView is set to false, I will not get the scroll position of RecyclerView due to this I will not be able to put pagination in RecyclerView.
I have also tried this
mRecylerview.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if(dy > 0) {
totalItemCount = linearLayoutManager.getItemCount();
lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition();
if (!isLoading && totalItemCount <= (lastVisibleItem + visibleThreshold)) {
// End has been reached
// Do something
if (mOnLoadMoreListener != null) {
mOnLoadMoreListener.onLoadMore();
isLoading = true;
}
}
}
}
});
But here lastVisibleItem always gives the length of the list to be displayed.
After few digging, I found that when we use RecylerView inside NestedScrollView all the view gets created at the very beginning so that's the reason why lastVisibleItem always gives the size of the list.
Or if I am wrong please explain, me how RecyclerView inside NestedScrollView works?.
Is there any workaround so that I can get RecyclerView scroll position so that I can make my pagination work?.
It will be a great help. Thanks in advance
Here scroll is NestedScrollView
scroll.getViewTreeObserver().addOnScrollChangedListener(() -> {
View view = (View) scroll.getChildAt(scroll.getChildCount() - 1);
Log.d("CategoryNeswList", scroll.getChildCount() + " child");
int diff = (view.getBottom() - (scroll.getHeight() + scroll
.getScrollY()));
if (diff == 0) {
// getPlaylistFromServer("more");
Toast.makeText(mContext, "Load More", Toast.LENGTH_SHORT).show();
}
});