如何在安卓中同步滚动两个Recyclerview?

6
我正在尝试使用RecyclerView在Android中创建EPG。它需要一个固定的顶行,可以横向滚动以显示对应时间的节目,并且需要一个固定的最左列,可以纵向滚动以显示各种频道。
基于这个 SO答案,我得到了下面的设计:设计
    <?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>

现在,我需要同步rcv_vertical和rcv_channel_name的垂直滚动。我按照这个github项目中的实现方式进行了实现。
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);
        }
    }
}

在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]

在HorizontalScrollView中进行水平滚动之前,它可以正常工作。但是在此之后,左侧的rcv_channel_name RecyclerView比右侧的rcv_vertical RecyclerView滚动得更快。

非常感谢任何有关解决此问题的帮助或建议。


我正在尝试做类似的事情,你有这个项目在Git里吗? - xanexpt
@xanexpt,你可以从 https://github.com/sathishkod/EPGguide 获取该项目。 - Sathish
大家好!我有一个问题:我的recyclerView项包含recyclerview,当我滚动时它们不同步滚动。那么我该如何解决这个问题? - user5583732
@SalutAmigo,与其尝试同步滚动所有的recyclerview,不如在任何一个recycleview滚动完成后,改变所有recyclerviews(_moveToPosition_)的位置。任何类似RxBus的EventBus都可以帮助您更改所有未滚动的recyclerviews的位置。 - Sathish
也许这个可以解决你的问题:相关问题链接 - Mario Velasco
2个回答

3

已编辑! 我终于成功让它工作了!这是我的简短解决方案(也可以查看旧答案以获取基于焦点的d-pad输入):

首先,您需要3个可回收视图(在Fragment或Activity中):

public RecyclerView vertical_recycler, current_focus_recycler, time_recycler;

然后添加。
public int scroll_offset;

这个变量将保存滚动偏移量,以备后用。 接下来,请正确地填充您的垂直 RecyclerView,确保您的 RecyclerView 有水平的 RecyclerView 作为子项。还要确保 time_recycler 比所有 horizontal_recycler 都要长(在其适配器中返回 Integer.MAX_VALUE 可以确保您是对的...某种程度上...):

@Override
    public int getItemCount() {
        return Integer.MAX_VALUE;
    }

接下来在Activity或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);
            }
        }
    }

在垂直的RecyclerView适配器中,除了其他所需的功能外,还需要添加以下内容:
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);
...
    }

在水平RecyclerView的适配器中的下一个

...
    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;
    }

我相信这就是全部了。如有需要,请随时评论并告诉我,如果您遇到问题,我将仔细检查我的代码(我已经测试过所有子项目的纵向和横向滚动)。

旧答案

好的,我会展示我使用的方法,这个方法在我的类似EPG的应用程序中有效。

假设您有一个带有RecyclerView子项的RecyclerView。父级垂直滚动,子级水平滚动。 在此布局顶部,您还有另一个带有时间轴的RecyclerView(也可以水平滚动)。

您需要滚动时间轴以滚动所有可重用视图 - 除了正在获取焦点/被触摸/正在滚动的那个视图(如果您不使用触摸屏功能,则只能使用水平视图的子项目的焦点)。

说到这里,您需要注册当前正在滚动的子RecyclerView。

public RecyclerView vertical_recycler, current_focus_recycler, time_recycler;

同时,我还注册了一个方法来一次性滚动所有水平的RecyclerView:

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

在为我的time_recycler设置适配器之后,我添加了监听器:

time_recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                horizontal_scroll(dx);
            }
        });

到目前为止还不错。然后在对应水平RecyclerView子项的适配器中,我添加了以下内容:
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);
    }
}

水平RecyclerView的项的布局如下:

<LinearLayout
    android:id="@+id/shedule_item" 
            .../>
    ...
</LinearLayout>

如果我使用键盘并通过箭头键在电子节目指南表中导航,这个方法对我有效。经过一些调整(有点麻烦),您将能够拦截从父水平回收器传递到其子项的触摸事件。希望这可以帮到您。


current_focus_recycler是什么,你在哪里初始化它? - Phantom
我将其作为公共变量在活动(或片段)中进行初始化,如上所述,然后在onBindViewHolder中将值分配给它,如 ((ActivityMain) context).current_focus_recycler = parent_recyclerview; - Roman T.
我在这段代码中遇到了问题,如果我有答案,我会提供修改后的答案。问题是同步滚动仅适用于屏幕上可见的项目,因为vertical_recycler.getChildCount()只返回布局管理器在屏幕上可见的项目计数。 - Roman T.
目前我已经成功实现了这样的行为,但只适用于基于子焦点监听器的键盘(聚焦导向)输入方法。通过逆向工程谷歌Live TV应用程序[链接](https://source.android.com/devices/tv/reference-tv-app)进行了实现。 - Roman T.

0
继Roman T上面提供的答案之后,这个修复程序可以解决在快速滑动操作(主要是在相反方向的情况下)中导致回收视图不同步的问题。
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;
        }
    });


添加了一个stopScroll方法,用于取消由最后触摸的RecyclerView引起的任何正在进行的fling滚动。
附注:由于声望不高,因此将其提交为答案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接