提问者:小点点

嵌套滚动视图和水平循环视图平滑滚动


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

1) 水平回收视图项目在大多数情况下都无法拦截触摸事件,即使我直接点击它。滚动视图似乎优先于大多数运动。我很难在水平运动中勾手。这种用户体验令人沮丧,因为我需要在它工作之前尝试几次。如果你检查游戏商店,它能够很好地拦截触摸事件,而且效果很好。我注意到,在游戏商店里,他们设置的方式是在一个垂直的回收站里面放置许多水平的回收站。无滚动视图。

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

以下是我使用的布局:

<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模式是非常基本的,很可能在许多不同的应用程序中使用。我读过很多SO,人们说把一个列表放在一个列表中是一个坏主意,但这是一个非常普遍和现代的UI模式,到处都在使用。想象一下网飞的界面,在一个垂直列表中有一系列水平滚动列表。难道没有一个平稳的方法来完成这个吗?

商店中的示例图像:


共3个答案

匿名用户

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

你可以在这里阅读这个问题和简单的解决方法:https://code.google.com/p/android/issues/detail?id=194398

简而言之,在您执行 fling 之后,嵌套滚动视图没有在滚动器组件上注册一个完整的,因此它需要一个额外的“ACTION_DOWN”事件来释放父嵌套滚动视图,使其免于拦截(吞噬)后续事件。因此,如果您尝试滚动子列表(或查看页),则在投掷后,第一次触摸会释放父 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文件中的嵌套滚动视图,子列表应该拦截并正确处理触摸事件。

呸,实际上有很多这样的bug让我想彻底放弃设计支持库,等到它更成熟时再重新访问它。

匿名用户

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

问题的背景原因仍然是一样的,可滚动视图通过在第二次点击时返回 true 来吃掉向下事件,而不应该,因为自然而然地,第二次点击 fling 视图停止滚动,并可能与下一步事件一起使用以开始相反的滚动该问题与 NestedScrollView回收器视图一样重现。我的解决方案是在本机视图能够在 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)
    }
}

对于< code > recycle view :

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)
    }

}

尽管< code > recycle view 的解决方案看起来很容易阅读,但对于< code>NestedScrollView来说,它有点复杂。不幸的是,在widget中没有明确的方法来手动停止滚动,widget唯一的职责就是管理scroll (omg)。我对< code > abortAnimatedScroll()方法感兴趣,但它是私有的。可以使用反射来绕过它,但对我来说更好的方法是调用方法,该方法调用< code > abortAnimatedScroll()本身。查看< code>ACTION_DOWN的< code>onTouchEvent处理:

 /*
 * 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 是用这种方法管理的,但稍晚一点,我们必须调用它来修复错误

不幸的是,由于这个原因,我们不能只创建< code>OnTouchListener并在外部设置它,所以只有继承符合要求

匿名用户

我成功地在一个垂直滚动的父视图中实现了水平滚动:

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

公共类UniverassiyNotnForPager患儿扩展Pager患儿{

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);
}

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