6

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 design

    <?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.

Community
  • 1
  • 1
Sathish
  • 346
  • 5
  • 9
  • i'm trying to do something similar, do you have the project in git? – xanexpt Sep 01 '16 at 10:27
  • @xanexpt you get the project from https://github.com/sathishkod/EPGguide – Sathish Sep 01 '16 at 12:48
  • Hi guys ! I have such a problem: my recyclerView items contains recyclerview and when I make a scroll they do not scroll synchronously. So how can I solve this problem ? –  Feb 24 '17 at 07:41
  • @SalutAmigo Rather trying to scroll all recyclerview synchronously, you may change the position of all recyclerviews (_moveToPosition_) when any one recycleview get settled after scroll. Any EventBus like RxBus might help you to change position of all non scrolled recyclerviews. – Sathish Feb 26 '17 at 08:17
  • maybe this can solve your problem: [linked question](https://stackoverflow.com/questions/31812674/scroll-multiple-horizontal-recyclerview-together/47873831#47873831) – Mario Velasco Dec 18 '17 at 17:52

2 Answers2

3

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.

Roman T.
  • 333
  • 3
  • 10
  • What is current_focus_recycler and where do you initialize it? – Phantom Jun 16 '17 at 08:02
  • I'm initializing it in activity (or fragment) as a public variable as mentioned, than I'm assigning value to it in onBindViewHolder as ((ActivityMain) context).current_focus_recycler = parent_recyclerview; – Roman T. Jun 20 '17 at 08:42
  • I'm experiencing issue with this code and i'll provide edited answer if I'd have answer. Issue is, that sync scroll works only for visible on screen items, because `vertical_recycler.getChildCount()` returns layout managers item count visible on screen. – Roman T. Jun 20 '17 at 08:45
  • Currently I've managed to implement such behaviour but only for keyboard (focus-oriented) input method based on child focus listener. By reverse engeneering Google Live TV app [link](https://source.android.com/devices/tv/reference-tv-app) – Roman T. Jun 28 '17 at 15:04
0

In continuation to the answer provided above by Roman T, this fixes the recycler views going out of sync incase of fling actions (mostly caused in the case of flings in opposite directions)

sheduleViewHolder.itemView.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {

            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    if (fragmentShedule.current_focus_recycler != null) {
                        //stop any scroll due to fling on recyclerview currently in focus
                        fragmentShedule.current_focus_recycler.stopScroll();
                    }
                    fragmentShedule.current_focus_recycler = parent_recyclerview;
                    break;
            }

            return false;
        }
    });


Added a stopScroll method to cancel any ongoing scroll due to fling on the last touched recycler view
P.S.: Submitting this as an answer because of low reputation.

Udit
  • 1,037
  • 6
  • 11