Макет Android: горизонтальное представление Recyclerview внутри вертикального представления Recyclerview внутри Viewpager с поведением прокрутки

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

Veggies app

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

Когда я кладу палец на часть «StrawBerry Plant» в recyclerview и прокручиваю вверх, панель инструментов прокручивается:

клубничное растение

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

Ниже приведен мой код макета 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

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

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 баллов Гриффиндору за правильный ответ и конечно же очки репутации на СО.




Ответы (7)


Проверенное решение:

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


Пояснение:

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

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

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


Примечание:

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

person Ari    schedule 04.11.2015
comment
Однако, если вы используете привязку данных, вы можете сделать app:nestedScrollingEnabled=@{true} и не делать этого в коде: D - person brAzzi64; 12.03.2016
comment
@ Ари, спасибо за твое объяснение. Я пытаюсь использовать setNestedScrollingEnabled(false) для решения аналогичной проблемы, но у меня минимальный уровень API 15. Я использую последнюю библиотеку поддержки. Это поддерживается только в API 21 и выше? - person user2095470; 14.03.2016
comment
Спасибо за примечание об атрибуте XML, это был мой следующий вопрос. - person darnmason; 13.07.2016
comment
RecyclerView has support for nested scrolling introduced in API 21 through implementing the NestedScrollingChild interface, а как насчет API до версии 21? - person inverted_index; 15.06.2017

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

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

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

person Ahmed na    schedule 13.04.2019

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

 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;
            }
person Shahid Sarwar    schedule 28.05.2016
comment
замена действия на case MotionEvent.ACTION_DOWN: сработала для меня - person tamtom; 20.02.2019

Протестированное решение, используйте пользовательский файл 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);
    }
}
person Anuj    schedule 28.04.2016

пытаться

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 будет использовать тот же пул просмотра, и он будет более плавным

person Abhilash Das    schedule 02.11.2018

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

person Daniel Nyamasyo    schedule 21.09.2018
comment
Лучше просто использовать эпоксидную библиотеку airbnbs. - person Simon; 21.09.2018

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

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

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

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

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

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

давайте рассмотрим еще один более простой способ в kotlin, чтобы сделать это на реальном примере. здесь мы сосредоточимся только на горизонтальном recyclerView:

предположим, что у нас есть RecyclerViewVertical и RecyclerViewHorizontal:

 RecyclerViewHorizontal.apply {
                addOnScrollListener(object : RecyclerView.OnScrollListener() {
                    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                        isNestedScrollingEnabled = RecyclerView.SCROLL_STATE_IDLE != newState
                    }
                })
            }

этот код говорит, что если RecyclerViewHorizontal не простаивает, включите вложенную прокрутку, в противном случае отключите ее. Это означает, что когда он простаивает, мы теперь можем использовать RecyclerViewVertical в макете координатора без какого-либо вмешательства со стороны RecyclerViewHorizontal, поскольку мы отключили его, когда он простаивает.

person j2emanue    schedule 20.06.2021