Scrolling SwipeRefreshLayout with RecyclerView Refresh anywhere in android 2.2 - android

I have problem with my layout, I created SwipeRefreshLayout with RecyclerView inside.
in android 4.2.2+ all is working good, but in andorid 2.3.4 I cant to scroll up because in any place in the RecyclerView it will refresh, and I must to scroll down and then scroll up.
This is my code:
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/forum_swipe_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/LVP"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center" />
</android.support.v4.widget.SwipeRefreshLayout>
I found this issue:https://code.google.com/p/android/issues/detail?id=78191 but no a solution.
Any idea how to fix it?

override RecyclerView's method OnScrollStateChanged
mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
// TODO Auto-generated method stub
//super.onScrollStateChanged(recyclerView, newState);
try {
int firstPos = mLayoutManager.findFirstCompletelyVisibleItemPosition();
if (firstPos > 0) {
mSwipeRefreshLayout.setEnabled(false);
} else {
mSwipeRefreshLayout.setEnabled(true);
if(mRecyclerView.getScrollState() == 1)
if(mSwipeRefreshLayout.isRefreshing())
mRecyclerView.stopScroll();
}
}catch(Exception e) {
Log.e(TAG, "Scroll Error : "+e.getLocalizedMessage());
}
}
Check if Swipe Refresh is Refreshing and try to Scroll up then you got error, so when swipe refresh is going on and i try do this mRecyclerView.stopScroll();

I fixed the scroll up issue using the following code :
private RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
LinearLayoutManager manager = ((LinearLayoutManager)recyclerView.getLayoutManager());
boolean enabled =manager.findFirstCompletelyVisibleItemPosition() == 0;
pullToRefreshLayout.setEnabled(enabled);
}
};
Then you need to use setOnScrollListener or addOnScrollListener depending if you have one or more listeners.

Unfortunately, this is a known issue and will be fixed in a future release.
https://code.google.com/p/android/issues/detail?id=78191
Meanwhile, if you need urgent fix, override canChildScrollUp in SwipeRefreshLayout.java and call recyclerView.canScrollVertically(mTarget, -1). Because canScrollVertically was added after gingerbread, you'll also need to copy that method and implement in recyclerview.
Alternatively, if you are using LinearLayoutManager, you can call findFirstCompletelyVisibleItemPosition.
Sorry for the inconvenience.

You can disable/enable the refresh layout based on recyclerview's scroll ability
public class RecyclerSwipeRefreshHelper extends RecyclerView.OnScrollListener{
private static final int DIRECTION_UP = -1;
private final SwipeRefreshLayout refreshLayout;
public RecyclerSwipeRefreshHelper(
SwipeRefreshLayout refreshLayout) {
this.refreshLayout = refreshLayout;
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
refreshLayout.setEnabled((recyclerView.canScrollVertically(DIRECTION_UP)));
}
}

You can override the method canChildScrollUp() in SwipeRefreshLayout like this:
public boolean canChildScrollUp() {
if (mTarget instanceof RecyclerView) {
final RecyclerView recyclerView = (RecyclerView) mTarget;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
int position = ((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition();
return position != 0;
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstCompletelyVisibleItemPositions(null);
for (int i = 0; i < positions.length; i++) {
if (positions[i] == 0) {
return false;
}
}
}
return true;
} else if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}

Based on #wrecker answer (https://stackoverflow.com/a/32318447/7508302).
In Kotlin we can use extension method. So:
class RecyclerViewSwipeToRefresh(private val refreshLayout: SwipeToRefreshLayout) : RecyclerView.OnScrollListener() {
companion object {
private const val DIRECTION_UP = -1
}
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
refreshLayout.isEnabled = !(recyclerView?.canScrollVertically(DIRECTION_UP) ?: return)
}
}
And let's add extension method to RecyclerView to easly apply this fix to RV.
fun RecyclerView.fixSwipeToRefresh(refreshLayout: SwipeRefreshLayout): RecyclerViewSwipeToRefresh {
return RecyclerViewSwipeToRefresh(refreshLayout).also {
this.addOnScrollListener(it)
}
}
Now, we can fix recyclerView using:
recycler_view.apply {
...
fixSwipeToRefresh(swipe_container)
...
}

Following code is working for me, please ensure that it is placed below the binding.refreshDiscoverList.setOnRefreshListener{} method.
binding.swipeToRefreshLayout.setOnChildScrollUpCallback(object : SwipeRefreshLayout.OnChildScrollUpCallback {
override fun canChildScrollUp(parent: SwipeRefreshLayout, child: View?): Boolean {
if (binding.rvDiscover != null) {
return binding.recyclerView.canScrollVertically(-1)
}
return false
}
})

Related

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?
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
You could try this approach:
Find RecyclerView inside NestedScrollingView with getChildAt() method.
Get LayoutManager from RecyclerView.
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!
Simple solution : Put the below scrollChangeListener on NestedScrollView and remove scrollListener from Recyclerview.
And put the Load more Pagination functionlity on onScrollChange under if condition mention below. That's it.
private void handleNestedScrollListener() {
mNestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
#Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if ((scrollY >= (v.getChildAt(v.getChildCount() - 1).getMeasuredHeight() - v.getMeasuredHeight())) &&
scrollY > oldScrollY) {
// Do Load more feature here
}
}
});
}
..Worked for me..
If your RecyclerView is inside a NestedScrollView, you need to add recyclerView.setNestedScrollingEnabled(false) to it.
Also another note (which is unrelated to your question is that you should definitely keep a reference to those RxJava subscriptions and dispose them on Fragment/Adtivity's onStop, since they can cause memory leak issues.

Recyclerview indicates that reaches the last item

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

How to sync scrolling first-positions of 2 RecyclerViews?

Background
I have 2 RecyclerView instances. One is horizontal, and the second is vertical.
They both show the same data and have the same amount of items, but in different ways, and the cells are not necessary equal in size through each of them .
I wish that scrolling in one will sync with the other, so that the first item that's shown on one, will always be shown on the other (as the first).
The problem
Even though I've succeeded making them sync (I just choose which one is the "master", to control the scrolling of the other), the direction of the scrolling seems to affect the way it works.
Suppose for a moment the cells have equal heeight:
If I scroll up/left, it works as I intended, more or less:
However, if I scroll down/right, it does let the other RecyclerView show the first item of the other, but usually not as the first item:
Note: on the above screenshots, I've scrolled in the bottom RecyclerView, but a similar result will be with the top one.
The situation gets much worse if, as I wrote, the cells have different sizes:
What I've tried
I tried to use other ways of scrolling and going to other positions, but all attempts fail.
Using smoothScrollToPosition made things even worse (though it does seem nicer), because if I fling, at some point the other RecyclerView takes control of the one I've interacted with.
I think I should use the direction of the scrolling, together with the currently shown items on the other RecyclerView.
Here's the current (sample) code. Note that in the real code, the cells might not have equal sizes (some are tall, some are short, etc...). One of the lines in the code makes the cells have different height.
activity_main.xml
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="#+id/topReccyclerView" android:layout_width="0dp" android:layout_height="100dp"
android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp"
android:orientation="horizontal" app:layoutManager="android.support.v7.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" tools:listitem="#layout/horizontal_cell"/>
<android.support.v7.widget.RecyclerView
android:id="#+id/bottomRecyclerView" android:layout_width="0dp" android:layout_height="0dp"
android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="8dp" app:layoutManager="android.support.v7.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="#+id/topReccyclerView"
tools:listitem="#layout/horizontal_cell"/>
</android.support.constraint.ConstraintLayout>
horizontal_cell.xml
<TextView
android:id="#+id/textView" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="100dp" android:layout_height="100dp"
android:gravity="center" tools:text="#tools:sample/lorem"/>
vertical_cell.xml
<TextView
android:id="#+id/textView" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="50dp"
android:gravity="center" tools:text="#tools:sample/lorem"/>
MainActivity
class MainActivity : AppCompatActivity() {
var masterView: View? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inflater = LayoutInflater.from(this)
topReccyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder.itemView as TextView).text = position.toString()
holder.itemView.setBackgroundColor(if(position%2==0) 0xffff0000.toInt() else 0xff00ff00.toInt())
}
override fun getItemCount(): Int {
return 100
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.horizontal_cell, parent, false)) {}
}
}
bottomRecyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val baseHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50f, resources.displayMetrics).toInt()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder.itemView as TextView).text = position.toString()
holder.itemView.setBackgroundColor(if(position%2==0) 0xffff0000.toInt() else 0xff00ff00.toInt())
// this makes the heights of the cells different from one another:
holder.itemView.layoutParams.height = baseHeight + (if (position % 3 == 0) 0 else baseHeight / (position % 3))
}
override fun getItemCount(): Int {
return 100
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.vertical_cell, parent, false)) {}
}
}
LinearSnapHelper().attachToRecyclerView(topReccyclerView)
LinearSnapHelper().attachToRecyclerView(bottomRecyclerView)
topReccyclerView.addOnScrollListener(OnScrollListener(topReccyclerView, bottomRecyclerView))
bottomRecyclerView.addOnScrollListener(OnScrollListener(bottomRecyclerView, topReccyclerView))
}
inner class OnScrollListener(private val thisRecyclerView: RecyclerView, private val otherRecyclerView: RecyclerView) : RecyclerView.OnScrollListener() {
var lastItemPos: Int = Int.MIN_VALUE
val thisRecyclerViewId = resources.getResourceEntryName(thisRecyclerView.id)
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
Log.d("AppLog", "onScrollStateChanged:$thisRecyclerViewId $newState")
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> if (masterView == null) {
Log.d("AppLog", "setting $thisRecyclerViewId to be master")
masterView = thisRecyclerView
}
RecyclerView.SCROLL_STATE_IDLE -> if (masterView == thisRecyclerView) {
Log.d("AppLog", "resetting $thisRecyclerViewId from being master")
masterView = null
lastItemPos = Int.MIN_VALUE
}
}
}
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if ((dx == 0 && dy == 0) || (masterView != null && masterView != thisRecyclerView))
return
// Log.d("AppLog", "onScrolled:$thisRecyclerView $dx-$dy")
val currentItem = (thisRecyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
if (lastItemPos == currentItem)
return
lastItemPos = currentItem
otherRecyclerView.scrollToPosition(currentItem)
// otherRecyclerView.smoothScrollToPosition(currentItem)
Log.d("AppLog", "currentItem:" + currentItem)
}
}
}
The questions
How do I let the other RecycerView to always have the first item the same as the currently controlled one?
How to modify the code to support smooth scrolling, without causing the issue of suddenly having the other RecyclerView being the one that controls ?
EDIT: after updating the sample code here with having different sizes of cells (because originally that's closer to the issue I have, as I described before), I noticed that the snapping doesn't work well.
That's why I chose to use this library to snap it correctly:
https://github.com/DevExchanges/SnappingRecyclerview
So instead of LinearSnapHelper, I use 'GravitySnapHelper'. Seems to work better, but still have the syncing issues, and touching while it scrolls.
EDIT:
I've finally fixed all syncing issues, and it works fine even if the cells have different sizes.
Still has some issues:
If you fling on one RecyclerView, and then touch the other one, it has very weird behavior of scrolling. Might scroll way more than it should.
The scrolling isn't smooth (when syncing and when flinging), so it doesn't look well.
Sadly, because of the snapping (which I actually might need only for the top RecyclerView), it causes another issue: the bottom RecyclerView might show the last item partially (screenshot with 100 items), and I can't scroll more to show it fully :
I don't even think that the bottom RecyclerView should be snapping, unless the top one was touched. Sadly this is all I got so far, that has no syncing issues.
Here's the new code, after all the fixes I've found:
class MainActivity : AppCompatActivity() {
var masterView: View? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inflater = LayoutInflater.from(this)
topReccyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder.itemView as TextView).text = position.toString()
holder.itemView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
}
override fun getItemCount(): Int = 1000
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.horizontal_cell, parent, false)) {}
}
}
bottomRecyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val baseHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50f, resources.displayMetrics).toInt()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder.itemView as TextView).text = position.toString()
holder.itemView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
holder.itemView.layoutParams.height = baseHeight + (if (position % 3 == 0) 0 else baseHeight / (position % 3))
}
override fun getItemCount(): Int = 1000
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.vertical_cell, parent, false)) {}
}
}
// GravitySnapHelper is available from : https://github.com/DevExchanges/SnappingRecyclerview
GravitySnapHelper(Gravity.START).attachToRecyclerView(topReccyclerView)
GravitySnapHelper(Gravity.TOP).attachToRecyclerView(bottomRecyclerView)
topReccyclerView.addOnScrollListener(OnScrollListener(topReccyclerView, bottomRecyclerView))
bottomRecyclerView.addOnScrollListener(OnScrollListener(bottomRecyclerView, topReccyclerView))
}
inner class OnScrollListener(private val thisRecyclerView: RecyclerView, private val otherRecyclerView: RecyclerView) : RecyclerView.OnScrollListener() {
var lastItemPos: Int = Int.MIN_VALUE
val thisRecyclerViewId = resources.getResourceEntryName(thisRecyclerView.id)
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> if (masterView == null) {
masterView = thisRecyclerView
}
RecyclerView.SCROLL_STATE_IDLE -> if (masterView == thisRecyclerView) {
masterView = null
lastItemPos = Int.MIN_VALUE
}
}
}
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dx == 0 && dy == 0 || masterView !== null && masterView !== thisRecyclerView) {
return
}
val otherLayoutManager = otherRecyclerView.layoutManager as LinearLayoutManager
val thisLayoutManager = thisRecyclerView.layoutManager as LinearLayoutManager
val currentItem = thisLayoutManager.findFirstCompletelyVisibleItemPosition()
if (lastItemPos == currentItem) {
return
}
lastItemPos = currentItem
otherLayoutManager.scrollToPositionWithOffset(currentItem, 0)
}
}
}
Combining the two RecyclerViews, there are four cases of movement:
a. Scrolling the horizontal recycler to the left
b. Scrolling it to the right
c. Scrolling the vertical recycler to the top
d. Scrolling it to the bottom
Cases a and c don't need to be taken care of since they work out of the box. For cases b and d you need to do two things:
Know which recycler you are in (vertical or horizontal) and which direction the scroll went (up or down resp. left or right) and
calculate an offset (of list items) from the number of visible items in otherRecyclerView (if the screen is bigger the offset needs to be bigger, too).
Figuring this out was a bit fiddly, but the result is pretty straight forward.
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
if (masterView == otherRecyclerView) {
thisRecyclerView.stopScroll();
otherRecyclerView.stopScroll();
syncScroll(1, 1);
}
masterView = thisRecyclerView;
} else if (newState == RecyclerView.SCROLL_STATE_IDLE && masterView == thisRecyclerView) {
masterView = null;
}
}
#Override
public void onScrolled(RecyclerView recyclerview, int dx, int dy) {
super.onScrolled(recyclerview, dx, dy);
if ((dx == 0 && dy == 0) || (masterView != null && masterView != thisRecyclerView)) {
return;
}
syncScroll(dx, dy);
}
void syncScroll(int dx, int dy) {
LinearLayoutManager otherLayoutManager = (LinearLayoutManager) otherRecyclerView.getLayoutManager();
LinearLayoutManager thisLayoutManager = (LinearLayoutManager) thisRecyclerView.getLayoutManager();
int offset = 0;
if ((thisLayoutManager.getOrientation() == HORIZONTAL && dx > 0) || (thisLayoutManager.getOrientation() == VERTICAL && dy > 0)) {
// scrolling horizontal recycler to left or vertical recycler to bottom
offset = otherLayoutManager.findLastCompletelyVisibleItemPosition() - otherLayoutManager.findFirstCompletelyVisibleItemPosition();
}
int currentItem = thisLayoutManager.findFirstCompletelyVisibleItemPosition();
otherLayoutManager.scrollToPositionWithOffset(currentItem, offset);
}
Of course you could combine the two if clauses since the bodies are the same. For the sake of readability, I thought it is good to keep them apart.
The second problem was syncing when the respective "other" recycler was touched while the "first" recycler was still scrolling. Here the following code (included above) is relevant:
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
if (masterView == otherRecyclerView) {
thisRecyclerView.stopScroll();
otherRecyclerView.stopScroll();
syncScroll(1, 1);
}
masterView = thisRecyclerView;
}
newState equals SCROLL_STATE_DRAGGING when the recycler is touched and dragged a little bit. So if this is a touch (& drag) after a touch on the respective "other" recycler, the second condition (masterView == otherRecyclerview) is true. Both recyclers are stopped then and the "other" recycler is synced with "this" one.
1-) Layout manager
The current smoothScrollToPosition does not take the element to the top. So let's write a new layout manager. And let's override this layout manager's smoothScrollToPosition.
public class TopLinearLayoutManager extends LinearLayoutManager
{
public TopLinearLayoutManager(Context context, int orientation)
{
//orientation : vertical or horizontal
super(context, orientation, false);
}
#Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position)
{
RecyclerView.SmoothScroller smoothScroller = new TopSmoothScroller(recyclerView.getContext());
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
private class TopSmoothScroller extends LinearSmoothScroller
{
TopSmoothScroller(Context context)
{
super(context);
}
#Override
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference)
{
return (boxStart - viewStart);
}
}
}
2-) Setup
//horizontal one
RecyclerView rvMario = (RecyclerView) findViewById(R.id.rvMario);
//vertical one
RecyclerView rvLuigi = (RecyclerView) findViewById(R.id.rvLuigi);
final LinearLayoutManager managerMario = new LinearLayoutManager(MainActivity.this, LinearLayoutManager.HORIZONTAL, false);
rvMario.setLayoutManager(managerMario);
ItemMarioAdapter adapterMario = new ItemMarioAdapter(itemList);
rvMario.setAdapter(adapterMario);
//Snap to start by using Ruben Sousa's RecyclerViewSnap
SnapHelper snapHelper = new GravitySnapHelper(Gravity.START);
snapHelper.attachToRecyclerView(rvMario);
final TopLinearLayoutManager managerLuigi = new TopLinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL);
rvLuigi.setLayoutManager(managerLuigi);
ItemLuigiAdapter adapterLuigi = new ItemLuigiAdapter(itemList);
rvLuigi.setAdapter(adapterLuigi);
3-) Scroll listener
rvMario.addOnScrollListener(new RecyclerView.OnScrollListener()
{
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy)
{
super.onScrolled(recyclerView, dx, dy);
//get firstCompleteleyVisibleItemPosition
int firstCompleteleyVisibleItemPosition = managerMario.findFirstCompletelyVisibleItemPosition();
if (firstCompleteleyVisibleItemPosition >= 0)
{
//vertical one, smooth scroll to position
rvLuigi.smoothScrollToPosition(firstCompleteleyVisibleItemPosition);
}
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState)
{
super.onScrollStateChanged(recyclerView, newState);
}
});
4-) Output
Building on Burak's TopLinearLayoutManager, but correcting the logic of the OnScrollListener we finally get working smoothscrolling and correct snapping (of the horizontal RecyclerView).
public class MainActivity extends AppCompatActivity {
View masterView = null;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
final LayoutInflater inflater = LayoutInflater.from(this);
final RecyclerView topRecyclerView = findViewById(R.id.topReccyclerView);
RecyclerView.Adapter adapterTop = new RecyclerView.Adapter<RecyclerView.ViewHolder>() {
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.horizontal_cell, parent, false));
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
((TextView) holder.itemView).setText(String.valueOf(position));
holder.itemView.setBackgroundColor(position % 2 == 0 ? Integer.valueOf(0xffff0000) : Integer.valueOf(0xff00ff00));
}
#Override
public int getItemCount() {
return 100;
}
class ViewHolder extends RecyclerView.ViewHolder {
final TextView textView;
ViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.textView);
}
}
};
topRecyclerView.setAdapter(adapterTop);
final RecyclerView bottomRecyclerView = findViewById(R.id.bottomRecyclerView);
RecyclerView.Adapter adapterBottom = new RecyclerView.Adapter() {
int baseHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50f, getResources().getDisplayMetrics());
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.vertical_cell, parent, false));
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
((TextView) holder.itemView).setText(String.valueOf(position));
holder.itemView.setBackgroundColor((position % 2 == 0) ? Integer.valueOf(0xffff0000) : Integer.valueOf(0xff00ff00));
holder.itemView.getLayoutParams().height = baseHeight + (position % 3 == 0 ? 0 : baseHeight / (position % 3));
}
#Override
public int getItemCount() {
return 100;
}
class ViewHolder extends RecyclerView.ViewHolder {
final TextView textView;
ViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.textView);
}
}
};
bottomRecyclerView.setAdapter(adapterBottom);
TopLinearLayoutManager topLayoutManager = new TopLinearLayoutManager(this, LinearLayoutManager.HORIZONTAL);
topRecyclerView.setLayoutManager(topLayoutManager);
TopLinearLayoutManager bottomLayoutManager = new TopLinearLayoutManager(this, LinearLayoutManager.VERTICAL);
bottomRecyclerView.setLayoutManager(bottomLayoutManager);
final OnScrollListener topOnScrollListener = new OnScrollListener(topRecyclerView, bottomRecyclerView);
final OnScrollListener bottomOnScrollListener = new OnScrollListener(bottomRecyclerView, topRecyclerView);
topRecyclerView.addOnScrollListener(topOnScrollListener);
bottomRecyclerView.addOnScrollListener(bottomOnScrollListener);
GravitySnapHelper snapHelperTop = new GravitySnapHelper(Gravity.START);
snapHelperTop.attachToRecyclerView(topRecyclerView);
}
class OnScrollListener extends RecyclerView.OnScrollListener {
private RecyclerView thisRecyclerView;
private RecyclerView otherRecyclerView;
int lastItemPos = Integer.MIN_VALUE;
OnScrollListener(RecyclerView thisRecyclerView, RecyclerView otherRecyclerView) {
this.thisRecyclerView = thisRecyclerView;
this.otherRecyclerView = otherRecyclerView;
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
masterView = thisRecyclerView;
} else if (newState == RecyclerView.SCROLL_STATE_IDLE && masterView == thisRecyclerView) {
masterView = null;
lastItemPos = Integer.MIN_VALUE;
}
}
#Override
public void onScrolled(RecyclerView recyclerview, int dx, int dy) {
super.onScrolled(recyclerview, dx, dy);
if ((dx == 0 && dy == 0) || (masterView != thisRecyclerView)) {
return;
}
int currentItem = ((TopLinearLayoutManager) thisRecyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
if (lastItemPos == currentItem) {
return;
}
lastItemPos = currentItem;
otherRecyclerView.getLayoutManager().smoothScrollToPosition(otherRecyclerView, null, currentItem);
}
}
}
Another simple solution working fine in my device
Variable
RecyclerView horizontalRecyclerView, verticalRecyclerView;
LinearLayoutManager horizontalLayoutManager, verticalLayoutManager;
ArrayList<String> arrayList = new ArrayList<>();
ArrayList<String> arrayList2 = new ArrayList<>();
RecyclerView code
horizontalRecyclerView = findViewById(R.id.horizontalRc);
verticalRecyclerView = findViewById(R.id.verticalRc);
horizontalRecyclerView.setHasFixedSize(true);
verticalRecyclerView.setHasFixedSize(true);
horizontalLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
verticalLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
horizontalRecyclerView.setLayoutManager(horizontalLayoutManager);
verticalRecyclerView.setLayoutManager(verticalLayoutManager);
for (int i = 0; i < 50; i++) {
arrayList.add("" + i);
arrayList2.add("" + i);
}
MyDataAdapter horizontalAdapter = new MyDataAdapter(this, arrayList);
MyDataAdapter verticalAdapter = new MyDataAdapter(this, arrayList2);
horizontalRecyclerView.setAdapter(horizontalAdapter);
verticalRecyclerView.setAdapter(verticalAdapter);
logic inside RecyclerView.addOnScrollListener
horizontalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int pos = horizontalLayoutManager.findFirstCompletelyVisibleItemPosition();
verticalLayoutManager.scrollToPositionWithOffset(pos, 20);
}
});
verticalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int pos = verticalLayoutManager.findFirstCompletelyVisibleItemPosition();
horizontalLayoutManager.scrollToPositionWithOffset(pos, 20);
}
});
Hope it help some one
WHOLE Code
public class Main4Activity extends AppCompatActivity {
RecyclerView horizontalRecyclerView, verticalRecyclerView;
LinearLayoutManager horizontalLayoutManager, verticalLayoutManager;
ArrayList<String> arrayList = new ArrayList<>();
ArrayList<String> arrayList2 = new ArrayList<>();
boolean isVertical = true, isHorizontal = true;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main4);
horizontalRecyclerView = findViewById(R.id.horizontalRc);
verticalRecyclerView = findViewById(R.id.verticalRc);
horizontalRecyclerView.setHasFixedSize(true);
verticalRecyclerView.setHasFixedSize(true);
horizontalLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
verticalLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
horizontalRecyclerView.setLayoutManager(horizontalLayoutManager);
verticalRecyclerView.setLayoutManager(verticalLayoutManager);
for (int i = 0; i < 50; i++) {
arrayList.add("" + i);
arrayList2.add("" + i);
}
MyDataAdapter horizontalAdapter = new MyDataAdapter(this, arrayList);
MyDataAdapter verticalAdapter = new MyDataAdapter(this, arrayList2);
horizontalRecyclerView.setAdapter(horizontalAdapter);
verticalRecyclerView.setAdapter(verticalAdapter);
horizontalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int pos = horizontalLayoutManager.findFirstCompletelyVisibleItemPosition();
verticalLayoutManager.scrollToPositionWithOffset(pos, 20);
}
});
verticalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int pos = verticalLayoutManager.findFirstCompletelyVisibleItemPosition();
horizontalLayoutManager.scrollToPositionWithOffset(pos, 20);
}
});
/* horizontalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int pos = horizontalLayoutManager.findFirstCompletelyVisibleItemPosition();
verticalLayoutManager.scrollToPositionWithOffset(pos, 20);
*//*if (isHorizontal) {
int pos = horizontalLayoutManager.findFirstCompletelyVisibleItemPosition();
verticalLayoutManager.scrollToPositionWithOffset(pos, 20);
Log.e("isHorizontal", "TRUE");
isVertical = false;
} else {
isHorizontal = true;
}*//*
}
*//* #Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
isVertical = true;
}*//*
});
verticalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
*//* #Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
isHorizontal = true;
}
*//*
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int pos = verticalLayoutManager.findFirstCompletelyVisibleItemPosition();
horizontalLayoutManager.scrollToPositionWithOffset(pos, 20);
*//* if (isVertical) {
int pos = verticalLayoutManager.findFirstCompletelyVisibleItemPosition();
horizontalLayoutManager.scrollToPositionWithOffset(pos, 20);
Log.e("isVertical", "TRUE");
isHorizontal = false;
} else {
isVertical = true;
}*//*
}
});*/
}
}
Adapter code
public class MyDataAdapter extends RecyclerView.Adapter<MyDataAdapter.ViewHolder> {
Context context;
ArrayList<String> arrayList;
public MyDataAdapter(Context context, ArrayList<String> arrayList) {
this.context = context;
this.arrayList = arrayList;
}
#Override
public MyDataAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.temp, parent, false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(MyDataAdapter.ViewHolder holder, int position) {
if (position % 2 == 0) {
holder.tvNumber.setBackgroundResource(R.color.colorGreen);
} else {
holder.tvNumber.setBackgroundResource(R.color.colorRed);
}
holder.tvNumber.setText(arrayList.get(position));
}
#Override
public int getItemCount() {
return arrayList.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView tvNumber;
public ViewHolder(View itemView) {
super(itemView);
tvNumber = itemView.findViewById(R.id.tvNumber);
}
}
}
Activity layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="#+id/horizontalRc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<android.support.v7.widget.RecyclerView
android:id="#+id/verticalRc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:visibility="gone" />
</LinearLayout>
temp Layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="40dp">
<TextView
android:id="#+id/tvNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp" />
</LinearLayout>
COLOR
<color name="colorGreen">#307832</color>
<color name="colorRed">#ff4c4c</color>

Detect when RecyclerView reaches the bottom most position while scrolling

I have this code for a 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);
I need to know when the RecyclerView reaches bottom most position while scrolling. Is it possible ? If yes, how ?
there is also a simple way to do it
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();
}
}
});
direction integers: -1 for up, 1 for down, 0 will always return false.
Use this code for avoiding repeated calls
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");
}
}
});
Just implement a addOnScrollListener() on your recyclerview. Then inside the scroll listener implement the code below.
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
}
}
};
After not being satisfied with most the other answers in this thread, I found something I think is better and is not anywhere on here.
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 TOP
}
}
});
This is simple and will hit exactly one time under all conditions when you have scrolled to the top or bottom.
Answer is in Kotlin, it will work in Java. IntelliJ should convert it for you if you copy and paste.
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)
}
})
This will also work for LinearLayoutManager because it has the same methods used above. Namely findLastVisibleItemPosition() and getItemCount() (itemCount in Kotlin).
I was not getting a perfect solution by the above answers because it was triggering twice even on 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")
}
Alternative solution which I had found some days ago,
rv_repatriations.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (!recyclerView.canScrollVertically(RecyclerView.FOCUS_DOWN) && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE
&& !isLoaded
) {
isLoaded = true
//do what you want here and after calling the function change the value of boolean
Log.e("RepatriationFragment", "Scroll end reached")
}
}
})
Using a boolean to ensure that it's not called multiple times when we hit the bottom.
Try This
I have used above answers it runs always when you will go at the end of recycler view,
If you want to check only one time whether it is on a bottom or not?
Example:- If I have the list of 10 items whenever I go on the bottom it will display me and again if I scroll top to bottom it will not print again, and if you add more lists and you go there it will again display.
Note:- Use this method when you deal with offset in hitting API
Create a class named as 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);
}
use this class like this
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();
}
});
Its running perfect at my end, commnent me if you are getting any issue
Kotlin Answer
You can use this Kotlin function for best practice of bottom scroll following to create infinite or endless scrolling.
// Scroll listener.
private fun setupListenerPostListScroll() {
val scrollDirectionDown = 1 // Scroll down is +1, up is -1.
var currentListSize = 0
mRecyclerView.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (!recyclerView.canScrollVertically(scrollDirectionDown)
&& newState == RecyclerView.SCROLL_STATE_IDLE
) {
val listSizeAfterLoading = recyclerView.layoutManager!!.itemCount
// List has more item.
if (currentListSize != listSizeAfterLoading) {
currentListSize = listSizeAfterLoading
// Get more posts.
postListScrollUpAction(listSizeAfterLoading)
}
else { // List comes limit.
showToastMessageShort("No more items.")
}
}
}
})
}
There is my implementation, it is very useful for StaggeredGridLayout.
Usage :
private EndlessScrollListener scrollListener =
new EndlessScrollListener(new EndlessScrollListener.RefreshList() {
#Override public void onRefresh(int pageNumber) {
//end of the list
}
});
rvMain.addOnScrollListener(scrollListener);
Listener implementation :
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);
} }
I was also searching for this question but I didn't find the answer that satisfied me, so I create own realization of recyclerView.
other solutions is less precise then mine. for example: if the last item is pretty big (lot of text) then callback of other solutions will come much earlier then recyclerView realy reached bottom.
my sollution fix this issue.
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)
}
then in your activity/fragment:
override fun onCreate() {
customRecyclerView.layoutManager = LinearLayoutManager(context)
}
override fun onResume() {
super.onResume()
customRecyclerView.setTopAndBottomListener(this)
}
override fun onStop() {
super.onStop()
customRecyclerView.setTopAndBottomListener(null)
}
hope it will hepl someone ;-)
Using Kotlin
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (!recyclerView.canScrollVertically(1)) {
Toast.makeText(context, "Last", Toast.LENGTH_LONG).show();
}
}
})
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
}
}
})
Most of the answers are poorly constructed and have some issues. One of the common issues is if the user scrolls fast, the end reached block executes multiple times I've found a solution, where the end block runs just 1 single time.
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (yourLayoutManager.findLastVisibleItemPosition() ==
yourLayoutManager.itemCount - 1 && !recyclerView.canScrollVertically(1)) {
Logger.log("End reached")
// Do your operations
}
super.onScrolled(recyclerView, dx, dy)
}
P.S. Sometimes if RecyclerView gets empty, the end listener might get called. As a solution, you can also add this check in the above code.
if (recyclerView.adapter?.itemCount!! > 0)
You can use this, if you put 1 thats will be indicated when you stay in end of list, if you want now when you stay in the start of the list you change 1 for -1
recyclerChat.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (!recyclerView.canScrollVertically(1)) {
}
}
})
This is my solution after reading all answers in this post.
I only want to show loading when the last item is shown at the end of the list and listview length is larger than screen height, meaning if there's only one or two items in the list, won't show the loading.
private var isLoadMoreLoading: Boolean = false
mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (!isLoadMoreLoading) {
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() == (list.size-1)) {
if (recyclerView.canScrollVertically(-1)) {
adapter?.addLoadMoreView()
loadMore()
}
}
}
}
})
private fun loadMore() {
isLoadMoreLoading = true
//call loadMore api
}
Because of this linearLayoutManager.findLastCompletelyVisibleItemPosition() == (list.size-1), we can know last item is shown, but we also need to know listview can scroll or not.
Therefore I added recyclerView.canScrollVertically(-1). Once you hit the bottom of the list, it cannot scroll down anymore. -1 means list can scroll up. That means listview length is larger than screen height.
This answer is in kotlin.
The most simple way to do it is in the adapter like this:
#Override
public void onBindViewHolder(HistoryMessageListAdapter.ItemViewHolder holder, int position) {
if (position == getItemCount()-1){
listener.onLastItemReached();
}
}
Because as soon as the last item is recycled the listener is triggered.
This is my solution:
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
}
}
}
We can use Interface for get the position
Interface :
Create an Interface for listener
public interface OnTopReachListener { void onTopReached(int position);}
Activity :
mediaRecycleAdapter = new MediaRecycleAdapter(Class.this, taskList); recycle.setAdapter(mediaRecycleAdapter); mediaRecycleAdapter.setOnSchrollPostionListener(new OnTopReachListener() {
#Override
public void onTopReached(int position) {
Log.i("Position","onTopReached "+position);
}
});
Adapter :
public void setOnSchrollPostionListener(OnTopReachListener topReachListener) {
this.topReachListener = topReachListener;}#Override public void onBindViewHolder(MyViewHolder holder, int position) {if(position == 0) {
topReachListener.onTopReached(position);}}
Use this method after declaring and initializing your recyclerView with adapter
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(dy > 0){ // Scrolling Down
//**Look at the condition inside if, this is how you can check, either you //have scrolled till last element of your recyclerView or not.**
//------------------------------------------------------------------------------
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (linearLayoutManager != null && linearLayoutManager.findLastCompletelyVisibleItemPosition() == recyclerViewList.size() - 1) {
//bottom of list!
}
//------------------------------------------------------------------------------
}else if(dy < 0){
// Scrolling Up
}
}
});
After a long search, I found the prefect solution, that only scrolls to bottom when you want to, and also maintains the smooth scroll behavior you get by using a ListAdapter:
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
scrollToBottomIfUserIsAtTheBottom(linearLayoutManager, positionStart)
}
})
private fun scrollToBottomIfUserIsAtTheBottom(linearLayout: LinearLayoutManager, positionStart: Int) {
val messageCount = adapter.itemCount
val lastVisiblePosition = linearLayout.findLastCompletelyVisibleItemPosition()
if (lastVisiblePosition == -1 ||
(positionStart >= (messageCount - 1) &&
lastVisiblePosition == (positionStart - 1)))
{
recyclerView.scrollToPosition(positionStart)
}
}
A few notes:
If you use a list adapter and you add a new item to the list each update, you must create a new list each time for the smooth scroll to work.
Don't use smoothScrollToPosition with the ListAdapter! It ruins the ListAdapter "smoothier" behavior that happens when the diff works good and it detects that the change between old and new list is in a newly added item.
linearLayoutManager is adapter.layoutManager as LinearLayoutManager
Try this,
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int visibleItemCount = recyclerView.getLayoutManager().getChildCount();
int totalItemCount = recyclerView.getLayoutManager().getItemCount();
int firstVisibleItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
final int lastItem = firstVisibleItem + visibleItemCount;
if(lastItem == totalItemCount) {
if(previousLast != lastItem) {
previousLast = lastItem;
load();
}
}
}
});
Kotlin version of ScrollListener with the ability to set the indent from the last element to load
class MyScrollListener(
private val indentForAction: Int = DEFAULT_INDENT_TO_INVOKE_ACTION,
private val onEndReached: () -> Unit,
) : RecyclerView.OnScrollListener() {
private var currentListSize = 0
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val linearLayoutManager = recyclerView.layoutManager as? LinearLayoutManager
?: error("ScrollListener works only with LinearLayoutManager")
val lastVisiblePosition = linearLayoutManager.findLastVisibleItemPosition()
val itemCount = linearLayoutManager.itemCount
if (lastVisiblePosition > itemCount - indentForAction && itemCount > 0) {
if (currentListSize != itemCount) {
currentListSize = itemCount
onEndReached.invoke()
}
}
}
private companion object {
const val DEFAULT_INDENT_TO_INVOKE_ACTION = 8
}
}

RecyclerView and SwipeRefreshLayout

I'm using the new RecyclerView-Layout in a SwipeRefreshLayout and experienced a strange behaviour. When scrolling the list back to the top sometimes the view on the top gets cut in.
If i try to scroll to the top now - the Pull-To-Refresh triggers.
If i try and remove the Swipe-Refresh-Layout around the Recycler-View the Problem is gone. And its reproducable on any Phone (not only L-Preview devices).
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<android.support.v7.widget.RecyclerView
android:id="#+id/hot_fragment_recycler"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
That's my layout - the rows are built dynamically by the RecyclerViewAdapter (2 Viewtypes in this List).
public class HotRecyclerAdapter extends TikDaggerRecyclerAdapter<GameRow> {
private static final int VIEWTYPE_GAME_TITLE = 0;
private static final int VIEWTYPE_GAME_TEAM = 1;
#Inject
Picasso picasso;
public HotRecyclerAdapter(Injector injector) {
super(injector);
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position, int viewType) {
switch (viewType) {
case VIEWTYPE_GAME_TITLE: {
TitleGameRowViewHolder holder = (TitleGameRowViewHolder) viewHolder;
holder.bindGameRow(picasso, getItem(position));
break;
}
case VIEWTYPE_GAME_TEAM: {
TeamGameRowViewHolder holder = (TeamGameRowViewHolder) viewHolder;
holder.bindGameRow(picasso, getItem(position));
break;
}
}
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
switch (viewType) {
case VIEWTYPE_GAME_TITLE: {
View view = inflater.inflate(R.layout.game_row_title, viewGroup, false);
return new TitleGameRowViewHolder(view);
}
case VIEWTYPE_GAME_TEAM: {
View view = inflater.inflate(R.layout.game_row_team, viewGroup, false);
return new TeamGameRowViewHolder(view);
}
}
return null;
}
#Override
public int getItemViewType(int position) {
GameRow row = getItem(position);
if (row.isTeamGameRow()) {
return VIEWTYPE_GAME_TEAM;
}
return VIEWTYPE_GAME_TITLE;
}
Here's the Adapter.
hotAdapter = new HotRecyclerAdapter(this);
recyclerView.setHasFixedSize(false);
recyclerView.setAdapter(hotAdapter);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
contentView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
loadData();
}
});
TypedArray colorSheme = getResources().obtainTypedArray(R.array.main_refresh_sheme);
contentView.setColorSchemeResources(colorSheme.getResourceId(0, -1), colorSheme.getResourceId(1, -1), colorSheme.getResourceId(2, -1), colorSheme.getResourceId(3, -1));
And the code of the Fragment containing the Recycler and the SwipeRefreshLayout.
If anyone else has experienced this behaviour and solved it or at least found the reason for it?
write the following code in addOnScrollListener of the RecyclerView
Like this:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener(){
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
int topRowVerticalPosition =
(recyclerView == null || recyclerView.getChildCount() == 0) ? 0 : recyclerView.getChildAt(0).getTop();
swipeRefreshLayout.setEnabled(topRowVerticalPosition >= 0);
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});
Before you use this solution:
RecyclerView is not complete yet, TRY NOT TO USE IT IN PRODUCTION UNLESS YOU'RE LIKE ME!
As for November 2014, there are still bugs in RecyclerView that would cause canScrollVertically to return false prematurely. This solution will resolve all scrolling problems.
The drop in solution:
public class FixedRecyclerView extends RecyclerView {
public FixedRecyclerView(Context context) {
super(context);
}
public FixedRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FixedRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean canScrollVertically(int direction) {
// check if scrolling up
if (direction < 1) {
boolean original = super.canScrollVertically(direction);
return !original && getChildAt(0) != null && getChildAt(0).getTop() < 0 || original;
}
return super.canScrollVertically(direction);
}
}
You don't even need to replace RecyclerView in your code with FixedRecyclerView, replacing the XML tag would be sufficient! (The ensures that when RecyclerView is complete, the transition would be quick and simple)
Explanation:
Basically, canScrollVertically(boolean) returns false too early,so we check if the RecyclerView is scrolled all the way to the top of the first view (where the first child's top would be 0) and then return.
EDIT:
And if you don't want to extend RecyclerView for some reason, you can extend SwipeRefreshLayout and override the canChildScrollUp() method and put the checking logic in there.
EDIT2:
RecyclerView has been released and so far there's no need to use this fix.
I came across the same problem recently. I tried the approach suggested by #Krunal_Patel, But It worked most of the times in my Nexus 4 and didn't work at all in samsung galaxy s2. While debugging, recyclerView.getChildAt(0).getTop() is always not correct for RecyclerView. So, After going through various methods, I figured that we can make use of the method findFirstCompletelyVisibleItemPosition() of the LayoutManager to predict whether the first item of the RecyclerView is visible or not, to enable SwipeRefreshLayout.Find the code below. Hope it helps someone trying to fix the same issue. Cheers.
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
}
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
swipeRefresh.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0);
}
});
This is how I have resolved this issue in my case. It might be useful for someone else who end up here for searching solutions similar to this.
recyclerView.addOnScrollListener(new OnScrollListener()
{
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy)
{
// TODO Auto-generated method stub
super.onScrolled(recyclerView, dx, dy);
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState)
{
// TODO Auto-generated method stub
//super.onScrollStateChanged(recyclerView, newState);
int firstPos=linearLayoutManager.findFirstCompletelyVisibleItemPosition();
if (firstPos>0)
{
swipeLayout.setEnabled(false);
}
else {
swipeLayout.setEnabled(true);
}
}
});
I hope this might definitely help someone who are looking for similar solution.
Source Code
https://drive.google.com/open?id=0BzBKpZ4nzNzURkRGNVFtZXV1RWM
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
}
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
swipeRefresh.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0);
}
});
None of the answers worked for me, but I managed to implement my own solution by making a custom implementation of LinearLayoutManager. Posting it here in case someone else needs it.
class LayoutManagerScrollFixed(context: Context) : LinearLayoutManager(context) {
override fun smoothScrollToPosition(
recyclerView: RecyclerView?,
state: RecyclerView.State?,
position: Int
) {
super.smoothScrollToPosition(recyclerView, state, position)
val child = getChildAt(0)
if (position == 0 && recyclerView != null && child != null) {
scrollVerticallyBy(child.top - recyclerView.paddingTop, recyclerView.Recycler(), state)
}
}
Then, you just call
recyclerView?.layoutManager = LayoutManagerScrollFixed(requireContext())
And it's working!
unfortunately, this is a known bug in LinearLayoutManager. It does not computeScrollOffset properly when the first item is visible.
will be fixed when it is released.
I have experienced same issue. I solved it by adding scroll listener that will wait until expected first visible item is drawn on the RecyclerView. You can bind other scroll listeners too, along this one. Expected first visible value is added to use it as threshold position when the SwipeRefreshLayout should be enabled in cases where you use header view holders.
public class SwipeRefreshLayoutToggleScrollListener extends RecyclerView.OnScrollListener {
private List<RecyclerView.OnScrollListener> mScrollListeners = new ArrayList<RecyclerView.OnScrollListener>();
private int mExpectedVisiblePosition = 0;
public SwipeRefreshLayoutToggleScrollListener(SwipeRefreshLayout mSwipeLayout) {
this.mSwipeLayout = mSwipeLayout;
}
private SwipeRefreshLayout mSwipeLayout;
public void addScrollListener(RecyclerView.OnScrollListener listener){
mScrollListeners.add(listener);
}
public boolean removeScrollListener(RecyclerView.OnScrollListener listener){
return mScrollListeners.remove(listener);
}
public void setExpectedFirstVisiblePosition(int position){
mExpectedVisiblePosition = position;
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
notifyScrollStateChanged(recyclerView,newState);
LinearLayoutManager llm = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstVisible = llm.findFirstCompletelyVisibleItemPosition();
if(firstVisible != RecyclerView.NO_POSITION)
mSwipeLayout.setEnabled(firstVisible == mExpectedVisiblePosition);
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
notifyOnScrolled(recyclerView, dx, dy);
}
private void notifyOnScrolled(RecyclerView recyclerView, int dx, int dy){
for(RecyclerView.OnScrollListener listener : mScrollListeners){
listener.onScrolled(recyclerView, dx, dy);
}
}
private void notifyScrollStateChanged(RecyclerView recyclerView, int newState){
for(RecyclerView.OnScrollListener listener : mScrollListeners){
listener.onScrollStateChanged(recyclerView, newState);
}
}
}
Usage:
SwipeRefreshLayoutToggleScrollListener listener = new SwipeRefreshLayoutToggleScrollListener(mSwiperRefreshLayout);
listener.addScrollListener(this); //optional
listener.addScrollListener(mScrollListener1); //optional
mRecyclerView.setOnScrollLIstener(listener);
I run into the same problem. My solution is overriding onScrolled method of OnScrollListener.
Workaround is here:
recyclerView.setOnScrollListener(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) {
super.onScrolled(recyclerView, dx, dy);
int offset = dy - ydy;//to adjust scrolling sensitivity of calling OnRefreshListener
ydy = dy;//updated old value
boolean shouldRefresh = (linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0)
&& (recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) && offset > 30;
if (shouldRefresh) {
swipeRefreshLayout.setRefreshing(true);
} else {
swipeRefreshLayout.setRefreshing(false);
}
}
});
Here's one way to handle this, which also handles ListView/GridView.
public class SwipeRefreshLayout extends android.support.v4.widget.SwipeRefreshLayout
{
public SwipeRefreshLayout(Context context)
{
super(context);
}
public SwipeRefreshLayout(Context context,AttributeSet attrs)
{
super(context,attrs);
}
#Override
public boolean canChildScrollUp()
{
View target=getChildAt(0);
if(target instanceof AbsListView)
{
final AbsListView absListView=(AbsListView)target;
return absListView.getChildCount()>0
&&(absListView.getFirstVisiblePosition()>0||absListView.getChildAt(0)
.getTop()<absListView.getPaddingTop());
}
else
return ViewCompat.canScrollVertically(target,-1);
}
}
The krunal's solution is good, but it works like hotfix and does not cover some specific cases, for example this one:
Let's say that the RecyclerView contains an EditText at the middle of screen. We start application (topRowVerticalPosition = 0), taps on the EditText. As result, software keyboard shows up, size of the RecyclerView is decreased, it is automatically scrolled by system to keep the EditText visible and topRowVerticalPosition should not be 0, but onScrolled is not called and topRowVerticalPosition is not recalculated.
Therefore, I suggest this solution:
public class SupportSwipeRefreshLayout extends SwipeRefreshLayout {
private RecyclerView mInternalRecyclerView = null;
public SupportSwipeRefreshLayout(Context context) {
super(context);
}
public SupportSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setInternalRecyclerView(RecyclerView internalRecyclerView) {
mInternalRecyclerView = internalRecyclerView;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mInternalRecyclerView.canScrollVertically(-1)) {
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
After you specify internal RecyclerView to SupportSwipeRefreshLayout, it will automatically send touch event to SupportSwipeRefreshLayout if RecyclerView cannot be scrolled up and to RecyclerView otherwise.
Single line solution.
setOnScrollListener is deprecated.
You can use setOnScrollChangeListener for same purspose like this :
recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0));
In case of someone find this question and is not satisfied by the answer :
It seems that SwipeRefreshLayout is not compatible with adapters that have more than 1 item type.
If you are using recyclerview without scrollview you can do this and it will work
recyclerview.isNestedScrollingEnabled = true

Categories

Resources