I'm trying to create an EPG in android using Recyclerviews. It needs fixed top row which scrolls horizontally to show programs corresponding to time and fixed left most column which scrolls vertically to show various channels.
Based on this SO answer, I came with the below
<?xml version="1.0" encoding="utf-8"?>
<!--Outer container layout-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".MainActivity">
<!--To display Channels list-->
<LinearLayout
android:layout_width="150dp"
android:layout_height="match_parent"
android:orientation="vertical">
<!--Position (0,0)-->
<TextView
android:id="@+id/tv_change_date"
android:layout_width="match_parent"
android:layout_height="50dp" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rcv_channel_name"
android:scrollbars="none"
android:layout_width="150dp"
android:layout_height="match_parent"/>
</LinearLayout>
<!--To display Time and Programs list-->
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<!--Time horizontal list-->
<android.support.v7.widget.RecyclerView
android:id="@+id/rcv_vertical_header"
android:scrollbars="none"
android:layout_width="match_parent"
android:layout_height="50dp"/>
<!--Vertical list whose each element is horizontal list to show programs-->
<android.support.v7.widget.RecyclerView
android:id="@+id/rcv_vertical"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
Now, I need to sync the vertical scroll of rcv_vertical and rcv_channel_name. I implemented it as in this github project.
public class SelfRemovingOnScrollListener extends RecyclerView.OnScrollListener {
@Override
public final void onScrollStateChanged(@NonNull final RecyclerView recyclerView, final int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
recyclerView.removeOnScrollListener(this);
}
}
}
In MainActivity
private final RecyclerView.OnScrollListener channelScrollListener = new SelfRemovingOnScrollListener() {
@Override
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
programsRecyclerView.scrollBy(dx, dy);
}
};
private final RecyclerView.OnScrollListener programScrollListener = new SelfRemovingOnScrollListener() {
@Override
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
channelsRecyclerView.scrollBy(dx, dy);
}
};
@Override
protected void onStart() {
super.onStart();
//Sync channel name RCV and Programs RCV scrolling
channelsRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
private int mLastY;
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
Log.d("debug", "LEFT: onInterceptTouchEvent");
final Boolean ret = rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
if (!ret) {
onTouchEvent(rv, e);
}
return Boolean.FALSE;
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
Log.d("debug", "LEFT: onTouchEvent");
final int action;
if ((action = e.getAction()) == MotionEvent.ACTION_DOWN && programsRecyclerView
.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
mLastY = rv.getScrollY();
Log.d("scroll","channelsRecyclerView Y: "+mLastY);
rv.addOnScrollListener(channelScrollListener);
}
else {
if (action == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
rv.removeOnScrollListener(channelScrollListener);
}
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
Log.d("debug", "LEFT: onRequestDisallowInterceptTouchEvent");
}
});
programsRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
private int mLastY;
@Override
public boolean onInterceptTouchEvent(@NonNull final RecyclerView rv, @NonNull final
MotionEvent e) {
Log.d("debug", "RIGHT: onInterceptTouchEvent");
final Boolean ret = rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
if (!ret) {
onTouchEvent(rv, e);
}
return Boolean.FALSE;
}
@Override
public void onTouchEvent(@NonNull final RecyclerView rv, @NonNull final MotionEvent e) {
Log.d("debug", "RIGHT: onTouchEvent");
final int action;
if ((action = e.getAction()) == MotionEvent.ACTION_DOWN && channelsRecyclerView
.getScrollState
() == RecyclerView.SCROLL_STATE_IDLE) {
mLastY = rv.getScrollY();
rv.addOnScrollListener(programScrollListener);
Log.d("scroll","programsRecyclerView Y: "+mLastY);
}
else {
if (action == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
rv.removeOnScrollListener(programScrollListener);
}
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(final boolean disallowIntercept) {
Log.d("debug", "RIGHT: onRequestDisallowInterceptTouchEvent");
}
});
[![}][3]][3]
It works fine until I do a horizontal scroll inside the HorizontalScrollView. After that the left recyclerview "rcv_channel_name" scrolls faster than the right rcv_vertical.
Any help or suggestion to fix this is highly appreciated.
EDITED! I've finally managed to make this working! Here's my short solution (see also old answer for focus-based input with d-pad):
First of all you need 3 recyclers (in Fragment or in Activity):
public RecyclerView vertical_recycler, current_focus_recycler, time_recycler;
Then add
public int scroll_offset;
This variable will hold scroll offset in pixels for further use. Next properly populate your vertical RecyclerView, make sure that your RecyclerView have horizontal RecyclerViews as children. Also make sure that your time_recycler will be longer than any of horizontal_recyclers (returning Integer.MAX_VALUE in getItemCount in its adapter will make sure that you'r good.. sort of... ):
@Override
public int getItemCount() {
return Integer.MAX_VALUE;
}
Next add scroll listener in Activity or in Fragment
time_recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
horizontal_scroll(dx);
if (dx == 0) {
scroll_offset = 0;
} else {
scroll_offset += dx;
}
}
});
public void horizontal_scroll(int dx) {
for (int i = 0, n = vertical_recycler.getChildCount(); i < n; ++i) {
if (vertical_recycler.getChildAt(i).findViewById(R.id.horizontal_recycler) != current_focus_recycler) {
vertical_recycler.getChildAt(i).findViewById(R.id.horizontal_recycler).scrollBy(dx, 0);
}
}
}
In vertical RecyclerView's adapter add along with other functionality needed:
private AdapterSheduleHorizontal adapter_horizontal;
@Override
public void onViewAttachedToWindow(AdapterSheduleVertical.VerticalViewHolder holder){
super.onViewAttachedToWindow(holder);
((LinearLayoutManager) holder.horizontal_recycler.getLayoutManager())
.scrollToPositionWithOffset(0,
-fragmentShedule.scroll_offset);
holder.horizontal_recycler.addOnScrollListener(onScrollListener);
}
@Override
public void onViewDetachedFromWindow(VerticalViewHolder holder) {
super.onViewDetachedFromWindow(holder);
holder.horizontal_recycler.removeOnScrollListener(onScrollListener);
}
RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(recyclerView == fragmentShedule.current_focus_recycler) {
fragmentShedule.time_recycler.scrollBy(dx, 0);
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int state) {
super.onScrollStateChanged(recyclerView, state);
switch (state) {
case RecyclerView.SCROLL_STATE_IDLE:
fragmentShedule.current_focus_recycler = null;
break;
}
}
};
@Override
public void onBindViewHolder(VerticalViewHolder sheduleViewHolder, final int position) {
...
LinearLayoutManager lm = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
sheduleViewHolder.horizontal_recycler.setLayoutManager(lm);
adapter_horizontal = new AdapterSheduleHorizontal(fragmentShedule, context, dayItemList);
sheduleViewHolder.horizontal_recycler.setAdapter(adapter_horizontal);
...
}
Next in horizontal RecyclerView's adapter
...
RecyclerView parent_recyclerview;
@Override
public void onBindViewHolder(final HorizontalViewHolder sheduleViewHolder, final int i) {
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
lp.width = 500;//some width to
sheduleViewHolder.shedule_item.setLayoutParams(lp);
sheduleViewHolder.itemView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
fragmentShedule.current_focus_recycler = parent_recyclerview;
break;
}
return false;
}
});
sheduleViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, "click"+i, Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
parent_recyclerview = recyclerView;
}
I believe that's all. Please, feel free to comment and tell me if it's not working for you I will double check my code (I have tested it for both vertical and horizontal scroll of all children).
OLD ANSWER
OK, I'll show my approach which works for my EPG-look-like application.
Lets assume you have RecyclerView with RecyclerView childs. Parent scrolls vertically, childs - horizontally. On top of this layout you have another RecyclerView with time line (scrolling horizontally as well).
You need to scroll timeline in order to scroll all recyclers-childs, except that one, that gains focus/is touched/is scrolling (if you're going to use no touchscreen capabilities, you can use only focus of childs of horizontal view).
With this been said, you need to register which child RecyclerView is currently scrolling.
public RecyclerView vertical_recycler, current_focus_recycler, time_recycler;
As well I registered method that scrolls all horizontal RecyclerViews all at once:
public void horizontal_scroll(int dx) {
for (int i = 0, n = vertical_recycler.getChildCount(); i < n; ++i) {
if (vertical_recycler.getChildAt(i).findViewById(R.id.horizontal_recycler) != current_focus_recycler) {
vertical_recycler.getChildAt(i).findViewById(R.id.horizontal_recycler).scrollBy(dx, 0);
}
}
}
After setting adapter for my time_recycler I added listener:
time_recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
horizontal_scroll(dx);
}
});
So far so good. Than in adapter that corresponds to horizontal RecyclerView-child I added:
RecyclerView parent_recyclerview;
RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
((ActivityMain) context).time_recycler.scrollBy(dx, 0);
}
};
//...
@Override
public void onBindViewHolder(final HorizontalViewHolder viewHolder, final int i) {
viewHolder.shedule_item.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (v.hasFocus()) {
((ActivityMain) context).current_focus_recycler = parent_recyclerview;
parent_recyclerview.addOnScrollListener(onScrollListener);
} else {
parent_recyclerview.removeOnScrollListener(onScrollListener);
}
}
});
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
parent_recyclerview = recyclerView;
}
//...
class HorizontalViewHolder extends RecyclerView.ViewHolder {
private LinearLayout shedule_item;
public HorizontalViewHolder(View view) {
super(view);
shedule_item = (LinearLayout) view.findViewById(R.id.shedule_item);
}
}
And layout of horizontal RecyclerView's item is:
<LinearLayout
android:id="@+id/shedule_item"
.../>
...
</LinearLayout>
This works in my case if I use keyboard and navigating through epg table with arrow keys. With some adjustments (with some pain in the ash), you'll be able to intercept touch events from parent horizontal recycler to it's childs. Hope that will help you.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With