NestedScrollView和水平的RecyclerView平滑滚动

23

我有一个嵌套的纵向NestedScrollView,并包含一系列具有水平LayoutManager设置的RecyclerView。这个想法与新的Google Play商店看起来非常相似。虽然我能够使它运行,但它并不流畅。以下是问题:

1)水平RecyclerView项目大多数时候无法拦截触摸事件,即使我在它上面轻按。滚动视图似乎在大多数情况下都优先处理动作。对于我来说很难抓住水平动作。这种用户体验令人沮丧,因为我需要尝试几次才能使其正常工作。如果您检查Play商店,则能够非常好地拦截触摸事件,并且可以正常工作。我注意到,在商店中设置方式是许多水平RecyclerViews内部没有ScrollView。

2)必须手动设置水平RecyclerView的高度,并且没有简单的方法来计算子元素的高度。

以下是我使用的布局:

<android.support.v4.widget.NestedScrollView
    android:id="@+id/scroll"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:background="@color/dark_bgd"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <LinearLayout
            android:id="@+id/main_content_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="gone"
            tools:visibility="gone"
            android:orientation="vertical">                

                <android.support.v7.widget.RecyclerView
                    android:id="@+id/starring_list"
                    android:paddingLeft="@dimen/spacing_major"
                    android:paddingRight="@dimen/spacing_major"
                    android:layout_width="match_parent"
                    android:layout_height="180dp" />

这个用户界面模式非常基础,很可能在许多不同的应用程序中使用。我读了很多SO(Stack Overflow)上的帖子,人们说在列表内部放置另一个列表是一个不好的主意,但这是一个非常常见和现代的UI模式,广泛运用于各个领域。想象一下像Netflix那样的界面,垂直列表内有一系列水平滚动列表。是否有一种流畅的方法来实现这一点呢?

商店的示例图像:

Google Play Store


1
奇怪的是,我遇到了相反的问题:我希望NestedScrollView可以允许垂直滚动,但是当我在它上面开始垂直滚动时,水平的RecyclerView接管了触摸事件,从而使得NestedScrollView无法响应。另外,如果你只需要显示几个项目,你可以使用简单的HorizontalScrollView与LinearLayout。 - android developer
3个回答

23

现在平滑滚动问题已经解决了。它是由Design Support库中的NestedScrollView中的一个错误引起的(目前为23.1.1)。

您可以在此处阅读该问题和简单的修复方法: https://code.google.com/p/android/issues/detail?id=194398

简而言之,在您执行了一次fling之后,nestedscrollview没有在滚动组件上注册完整的on事件,因此它需要一个额外的“ACTION_DOWN”事件来释放父nestedscrollview从拦截(吞掉)后续事件。所以发生的情况是,如果您尝试在fling之后滚动子列表(或viewpager),则第一个触摸会释放父NSV绑定,并且随后的触摸将起作用。这使用户体验非常糟糕。

基本上需要在NSV的ACTION_DOWN事件中添加以下行:

computeScroll();

这是我正在使用的:

public class MyNestedScrollView extends NestedScrollView {
private int slop;
private float mInitialMotionX;
private float mInitialMotionY;

public MyNestedScrollView(Context context) {
    super(context);
    init(context);
}

private void init(Context context) {
    ViewConfiguration config = ViewConfiguration.get(context);
    slop = config.getScaledEdgeSlop();
}

public MyNestedScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

public MyNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}


private float xDistance, yDistance, lastX, lastY;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final float x = ev.getX();
    final float y = ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            xDistance = yDistance = 0f;
            lastX = ev.getX();
            lastY = ev.getY();

            // This is very important line that fixes 
           computeScroll();


            break;
        case MotionEvent.ACTION_MOVE:
            final float curX = ev.getX();
            final float curY = ev.getY();
            xDistance += Math.abs(curX - lastX);
            yDistance += Math.abs(curY - lastY);
            lastX = curX;
            lastY = curY;

            if (xDistance > yDistance) {
                return false;
            }
    }


    return super.onInterceptTouchEvent(ev);
}

}

在xml文件中,使用此类替换您的nestedscrollview,子列表应正确拦截和处理触摸事件。

哎呀,实际上还有很多这样的错误,让我想完全放弃设计支持库,并在成熟后重新审视它。


slopmInitialMotionXmInitialMotionYxyMyNestedScrollView 中没有被使用。 - Rany Albeg Wein
1
这个解决方案在支持库版本26.x.x及以上不再有效!仍被标记为一个错误:https://code.google.com/p/android/issues/detail?id=194398 - Sjd

1

由于 falc0nit3 的解决方案不再适用(目前项目使用的是支持库版本 28.0.0),我找到了另一个解决方案。

问题的背景原因仍然相同,可滚动视图通过在第二次点击时返回true来吃掉下降事件,而不应该这样做,因为自然情况下,对于fling视图的第二次点击会停止滚动,并可以与下一个move事件一起使用以开始相反的滚动。此问题与NestedScrollViewRecyclerView均有重现。

我的解决方案是在本地视图能够在onInterceptTouchEvent中截取之前手动停止滚动。在这种情况下,它不会吃掉ACTION_DOWN事件,因为它已经被停止了。

所以,对于NestedScrollView

class NestedScrollViewFixed(context: Context, attrs: AttributeSet) :
    NestedScrollView(context, attrs) {

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            onTouchEvent(ev)
        }
        return super.onInterceptTouchEvent(ev)
    }
}

对于 RecyclerView
class RecyclerViewFixed(context: Context, attrs: AttributeSet) :
    RecyclerView(context, attrs) {

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        if (e.actionMasked == MotionEvent.ACTION_DOWN) {
            this.stopScroll()
        }
        return super.onInterceptTouchEvent(e)
    }

}

尽管对于RecyclerView的解决方案看起来很容易,但对于NestedScrollView来说却有点复杂。 不幸的是,在仅负责管理滚动的小部件中,没有明确的方法可以手动停止滚动(天哪)。我对abortAnimatedScroll()方法很感兴趣,但它是私有的。虽然可以使用反射绕过它,但对我而言更好的方法是调用调用abortAnimatedScroll()的方法。 请查看ACTION_DOWNonTouchEvent处理方式:
 /*
 * If being flinged and user touches, stop the fling. isFinished
 * will be false if being flinged.
 */
if (!mScroller.isFinished()) {
    Log.i(TAG, "abort animated scroll");
    abortAnimatedScroll();
}

基本上,停止fling是在这个方法中管理的,但是稍晚一点,我们必须调用它来修复错误。
不幸的是,由于这个原因,我们不能只创建OnTouchListener并将其设置在外部,所以只有继承符合要求。

我发现我的解决方案存在一个漏洞,当用户停止滑动并且不进行垂直滚动时,它不会阻塞元素上的点击。仍在寻找改进方法(Google Play 处理得很好)。 - Beloo

0

我已经成功地在一个包含ViewPager的垂直滚动父容器中实现了水平滚动:

<android.support.v4.widget.NestedScrollView

    ...

    <android.support.v4.view.ViewPager
        android:id="@+id/pager_known_for"
        android:layout_width="match_parent"
        android:layout_height="350dp"
        android:minHeight="350dp"
        android:paddingLeft="24dp"
        android:paddingRight="24dp"
        android:clipToPadding="false"/>

公共类 UniversityKnownForPagerAdapter 扩展 PagerAdapter {

public UniversityKnownForPagerAdapter(Context context) {
    mContext = context;
    mInflater = LayoutInflater.from(mContext);
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
    View rootView = mInflater.inflate(R.layout.card_university_demographics, container, false);

    ...

    container.addView(rootView);

    return rootView;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    container.removeView((View)object);
}

@Override
public int getCount() {
    return 4;
}

@Override
public boolean isViewFromObject(View view, Object object) {
    return (view == object);
}

唯一的问题:您必须为视图页提供固定高度


谢谢您的回复。我也尝试了您的解决方案,成功让viewpager工作并滚动了,但是我之前提到的滚动问题在这里也出现了。请查看我的答案以获取修复方法。 - falc0nit3
你试过使用ViewPager而不是RecyclerView吗? - Guillaume Imbert
我试过了,但它仍然不能很好地拦截事件。我认为问题不在于子列表实现(Recyclerview/Viewpager/HorizontalListView等),而是在于NestedScrollView库。(正如我在上面的答案中提到的) - falc0nit3
你的ViewPager实现一直运行得很完美吗?我必须提到,我最终编写的实现在每个ViewPager项上使用了3个ImageView。因此,滑动会一次滑动3张图片。可以看看Netflix Android应用程序的示例。 - falc0nit3

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