Android 布局:具有滚动行为的 Viewpager 内的垂直 Recyclerview 内的水平 Recyclerview
Android Layout: Horizontal Recyclerview inside a Vertical Recyclerview inside a Viewpager with Scroll Behaviors
这是我正在尝试构建的应用程序,其中列出了以下所有元素:
一切正常,但是,我希望内部水平回收视图不捕获任何垂直滚动条。所有垂直滚动都必须朝向外部垂直 recyclerview,而不是水平的,这样垂直滚动将允许工具栏根据它的 scrollFlag 退出视图。
当我将手指放在 recyclerview 的 "StrawBerry Plant" 部分并向上滚动时,它会滚出工具栏:
如果我将手指放在水平滚动视图上并向上滚动,它根本不会滚出工具栏。
以下是我目前的 xml 布局代码。
Activityxml布局:
<?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_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fragment_container"
android:clipChildren="false">
<android.support.design.widget.CoordinatorLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/container"
>
<android.support.design.widget.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:minHeight="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/sliding_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
style="@style/CustomTabLayout"
/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
/>
</android.support.design.widget.CoordinatorLayout>
</FrameLayout>
"Fruits" fragment xml布局(这是fragment的代码——fragment在上图中标注):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
我使用了一个名为 VerticallyScrollRecyclerView 的自定义 class,它遵循 google 在视图组中处理触摸事件的示例。它的目的是拦截和消耗所有垂直滚动事件,以便它可以滚动进/出工具栏: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;
}
}
"Favourite"布局是垂直回收视图中的回收视图:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
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_width="match_parent"
android:layout_height="wrap_content"
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_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_below="@+id/header_fav"
android:id="@+id/recyclerview_fav">
</android.support.v7.widget.RecyclerView>
</RelativeLayout>
这个问题困扰了我一段时间,我一直没有想出解决办法。有谁知道如何解决这个问题?
5 分给 Griffindor 以获得正确答案,当然还有关于 SO 的声望分。
测试解决方案:
您只需在您内心的 RecyclerView
上调用 mInnerRecycler.setNestedScrollingEnabled(false);
解释:
RecyclerView
通过实现 NestedScrollingChild
接口支持 API 21
中引入的嵌套滚动。当您在另一个以相同方向滚动的滚动视图中有一个滚动视图并且您只想在聚焦时滚动内部 View
时,这是一个有价值的功能。
在任何情况下,RecyclerView
默认情况下会在初始化时自行调用 RecyclerView.setNestedScrollingEnabled(true);
。现在,回到问题,因为您的两个 RecyclerView
都在具有 AppBarBehavior
的同一个 ViewPager
中,CoordinateLayout
必须决定何时响应哪个滚动条你从内心 RecyclerView
滚动;当您的内部 RecyclerView
的嵌套滚动启用时,它获得滚动焦点并且 CoordinateLayout
将选择响应它在外部 RecyclerView
的滚动上的滚动。问题是,因为你的内部 RecyclerView
s 不垂直滚动,所以没有垂直滚动变化(从 CoordinateLayout
的角度来看),如果没有变化,AppBarLayout
也没有改变。
在你的例子中,因为你的内部 RecyclerView
正在向不同的方向滚动,你可以禁用它,从而导致 CoordinateLayout
忽略它的滚动并响应外部 RecyclerView
的滚动。
注意事项:
xml 属性 android:nestedScrollingEnabled="boolean"
不适用于 RecyclerView
,尝试使用 android:nestedScrollingEnabled="false"
将导致 java.lang.NullPointerException
,因此,至少现在,你必须用代码来完成。
经过测试的解决方案,使用自定义 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);
}
}
我来晚了一点,但这对其他面临同样问题的人肯定有用
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;
}
我建议您在 google 应用
等片段中添加水平回收视图
尝试
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);
}
inner recyclerview 将使用相同的视图池,并且会更流畅
如果还有人在看,试试这个:
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) {
}
})
它检查当前是否水平滚动,然后不允许父级处理事件
我通读了提供的将 setNestedScrollingEnabled 设置为 false 的答案,这对我来说很糟糕,因为它使 recyclerview 无法回收,如果你有一个巨大的列表,你可能会遇到内存崩溃、性能问题等。所以我会给你一个算法,让它在没有任何代码的情况下工作。
在垂直回收视图上有一个侦听器,例如滚动侦听器。每当滚动列表时,您都会收到一个正在滚动的回调。你也应该在空闲时得到回电。
现在垂直recylerview滚动时,水平列表setNestedScrollingEnabled = false
一旦垂直 Recyclerview 处于空闲状态,在同一水平列表上设置 NestedScrollingEnabled = 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,否则禁用它。这意味着当它空闲时,我们现在可以在 coordinatorlayout 中使用 RecyclerViewVertical 而不受 RecyclerViewHorizontal 的任何干扰,因为我们在空闲时禁用了它。
这是我正在尝试构建的应用程序,其中列出了以下所有元素:
一切正常,但是,我希望内部水平回收视图不捕获任何垂直滚动条。所有垂直滚动都必须朝向外部垂直 recyclerview,而不是水平的,这样垂直滚动将允许工具栏根据它的 scrollFlag 退出视图。
当我将手指放在 recyclerview 的 "StrawBerry Plant" 部分并向上滚动时,它会滚出工具栏:
如果我将手指放在水平滚动视图上并向上滚动,它根本不会滚出工具栏。
以下是我目前的 xml 布局代码。
Activityxml布局:
<?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_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fragment_container"
android:clipChildren="false">
<android.support.design.widget.CoordinatorLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/container"
>
<android.support.design.widget.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:minHeight="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/sliding_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
style="@style/CustomTabLayout"
/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
/>
</android.support.design.widget.CoordinatorLayout>
</FrameLayout>
"Fruits" fragment xml布局(这是fragment的代码——fragment在上图中标注):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
我使用了一个名为 VerticallyScrollRecyclerView 的自定义 class,它遵循 google 在视图组中处理触摸事件的示例。它的目的是拦截和消耗所有垂直滚动事件,以便它可以滚动进/出工具栏: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;
}
}
"Favourite"布局是垂直回收视图中的回收视图:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
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_width="match_parent"
android:layout_height="wrap_content"
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_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_below="@+id/header_fav"
android:id="@+id/recyclerview_fav">
</android.support.v7.widget.RecyclerView>
</RelativeLayout>
这个问题困扰了我一段时间,我一直没有想出解决办法。有谁知道如何解决这个问题?
5 分给 Griffindor 以获得正确答案,当然还有关于 SO 的声望分。
测试解决方案:
您只需在您内心的 RecyclerView
上调用 mInnerRecycler.setNestedScrollingEnabled(false);
解释:
RecyclerView
通过实现 NestedScrollingChild
接口支持 API 21
中引入的嵌套滚动。当您在另一个以相同方向滚动的滚动视图中有一个滚动视图并且您只想在聚焦时滚动内部 View
时,这是一个有价值的功能。
在任何情况下,RecyclerView
默认情况下会在初始化时自行调用 RecyclerView.setNestedScrollingEnabled(true);
。现在,回到问题,因为您的两个 RecyclerView
都在具有 AppBarBehavior
的同一个 ViewPager
中,CoordinateLayout
必须决定何时响应哪个滚动条你从内心 RecyclerView
滚动;当您的内部 RecyclerView
的嵌套滚动启用时,它获得滚动焦点并且 CoordinateLayout
将选择响应它在外部 RecyclerView
的滚动上的滚动。问题是,因为你的内部 RecyclerView
s 不垂直滚动,所以没有垂直滚动变化(从 CoordinateLayout
的角度来看),如果没有变化,AppBarLayout
也没有改变。
在你的例子中,因为你的内部 RecyclerView
正在向不同的方向滚动,你可以禁用它,从而导致 CoordinateLayout
忽略它的滚动并响应外部 RecyclerView
的滚动。
注意事项:
xml 属性 android:nestedScrollingEnabled="boolean"
不适用于 RecyclerView
,尝试使用 android:nestedScrollingEnabled="false"
将导致 java.lang.NullPointerException
,因此,至少现在,你必须用代码来完成。
经过测试的解决方案,使用自定义 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);
}
}
我来晚了一点,但这对其他面临同样问题的人肯定有用
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;
}
我建议您在 google 应用
等片段中添加水平回收视图尝试
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);
}
inner recyclerview 将使用相同的视图池,并且会更流畅
如果还有人在看,试试这个:
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) {
}
})
它检查当前是否水平滚动,然后不允许父级处理事件
我通读了提供的将 setNestedScrollingEnabled 设置为 false 的答案,这对我来说很糟糕,因为它使 recyclerview 无法回收,如果你有一个巨大的列表,你可能会遇到内存崩溃、性能问题等。所以我会给你一个算法,让它在没有任何代码的情况下工作。
在垂直回收视图上有一个侦听器,例如滚动侦听器。每当滚动列表时,您都会收到一个正在滚动的回调。你也应该在空闲时得到回电。
现在垂直recylerview滚动时,水平列表setNestedScrollingEnabled = false
一旦垂直 Recyclerview 处于空闲状态,在同一水平列表上设置 NestedScrollingEnabled = 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,否则禁用它。这意味着当它空闲时,我们现在可以在 coordinatorlayout 中使用 RecyclerViewVertical 而不受 RecyclerViewHorizontal 的任何干扰,因为我们在空闲时禁用了它。