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,
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>
Combining the two RecyclerView
s, 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:
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