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】:经过测试的解决方案:
您只需要在您的内部RecyclerView
s 上调用mInnerRecycler.setNestedScrollingEnabled(false);
说明:
RecyclerView
通过实现NestedScrollingChild
接口支持API 21
中引入的嵌套滚动。当您在另一个视图内部有一个沿相同方向滚动的滚动视图并且您希望仅在聚焦时滚动内部 View
时,这是一个有价值的功能。
无论如何,RecyclerView
在初始化时默认调用RecyclerView.setNestedScrollingEnabled(true);
。现在,回到问题,因为您的两个RecyclerView
s 都在具有AppBarBehavior
的同一个ViewPager
内,当您从内部RecyclerView
滚动时,CoordinateLayout
必须决定要响应哪个滚动;当您的内部RecyclerView
的嵌套滚动启用时,它会获得滚动焦点,并且CoordinateLayout
将选择响应其对外部RecyclerView
滚动的滚动。问题是,由于您的内部 RecyclerView
s 不会垂直滚动,因此没有垂直滚动变化(从 CoordinateLayout
的角度来看),如果没有变化,AppBarLayout
不会也不要改变。
在您的情况下,由于您的内部RecyclerView
s 正在向不同的方向滚动,您可以禁用它,从而导致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 和 RecyclerViewHorizontal:
RecyclerViewHorizontal.apply
addOnScrollListener(object : RecyclerView.OnScrollListener()
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int)
isNestedScrollingEnabled = RecyclerView.SCROLL_STATE_IDLE != newState
)
这段代码的意思是,如果 RecyclerViewHorizontal 不是空闲的,则启用nestedScrolling,否则禁用它。这意味着当它空闲时,我们现在可以在协调器布局中使用 RecyclerViewVertical,而不会受到 RecyclerViewHorizontal 的任何干扰,因为我们在它空闲时禁用了它。
【讨论】:
以上是关于Android 布局:具有滚动行为的 Viewpager 内的垂直 Recyclerview 内的水平 Recyclerview的主要内容,如果未能解决你的问题,请参考以下文章
Android:向上滚动时显示工具栏(向上拖动),向下滚动时隐藏(向下拖动)
Android:一旦我以编程方式禁用滚动视图行为,我将无法再次启用它