Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to sync scrolling of two Recyclerviews in android?

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.

like image 935
Sathish Avatar asked Aug 18 '16 08:08

Sathish


1 Answers

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.

like image 192
Roman T. Avatar answered Nov 06 '22 20:11

Roman T.