NestedScrollView和Horizo​​ntal RecyclerView平滑滚动

时间:2015-12-14 01:30:07

标签: android android-recyclerview horizontal-scrolling nestedscrollview

我有一个垂直的nestedscrollview,其中包含一堆带有水平布局管理器设置的recyclerview。这个想法非常类似于新的谷歌游戏商店的外观。我能够使它发挥作用,但它根本不光滑。以下是问题:

1)水平回收物品项目大多数时间都无法拦截触摸事件,即使我点击它也是如此。滚动视图似乎优先于大多数动作。我很难在水平运动上找到钩子。这个用户体验令人沮丧,因为我需要在它工作之前尝试几次。如果你检查游戏商店,它能够很好地拦截触摸事件,它只是运作良好。我注意到在游戏商店中他们设置的方式是在一个垂直的回收者视图中的许多水平回收视图。没有滚动视图。

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

以下是我使用的布局:

<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" />

此UI模式非常基本,很可能在许多不同的应用程序中使用。我已经阅读了许多人们说ppl说列表中的列表是个坏主意,但它是一个非常普遍和现代的UI模式,用于遍布整个地方。想想netflix就像在垂直列表中与一系列水平滚动列表接口。难道没有一个顺利的方法来实现这个目标吗?

商店的示例图片:

Google Play Store

3 个答案:

答案 0 :(得分:19)

所以现在修复了平滑滚动问题。它是由设计支持库(目前为23.1.1)中的NestedScrollView中的错误引起的。

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

简而言之,在您执行了一次投掷之后,嵌套的scrollview没有在滚动条组件上注册完整版,因此它需要一个额外的&#39; ACTION_DOWN&#39;事件,以释放父nestedscrollview拦截(吃掉)后续事件。所以发生的事情是如果你尝试滚动你的子列表(或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,子列表应该正确拦截和处理触摸事件。

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

答案 1 :(得分: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);
}

仅发出:您必须为视图寻呼机提供固定高度

答案 2 :(得分:0)

由于 falc0nit3 解决方案不再起作用(当前使用28.0.0版本的支持库的项目),我发现了另一个解决方案。

问题的背景原因仍然是相同的,可滚动视图在第二次点击时返回true,从而在发生向下事件时吃了它,这是不应该的,因为第二个点击自然会停止滚动并且可能会滚动与下一个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有点复杂。 不幸的是,没有明确的方法来停止在小部件中手动滚动,唯一的职责是管理滚动(omg)。我对abortAnimatedScroll()方法很感兴趣,但它是私有的。可以使用反射来解决它,但对我来说更好的是调用方法,该方法本身会调用abortAnimatedScroll()。 查看onTouchEventACTION_DOWN的处理方式:

 /*
 * 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();
}

基本上可以通过这种方法来管理逃跑,但是要稍晚一些,我们需要调用它来修复错误

不幸的是,由于这个原因,我们不能只创建OnTouchListener并将其设置在外部,所以只有继承才能满足要求