如何同步垂直RecyclerViews中所有水平嵌套RecyclerViews的滚动?

时间:2016-12-15 19:56:04

标签: android scroll android-recyclerview nestedrecyclerview

背景

假设我有一个垂直的RecyclerView,其中每一行都是一个水平的RecyclerView。

我想做的是,无论您滚动哪个水平的RecyclerViews,所有其他的都会相应滚动,并始终与完全相同的滚动X坐标同步

问题

我实际上对基本操作没问题:

enter image description here

它的工作原理是拥有一个滚动的侦听器,所有水平的RecyclerViews都有,但是当一个人开始滚动时,它是唯一一个拥有它的人,同时它也影响其他人用它滚动。

但是,我做了两件主要问题:

  1. 在某些(水平)滚动操作中(可能是某些手势,如fling),多个RecyclerViews的滚动不同步,因此有些在X坐标中与其他坐标不同。

  2. 垂直滚动时,我无法正确设置X坐标。不仅如此,垂直RecyclerView的onBindViewHolder在我预期被调用时也不会被调用(当我滚动很多时调用,而不仅仅是当我看到一个被重新显示的时候被调用时)。

  3. 我尝试了什么

    这是当前的代码:

    MainActivity.java

    public class MainActivity extends AppCompatActivity {
        int mCurX = 0;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            final RecyclerView mainRecyclerView = (RecyclerView) findViewById(R.id.activity_main);
            final LinearLayoutManager verticalLinearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
            mainRecyclerView.setLayoutManager(verticalLinearLayoutManager);
            final LayoutInflater layoutInflater = LayoutInflater.from(this);
            final OnScrollListener masterOnScrollListener = new OnScrollListener() {
                RecyclerView masterRecyclerView = null;
    
                @Override
                public void onScrollStateChanged(final RecyclerView recyclerView, final int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    switch (newState) {
                        case RecyclerView.SCROLL_STATE_IDLE:
                            if (masterRecyclerView != null) {
                                masterRecyclerView = null;
                                final int firstVisibleItemPosition = verticalLinearLayoutManager.findFirstVisibleItemPosition();
                                final int lastVisibleItemPosition = verticalLinearLayoutManager.findLastVisibleItemPosition();
                                for (int i = firstVisibleItemPosition; i <= lastVisibleItemPosition; ++i) {
                                    RecyclerView horizontalRecyclerView = (RecyclerView) mainRecyclerView.findViewHolderForAdapterPosition(i).itemView;
                                    if (horizontalRecyclerView != recyclerView)
                                        horizontalRecyclerView.addOnScrollListener(this);
                                }
                            }
                            break;
                        case RecyclerView.SCROLL_STATE_SETTLING:
                            //TODO fix out-of-sync scrolling issues, probably here
                        case RecyclerView.SCROLL_STATE_DRAGGING:
                            if (masterRecyclerView == null) {
                                masterRecyclerView = recyclerView;
                                final int firstVisibleItemPosition = verticalLinearLayoutManager.findFirstVisibleItemPosition();
                                final int lastVisibleItemPosition = verticalLinearLayoutManager.findLastVisibleItemPosition();
                                for (int i = firstVisibleItemPosition; i <= lastVisibleItemPosition; ++i) {
                                    RecyclerView horizontalRecyclerView = (RecyclerView) mainRecyclerView.findViewHolderForAdapterPosition(i).itemView;
                                    if (horizontalRecyclerView != recyclerView)
                                        horizontalRecyclerView.removeOnScrollListener(this);
                                }
                            }
                    }
                }
    
                @Override
                public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
                    super.onScrolled(recyclerView, dx, dy);
                    mCurX += dx;
                    final int firstVisibleItemPosition = verticalLinearLayoutManager.findFirstVisibleItemPosition();
                    final int lastVisibleItemPosition = verticalLinearLayoutManager.findLastVisibleItemPosition();
                    for (int i = firstVisibleItemPosition; i <= lastVisibleItemPosition; ++i) {
                        RecyclerView horizontalRecyclerView = (RecyclerView) mainRecyclerView.findViewHolderForAdapterPosition(i).itemView;
                        if (horizontalRecyclerView != recyclerView)
                            horizontalRecyclerView.scrollBy(dx, dy);
                    }
                }
            };
            mainRecyclerView.setAdapter(new Adapter() {
                @Override
                public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
                    RecyclerView horizontalRecyclerView = (RecyclerView) layoutInflater.inflate(R.layout.horizontal_recycler_view, parent, false);
                    horizontalRecyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this, LinearLayoutManager.HORIZONTAL, false));
                    horizontalRecyclerView.addOnScrollListener(masterOnScrollListener);
                    final ViewHolder horizontalViewHolder = new ViewHolder(horizontalRecyclerView) {
                    };
                    horizontalRecyclerView.setAdapter(new Adapter() {
                        @Override
                        public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
                            return new ViewHolder(layoutInflater.inflate(R.layout.single_item, parent, false)) {
                            };
                        }
    
                        @Override
                        public void onBindViewHolder(final ViewHolder holder, final int position) {
                            ((TextView) holder.itemView).setText("horizontalRecyclerView:" + horizontalViewHolder.getAdapterPosition() + "\nitem:" + position);
                        }
    
                        @Override
                        public int getItemCount() {
                            return 100;
                        }
                    });
                    return horizontalViewHolder;
                }
    
                @Override
                public void onBindViewHolder(final ViewHolder holder, final int position) {
                    //TODO check why this isn't called for some cases
                    RecyclerView recyclerView = (RecyclerView) holder.itemView;
                    recyclerView.removeOnScrollListener(masterOnScrollListener);
                    //TODO scroll to correct location here. The below code doesn't seem to work at all
                    recyclerView.scrollToPosition(0);
                    recyclerView.scrollBy(mCurX,0);
                    recyclerView.addOnScrollListener(masterOnScrollListener);
                    recyclerView.getAdapter().notifyDataSetChanged();
                }
    
                @Override
                public int getItemCount() {
                    return 40;
                }
            });
        }
    }
    

    activity_main.xml中

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.v7.widget.RecyclerView
        android:id="@+id/activity_main"
        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"
        tools:context="lb.com.nestedallscrollingrecyclerviewtest.MainActivity"/>
    

    horizo​​ntal_recycler_view.xml

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.v7.widget.RecyclerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="100dp"/>
    

    single_item.xml

    <?xml version="1.0" encoding="utf-8"?>
    <TextView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        android:padding="10dp"/>
    

    问题

    1. 导致其滚动同步的代码出了什么问题?

    2. 我是否有可能无法掌握所有我应该使用的RecyclerView?

    3. 为什么垂直RecyclerView的onBindViewHolder在我预期的时候没有被调用?

    4. 如何在垂直的onBindViewHolder中将水平RecyclerView的x坐标滚动设置为其他?

    5. 我不确定这是否有问题,但如果每个水平RecyclerView中的每个项目的宽度都不同于其他项目,我该怎么办?

1 个答案:

答案 0 :(得分:0)

聚会晚了一点,但只是把它放在这里,以防其他人偶然发现同一问题。 请注意,该解决方案是用 Kotlin 编写的,如果您选择的语言,则可能必须将其转换为 Java

解决方案

需要考虑几个问题。

  1. horizontal回收者视图的同步滚动
  2. 滚动vertical回收站视图时保留偏移量

Adapter的回收者视图中的vertical中添加此代码

var horizontalRecyclerViews = mutableListOf<RecyclerView>()
var absoluteOffset: Int? = null    //Used to solve issue number 2

// matchOffset is used to synchronise the offset of each horizontal recyclerview.
// It is called when a horizontal recyclerview is scrolled with that recyclerview's
// offset. It is also called when the vertical recycler view is scrolled but without
// an offset value (in which case, it uses the absoluteOffset which is set when
// the horizontal scrolling is stopped) 
fun matchOffset(offset: Int? = absoluteOffset) {
    offset?.let { offsetValue ->
        horizontalRecyclerViews.forEach { recyclerView ->
            val currentOffset = recyclerView.computeHorizontalScrollOffset()
            if (offsetValue != currentOffset) {
                recyclerView.scrollBy(offsetValue-currentOffset, 0)
            }
        }
    }
}

override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
    ...
    ...
    ...

    val onTouchListener = object: RecyclerView.OnItemTouchListener {
        override fun onTouchEvent(p0: RecyclerView, p1: MotionEvent) {
        }
        override fun onInterceptTouchEvent(p0: RecyclerView, p1: MotionEvent): Boolean {
            if (p1.action == MotionEvent.ACTION_UP) {
                // This value is used by the vertical recycler view
                absoluteOffset = p0.computeHorizontalScrollOffset()

                // Disable the fling scroll to make life easier
                return true
            }
            return false
        }
        override fun onRequestDisallowInterceptTouchEvent(p0: Boolean) {
        }
    }

    val onScrollListener = object: RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            val value = recyclerView.computeHorizontalScrollOffset()
            matchOffset(value)
        }
    }

    ...
    ...
    ...

    //Clear scroll listeners on each bind to stop them from accumulating
    horizontalRecyclerView.clearOnScrollListeners()

    //Add touch and scroll listeners to horizontalRecyclerView
    horizontalRecyclerView.addOnItemTouchListener(onTouchListener)
    horizontalRecyclerView.addOnScrollListener(onScrollListener)

    //Add each horizontal recyclerView into the mutableList
    horizontalRecyclerViews.add(horizontalRecyclerView)

    ...
    ...
    ...
}

要解决此问题以解决第2期,请在您的vertical回收商视图中添加以下滚动侦听器

val onScrollListener = object: RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)

        //Cast the Adapter to access the matchOffset method
        (recyclerView.adapter as? Adapter)?.matchOffset()
    }
}

verticalRecyclerView.addOnScrollListener(onScrollListener)