NestedScrollView 和 Horizo​​ntal RecyclerView 平滑滚动

Posted

技术标签:

【中文标题】NestedScrollView 和 Horizo​​ntal RecyclerView 平滑滚动【英文标题】:NestedScrollView and Horizontal RecyclerView Smooth Scrolling 【发布时间】:2016-03-19 10:18:57 【问题描述】:

我有一个垂直的nestedscrollview,其中包含一堆带有水平布局管理器设置的recyclerview。这个想法与新的 google play 商店的外观非常相似。我能够使其正常工作,但它一点也不流畅。以下是问题:

1) 即使我点击它,水平 recyclerview 项目大多数时候都无法拦截触摸事件。滚动视图似乎优先于大多数动作。我很难抓住水平运动。这个 UX 令人沮丧,因为我需要尝试几次才能正常工作。如果你查看 play store,它能够很好地拦截触摸事件,而且效果很好。我注意到在 Play Store 中,他们设置的方式是在一个垂直的 recyclerview 中有许多水平的 recyclerviews。没有滚动视图。

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

这是我正在使用的布局:

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

    <LinearLayout
        android:layout_
        android:layout_
        android:orientation="vertical">

        <LinearLayout
            android:id="@+id/main_content_container"
            android:layout_
            android:layout_
            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_
                    android:layout_ />

这种 UI 模式非常基本,很可能在许多不同的应用程序中使用。我读过很多 SO,其中 ppl 说将列表放在列表中是一个坏主意,但它是一种非常常见且现代的 UI 模式,到处都在使用。想想 netflix 之类的界面,里面有一系列水平滚动列表一个垂直列表。难道没有一种顺利的方法可以做到这一点吗?

来自商店的示例图片:

【问题讨论】:

奇怪的是我有相反的问题:我希望 NestedScrollView 允许垂直滚动,但水平 RecyclerView 需要触摸事件,所以当我开始垂直滚动时,它不会允许它。另外,顺便说一句,如果您只有几个项目要显示,您可以使用带有 LinearLayout 的简单 Horizo​​ntalScrollView。 【参考方案1】:

因此,平滑滚动问题现已修复。这是由设计支持库(当前为 23.1.1)中的 NestedScrollView 中的错误引起的。

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

简而言之,在您执行投掷后,nestedscrollview 没有在滚动组件上注册完成,因此它需要一个额外的 'ACTION_DOWN' 事件来释放父级 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,子列表应正确拦截和处理触摸事件。

唷,实际上有很多类似的错误让我想完全放弃设计支持库并在它更成熟时重新访问它。

【讨论】:

slopmInitialMotionXmInitialMotionYxy 未在 MyNestedScrollView 中使用。 此解决方案在支持 lib 26.x.x 及更高版本中不再有效!仍然在code.google.com/p/android/issues/detail?id=194398中标记为错误【参考方案2】:

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

问题的背景原因仍然相同,可滚动视图通过在第二次点击时返回 true 来吞噬向下事件,这是不应该的,因为自然地第二次点击 fling 视图会停止滚动并且可以使用下一个 move 事件开始反向滚动 该问题与NestedScrollView 一样与RecyclerView 一样重现。 我的解决方案是在原生视图能够在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() 本身。 看onTouchEvent处理ACTION_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 并将其设置在外部,因此只有继承符合要求

【讨论】:

我在我的解决方案中发现了一个缺陷,当用户停止投掷并且不进行垂直滚动时,它不会阻止对元素的点击。仍在寻求改进(google play 处理得很好)【参考方案3】:

我已经成功地使用 ViewPager 在垂直滚动的父级中进行水平滚动:

<android.support.v4.widget.NestedScrollView

    ...

    <android.support.v4.view.ViewPager
        android:id="@+id/pager_known_for"
        android:layout_
        android:layout_
        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 工作和滚动,但我上面提到的相同滚动问题也发生在这里。请参阅我的解决方法。 您是否尝试过使用 viewpager 而不是 recyclerview? 我做到了,它仍然没有很好地拦截事件。我认为问题不在于子列表实现(Recyclerview/Viewpager/Horizo​​ntalListView 等),而在于 NestedScrollView 库。 (正如我在上面的回答中提到的) 您的 viewpager 实施是否完美无缺?我不得不提到我最终编写的实现在每个 viewpager 项目上使用了 3 个 Imageview。因此,一次滑动会一次滑动 3 张图像。以 netflix android 应用为例。

以上是关于NestedScrollView 和 Horizo​​ntal RecyclerView 平滑滚动的主要内容,如果未能解决你的问题,请参考以下文章

LinearLayout 和 NestedScrollView 问题

NestedScrollView 平滑滚动到顶部和内部的视图

Flutter tabsView 和 NestedScrollView 滚动问题

有关NestedScrollView的问题

当在nestedscrollview的recyclerview中将项目拖出可见空间时-nestedscrollView不滚动

使用多个 RecyclerView 和其他视图保存/恢复 NestedScrollView 的状态