Android Layout: Горизонтальный Recyclerview внутри Vertical Recyclerview внутри Viewpager с поведением прокрутки

Это приложение, которое я пытаюсь построить со всеми элементами, указанными ниже:

Приложение для овощей

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

Когда я указываю пальцем на часть "StrawBerry Plant" в обзоре переработчика и прокручиваю вверх, она прокручивает панель инструментов:

клубника

Если я положу палец на горизонтальную прокрутку и прокручиваю вверх, она вообще не прокручивается на панели инструментов.

Ниже приведен мой код компоновки xml.

XML-макет Activity:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/fragment_container"
    android:clipChildren="false">

    <android.support.design.widget.CoordinatorLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/container"
        >

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:minHeight="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|enterAlways">

        </android.support.v7.widget.Toolbar>

        <android.support.design.widget.TabLayout
            android:id="@+id/sliding_tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary"
            style="@style/CustomTabLayout"
            />

    </android.support.design.widget.AppBarLayout>
    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        />

    </android.support.design.widget.CoordinatorLayout>

</FrameLayout>

XML-макет фрагмента "Фрукты" (код фрагмента - фрагмент помечен на рисунке выше):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/progressBar"
        android:visibility="gone"
        android:layout_centerInParent="true"
        android:indeterminate="true"/>

<!--    <android.support.v7.widget.RecyclerView-->
    <com.example.simon.customshapes.VerticallyScrollRecyclerView
        android:id="@+id/main_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</RelativeLayout>

Я использовал пользовательский класс VerticalScrollRecyclerView, который следует примеру Google по обработке событий касания в группе просмотра. Его цель - перехватывать и использовать все события вертикальной прокрутки, чтобы прокручивать / выводить панель инструментов: http://developer.android.com/training/gestures/viewgroup.html

Код для VerticalScrollRecyclerView приведен ниже:

public class VerticallyScrollRecyclerView extends RecyclerView {

    public VerticallyScrollRecyclerView(Context context) {
        super(context);
    }

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

    public VerticallyScrollRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    ViewConfiguration vc = ViewConfiguration.get(this.getContext());
    private int mTouchSlop = vc.getScaledTouchSlop();
    private boolean mIsScrolling;
    private float startY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        // Always handle the case of the touch gesture being complete.
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Release the scroll.
            mIsScrolling = false;
            startY = ev.getY();
            return super.onInterceptTouchEvent(ev); // Do not intercept touch event, let the child handle it
        }
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                Log.e("VRecView", "its moving");
                if (mIsScrolling) {
                    // We're currently scrolling, so yes, intercept the
                    // touch event!
                    return true;
                }
                // If the user has dragged her finger horizontally more than
                // the touch slop, start the scroll
                // left as an exercise for the reader
                final float yDiff = calculateDistanceY(ev.getY());
                Log.e("yDiff ", ""+yDiff);
                // Touch slop should be calculated using ViewConfiguration
                // constants.
                if (Math.abs(yDiff) > 5) {
                    // Start scrolling!
                    Log.e("Scroll", "we are scrolling vertically");
                    mIsScrolling = true;
                    return true;
                }
                break;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }


    private float calculateDistanceY(float endY) {
        return startY - endY;
    }

}

"Любимый" макет, который представляет собой представление о переработчике в вертикальном представлении:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="@color/white"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Favourite"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="16dp"
        android:id="@+id/header_fav"/>

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_below="@+id/header_fav"
        android:id="@+id/recyclerview_fav">
    </android.support.v7.widget.RecyclerView>

</RelativeLayout>

Это беспокоило меня некоторое время, и мне не удалось найти решение. кто-нибудь знает, как решить эту проблему?

5 баллов Гриффиндору за правильный ответ и, конечно, репутацию на SO.

7 ответов

Решение

Протестированное решение:

Все, что вам нужно, это позвонить mInnerRecycler.setNestedScrollingEnabled(false); на вашей внутренней RecyclerViews


Пояснение:

RecyclerView Вложена поддержка вложенной прокрутки API 21 путем реализации NestedScrollingChild интерфейс. Это полезная функция, когда у вас есть прокручиваемый вид внутри другого, который прокручивается в том же направлении, и вы хотите прокрутить внутренний View только когда сфокусировано.

В любом случае, RecyclerView по умолчанию звонки RecyclerView.setNestedScrollingEnabled(true); на себя при инициализации. Теперь вернемся к проблеме, так как оба ваших RecyclerViewс в пределах ViewPager это имеет AppBarBehavior, CoordinateLayout должен решить, на какой свиток реагировать, когда вы прокручиваете из своего внутреннего RecyclerView; когда ваш внутренний RecyclerViewвключена вложенная прокрутка, она получает фокус прокрутки и CoordinateLayout выберет ответить на его прокрутку по внешнему RecyclerViewпрокручиваем. Дело в том, что, так как ваш внутренний RecyclerViews не прокручивать по вертикали, вертикальная прокрутка не меняется (из CoordinateLayoutс точки зрения), и если нет никаких изменений, то AppBarLayout тоже не меняется.

В вашем случае, потому что ваш внутренний RecyclerViewS прокручивается в другом направлении, вы можете отключить его, вызывая CoordinateLayout игнорировать его прокрутку и реагировать на внешние RecyclerViewпрокручиваем.


Обратите внимание:

Атрибут xml android:nestedScrollingEnabled="boolean" не предназначен для использования с RecyclerViewи попытка использовать android:nestedScrollingEnabled="false" приведет к java.lang.NullPointerException так что, по крайней мере, сейчас вам придется делать это в коде.

Если кто-то еще ищет, попробуйте это:

private val Y_BUFFER = 10
private var preX = 0f
private var preY = 0f

mView.rv.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
        override fun onTouchEvent(p0: RecyclerView, p1: MotionEvent) {

            }

        override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
                when (e.action) {
                    MotionEvent.ACTION_DOWN -> rv.parent.requestDisallowInterceptTouchEvent(true)
                    MotionEvent.ACTION_MOVE -> {
                        if (Math.abs(e.x - preX) > Math.abs(e.y - preY)) {
                            rv.parent.requestDisallowInterceptTouchEvent(true)
                        } else if (Math.abs(e.y - preY) > Y_BUFFER) {
                            rv.parent.requestDisallowInterceptTouchEvent(false)
                        }

                    }
                }
                preX = e.x
                preY = e.y
                return false
            }

            override fun onRequestDisallowInterceptTouchEvent(p0: Boolean) {
            }
        })

это проверяет, прокручивается ли в настоящее время горизонтальное, тогда не позволяет родительскому событию handel

Я немного опоздал, но это определенно будет работать для тех, кто сталкивается с той же проблемой

 mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
            @Override
            public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                int action = e.getAction();
               // Toast.makeText(getActivity(),"HERE",Toast.LENGTH_SHORT).show();
                switch (action) {
                    case MotionEvent.ACTION_POINTER_UP:
                        rv.getParent().requestDisallowInterceptTouchEvent(true);

                        break;
                }
                return false;
            }

Пытаться

public OuterRecyclerViewAdapter(List<Item> items) {
    //Constructor stuff
    viewPool = new RecyclerView.RecycledViewPool();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    //Create viewHolder etc
    holder.innerRecyclerView.setRecycledViewPool(viewPool);

}

Внутреннее recylerview будет использовать тот же viewpool, и это будет более плавным

Проверенное решение, используйте пользовательский NestedScrollView(),

Код:

public class CustomNestedScrollView extends NestedScrollView {
    public CustomNestedScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
         // Explicitly call computeScroll() to make the Scroller compute itself
            computeScroll();
        }
        return super.onInterceptTouchEvent(ev);
    }
}

Я бы посоветовал вам добавить горизонтальный обзор реселлера внутри фрагментов, таких как приложение Google

Я прочитал предложенные ответы об использовании setNestedScrollingEnabled в false, и это было ужасно для меня, так как из-за этого recyclerview не перерабатывается, и вы можете получить сбои в памяти, проблемы с производительностью, возможно, и т. д., если у вас огромный список. поэтому я дам вам алгоритм, чтобы он работал без кода.

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

теперь, когда вертикальный recylerview прокручивается, setNestedScrollingEnabled = false в горизонтальном списке

как только вертикальный recyclerview бездействует, setNestedScrollingEnabled = true в том же горизонтальном списке.

также изначально установите для горизонтального recyclerview значение setNestedScrollingEnabled = false в xml

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