NestedScrollView и горизонтальный рецикливатор с гладкой прокруткой

У меня есть одно вертикальное вложенное scrollview, которое содержит кучу recyclerview с горизонтальной настройкой layoutmanager. Идея довольно похожа на то, как выглядит новый игровой магазин google. Я могу сделать его функциональным, но он не является гладким. Вот проблемы:

1) Элемент горизонтального повторного просмотра не может перехватить событие касания в большинстве случаев, даже если я коснусь его. Кажется, что представление прокрутки имеет приоритет для большинства движений. Мне тяжело зацепиться за горизонтальное движение. Этот UX разочаровывает, поскольку мне нужно попробовать несколько раз, прежде чем он сработает. Если вы проверите магазин воспроизведения, он сможет очень хорошо перехватить событие касания, и он работает хорошо. Я заметил в игровом магазине то, как они его устанавливали, - это много горизонтальных переработок внутри одного вертикального рециклинга. Нет прокрутки.

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

Этот шаблон пользовательского интерфейса является очень простым и, скорее всего, используется во многих разных приложениях. Я прочел много SO, где ppl заявляет, что это плохая идея поместить список в список, но это очень распространенный и современный шаблон пользовательского интерфейса, используемый повсеместно. Принимая во внимание netflix-интерфейс с серией горизонтальных списков прокрутки внутри вертикальный список. Разве нет гладкого пути для этого?

Пример изображения из магазина:

Google Play Store

Ответ 1

Итак, проблема с гладкой прокруткой исправлена. Это было вызвано ошибкой в ​​NestedScrollView в Библиотеке поддержки дизайна (в настоящее время 23.1.1).

Вы можете прочитать о проблеме и просто исправить здесь: https://code.google.com/p/android/issues/detail?id=194398

Вкратце, после того, как вы выполнили сброс, вложенное scrollview не зарегистрировало полную информацию о компоненте скроллера, и поэтому ему понадобилось дополнительное событие ACTION_DOWN для освобождения родительского вложенного scrollview от перехвата (съедания) последующих событий. Итак, что случилось, если вы пробовали прокручивать свой дочерний список (или просмотрщик), после переброски первый штрих освобождает родительское связывание NSV, и последующие штрихи будут работать. Это делало UX очень плохим.

По существу необходимо добавить эту строку в событие ACTION_DOWN NSV:

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

}

Используйте этот класс вместо вашего nestedscrollview в XML файле, а дочерние списки должны правильно перехватывать и обрабатывать события касания.

Фу, на самом деле существует довольно много таких ошибок, из-за которых я хочу полностью отказаться от библиотеки поддержки дизайна и пересмотреть ее, когда она станет более зрелой.

Ответ 2

Я добился горизонтальной прокрутки в вертикальном прокручиваемом родителе с помощью 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);
}

Только проблема: вы должны указать фиксированную высоту для пейджера представления

Ответ 3

Поскольку решение @falc0nit3 больше не работает (в настоящее время проект использует версию библиотеки поддержки 28.0.0), я нашел другое.

Фоновая причина проблемы остается той же: прокручиваемое представление съедает событие "вниз", возвращая значение " true при втором касании, где это не должно происходить, поскольку, естественно, второе касание в режиме броска перестает прокручиваться и может использоваться со следующим событием move в начать противоположную прокрутку Проблема воспроизводится как с NestedScrollView так и с RecyclerView. Мое решение состоит в том, чтобы прекратить прокрутку вручную, прежде чем собственный вид сможет перехватить его в onInterceptTouchEvent. В этом случае оно не будет ACTION_DOWN событие ACTION_DOWN, потому что оно уже было остановлено.

Итак, для NestedScrollView:

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

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

Для RecyclerView:

class WorkaroundRecyclerView(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();
}

По сути, в этом методе происходит остановка fling, но чуть позже, чем мы должны вызвать, чтобы исправить ошибку.