Android 布局:具有滚动行为的 Viewpager 内的垂直 Recyclerview 内的水平 Recyclerview

Posted

技术标签:

【中文标题】Android 布局:具有滚动行为的 Viewpager 内的垂直 Recyclerview 内的水平 Recyclerview【英文标题】:Android Layout: Horizontal Recyclerview inside a Vertical Recyclerview inside a Viewpager with Scroll Behaviors 【发布时间】:2016-02-01 01:45:09 【问题描述】:

这是我正在尝试构建的应用程序,其中包含以下映射的所有元素:

一切正常,但是,我希望内部水平回收视图不捕获任何垂直滚动。所有垂直滚动都必须朝向外部垂直回收视图,而不是水平滚动视图,以便垂直滚动允许工具栏根据它的 scrollFlag 退出视图。

当我将手指放在 recyclerview 的“StrawBerry Plant”部分并向上滚动时,它会滚动出工具栏:

如果我将手指放在水平滚动视图上并向上滚动,它根本不会滚动出工具栏。

以下是我目前为止的xml布局代码。

Activity xml 布局:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_
    android:layout_
    android:id="@+id/fragment_container"
    android:clipChildren="false">

    <android.support.design.widget.CoordinatorLayout
        android:orientation="vertical"
        android:layout_
        android:layout_
        android:id="@+id/container"
        >

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_
        android:layout_>

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:minHeight="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            android:layout_
            android:layout_
            app:layout_scrollFlags="scroll|enterAlways">

        </android.support.v7.widget.Toolbar>

        <android.support.design.widget.TabLayout
            android:id="@+id/sliding_tabs"
            android:layout_
            android:layout_
            android:background="?attr/colorPrimary"
            style="@style/CustomTabLayout"
            />

    </android.support.design.widget.AppBarLayout>
    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_
        android:layout_
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        />

    </android.support.design.widget.CoordinatorLayout>

</FrameLayout>

“Fruits”片段 xml布局(这是片段的代码-片段在上图中标记):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_>

    <ProgressBar
        android:layout_
        android:layout_
        android:id="@+id/progressBar"
        android:visibility="gone"
        android:layout_centerInParent="true"
        android:indeterminate="true"/>

<!--    <android.support.v7.widget.RecyclerView-->
    <com.example.simon.customshapes.VerticallyScrollRecyclerView
        android:id="@+id/main_recyclerview"
        android:layout_
        android:layout_
        />

</RelativeLayout>

我使用了一个名为 VerticallyScrollRecyclerView 的自定义类,它遵循谷歌在视图组中处理触摸事件的示例。它的目的是拦截和消耗所有垂直滚动事件,以便它可以滚动进/出工具栏:http://developer.android.com/training/gestures/viewgroup.html

VerticallyScrollRecyclerView的代码如下:

public class VerticallyScrollRecyclerView extends RecyclerView 

    public VerticallyScrollRecyclerView(Context context) 
        super(context);
    

    public VerticallyScrollRecyclerView(Context context, AttributeSet attrs) 
        super(context, attrs);
    

    public VerticallyScrollRecyclerView(Context context, AttributeSet attrs, int defStyle) 
        super(context, attrs, defStyle);
    

    ViewConfiguration vc = ViewConfiguration.get(this.getContext());
    private int mTouchSlop = vc.getScaledTouchSlop();
    private boolean mIsScrolling;
    private float startY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) 
        final int action = MotionEventCompat.getActionMasked(ev);
        // Always handle the case of the touch gesture being complete.
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) 
            // Release the scroll.
            mIsScrolling = false;
            startY = ev.getY();
            return super.onInterceptTouchEvent(ev); // Do not intercept touch event, let the child handle it
        
        switch (action) 
            case MotionEvent.ACTION_MOVE: 
                Log.e("VRecView", "its moving");
                if (mIsScrolling) 
                    // We're currently scrolling, so yes, intercept the
                    // touch event!
                    return true;
                
                // If the user has dragged her finger horizontally more than
                // the touch slop, start the scroll
                // left as an exercise for the reader
                final float yDiff = calculateDistanceY(ev.getY());
                Log.e("yDiff ", ""+yDiff);
                // Touch slop should be calculated using ViewConfiguration
                // constants.
                if (Math.abs(yDiff) > 5) 
                    // Start scrolling!
                    Log.e("Scroll", "we are scrolling vertically");
                    mIsScrolling = true;
                    return true;
                
                break;
            
        
        return super.onInterceptTouchEvent(ev);
    


    private float calculateDistanceY(float endY) 
        return startY - endY;
    


“收藏夹”布局,它是垂直回收视图中的回收视图:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout
    android:layout_
    android:layout_
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="@color/white"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:layout_
        android:layout_
        android:text="Favourite"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="16dp"
        android:id="@+id/header_fav"/>

    <android.support.v7.widget.RecyclerView
        android:layout_
        android:layout_
        android:orientation="horizontal"
        android:layout_below="@+id/header_fav"
        android:id="@+id/recyclerview_fav">
    </android.support.v7.widget.RecyclerView>

</RelativeLayout>

这已经困扰我一段时间了,我还没有想出一个解决方案。有谁知道如何解决这个问题?

5 分给 Griffindor 的正确答案,当然还有 SO 的声望分。

【问题讨论】:

【参考方案1】:

经过测试的解决方案:

您只需要在您的内部RecyclerViews 上调用mInnerRecycler.setNestedScrollingEnabled(false);

说明

RecyclerView 通过实现NestedScrollingChild 接口支持API 21 中引入的嵌套滚动。当您在另一个视图内部有一个沿相同方向滚动的滚动视图并且您希望仅在聚焦时滚动内部 View 时,这是一个有价值的功能。

无论如何,RecyclerView 在初始化时默认调用RecyclerView.setNestedScrollingEnabled(true);。现在,回到问题,因为您的两个RecyclerViews 都在具有AppBarBehavior 的同一个ViewPager 内,当您从内部RecyclerView 滚动时,CoordinateLayout 必须决定要响应哪个滚动;当您的内部RecyclerView 的嵌套滚动启用时,它会获得滚动焦点,并且CoordinateLayout 将选择响应其对外部RecyclerView 滚动的滚动。问题是,由于您的内部 RecyclerViews 不会垂直滚动,因此没有垂直滚动变化(从 CoordinateLayout 的角度来看),如果没有变化,AppBarLayout 不会也不要改变。

在您的情况下,由于您的内部RecyclerViews 正在向不同的方向滚动,您可以禁用它,从而导致CoordinateLayout 忽略其滚动并响应外部RecyclerView 的滚动。

通知

xml 属性android:nestedScrollingEnabled="boolean" 不适合与RecyclerView 一起使用,尝试使用android:nestedScrollingEnabled="false" 将导致java.lang.NullPointerException 因此,至少现在,您必须在代码中执行此操作.

【讨论】:

如果你使用数据绑定,你可以做 app:nestedScrollingEnabled="@true",并且不用在代码中做这件事:D @Ari 感谢您的解释。我正在尝试使用 setNestedScrollingEnabled(false) 来解决类似的问题,但我的最低 API 级别为 15。我正在使用最新的支持库。这仅在 API 21 及更高版本中支持吗? 感谢关于 XML 属性的注释,这正是我的下一个问题。 RecyclerView has support for nested scrolling introduced in API 21 through implementing the NestedScrollingChild interface,21 之前的 API 呢?【参考方案2】:

如果有人还在寻找,试试这个:

private val Y_BUFFER = 10
private var preX = 0f
private var preY = 0f

mView.rv.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener 
        override fun onTouchEvent(p0: RecyclerView, p1: MotionEvent) 

            

        override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean 
                when (e.action) 
                    MotionEvent.ACTION_DOWN -> rv.parent.requestDisallowInterceptTouchEvent(true)
                    MotionEvent.ACTION_MOVE -> 
                        if (Math.abs(e.x - preX) > Math.abs(e.y - preY)) 
                            rv.parent.requestDisallowInterceptTouchEvent(true)
                         else if (Math.abs(e.y - preY) > Y_BUFFER) 
                            rv.parent.requestDisallowInterceptTouchEvent(false)
                        

                    
                
                preX = e.x
                preY = e.y
                return false
            

            override fun onRequestDisallowInterceptTouchEvent(p0: Boolean) 
            
        )

它检查当前是否水平滚动然后不允许父处理事件

【讨论】:

【参考方案3】:

我有点晚了,但这肯定适用于面临同样问题的其他人

 mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() 
            @Override
            public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) 
                int action = e.getAction();
               // Toast.makeText(getActivity(),"HERE",Toast.LENGTH_SHORT).show();
                switch (action) 
                    case MotionEvent.ACTION_POINTER_UP:
                        rv.getParent().requestDisallowInterceptTouchEvent(true);

                        break;
                
                return false;
            

【讨论】:

case MotionEvent.ACTION_DOWN: 替换操作对我有用【参考方案4】:

经过测试的解决方案,使用自定义NestedScrollView()

代码:

public class CustomNestedScrollView extends NestedScrollView 
    public CustomNestedScrollView(Context context, AttributeSet attrs) 
        super(context, attrs);
    

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) 
        if (ev.getAction() == MotionEvent.ACTION_DOWN) 
         // Explicitly call computeScroll() to make the Scroller compute itself
            computeScroll();
        
        return super.onInterceptTouchEvent(ev);
    

【讨论】:

【参考方案5】:

试试

public OuterRecyclerViewAdapter(List<Item> items) 
    //Constructor stuff
    viewPool = new RecyclerView.RecycledViewPool();

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) 
    //Create viewHolder etc
    holder.innerRecyclerView.setRecycledViewPool(viewPool);


内部 recylerview 将使用相同的视图池,它会更流畅

【讨论】:

【参考方案6】:

我建议你在像谷歌应用这样的片段中添加水平回收视图

【讨论】:

最好只使用 airbnbs 环氧树脂库。【参考方案7】:

我阅读了使用 setNestedScrollingEnabled 为 false 提供的答案,这对我来说很糟糕,因为它使 recyclerview 无法回收,如果你有一个巨大的列表,你可能会在内存中崩溃,性能问题可能等等。所以我会给你一个算法,让它在没有任何代码的情况下工作。

在垂直回收器视图上有一个监听器,例如滚动监听器。每当滚动列表时,您都会收到一个回调它正在滚动。您也应该在空闲时收到回电。

现在当垂直 recylerview 被滚动时,setNestedScrollingEnabled = false 在水平列表中

一旦垂直回收视图空闲 setNestedScrollingEnabled = true 在同一个水平列表上。

还在xml中初始设置水平recyclerview为setNestedScrollingEnabled = false

当 coordinatorLayout 与不同方向的两个 recyclerview 混淆时,这也可以很好地与 appbarlayout 一起使用,但这是另一个问题。

让我们看一下 kotlin 中另一种更简单的方法,通过一个真实的例子来完成这项工作。这里我们只关注水平的recyclerView:

假设我们有 RecyclerViewVertical 和 RecyclerViewHorizo​​ntal:

 RecyclerViewHorizontal.apply 
                addOnScrollListener(object : RecyclerView.OnScrollListener() 
                    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) 
                        isNestedScrollingEnabled = RecyclerView.SCROLL_STATE_IDLE != newState
                    
                )
            

这段代码的意思是,如果 RecyclerViewHorizo​​ntal 不是空闲的,则启用nestedScrolling,否则禁用它。这意味着当它空闲时,我们现在可以在协调器布局中使用 RecyclerViewVertical,而不会受到 RecyclerViewHorizo​​ntal 的任何干扰,因为我们在它空闲时禁用了它。

【讨论】:

以上是关于Android 布局:具有滚动行为的 Viewpager 内的垂直 Recyclerview 内的水平 Recyclerview的主要内容,如果未能解决你的问题,请参考以下文章

Android:向上滚动时显示工具栏(向上拖动),向下滚动时隐藏(向下拖动)

UITableView 滚动时奇怪的布局行为变化

Android:一旦我以编程方式禁用滚动视图行为,我将无法再次启用它

具有可滚动布局和过滤器面板的视图排列

使用约束布局或android中的任何其他方式进行垂直滚动的粘性视图

如何控制 Android 视图中的轨迹球行为?