HorizontalScrollView в SwipeRefreshLayout

Внедрих новия компонент SwipeRefreshLayout в моето приложение и той работи добре с всякакви вертикални изгледи, като ListView, GridView и ScrollView.

Държи се много лошо с хоризонтални изгледи, като HorizontalScrollView. При превъртане надясно или наляво, изгледът SwipeRefreshLayout кешира докосването, не позволява на HorizontalScrollView да го получи и започва да превърта вертикално, за да извърши опресняването.

Опитах се да разреша този проблем, както преди решавах проблеми с вертикала ScrollView с ViewPager вътре, използвайки requestDisallowInterceptTouchEvent, но не се получи. Също така забелязах, че този метод е заменен в оригиналния клас SwipeRefreshLayout, без да връща супер. Разработчикът на Google остави коментар вместо "//Nope." :)

Тъй като компонентът SwipeRefreshLayout е сравнително нов, не можах да намеря решение, което коригира проблема с хоризонталното превъртане, като същевременно позволява плъзгането за опресняване на изгледа за проследяване и обработка на вертикалното превъртане, така че реших да споделя решението си с надеждата, че ще спести на някого час или два.


person Lior Iluz    schedule 02.06.2014    source източник
comment
Този проблем изглежда е решен в 1.2.0-alpha01   -  person Muntashir Akon    schedule 01.08.2020


Отговори (5)


Реших го, като разширих SwipeRefreshLayout и замених неговия onInterceptTouchEvent. Вътре изчислявам дали разстоянието X, което потребителят е изминал, е по-голямо от наклона при докосване. Ако го направи, това означава, че потребителят плъзга хоризонтално, затова връщам false, което позволява на дъщерния изглед (HorizontalScrollView в този случай), за да получи събитието за докосване.


public class CustomSwipeToRefresh extends SwipeRefreshLayout {

    private int mTouchSlop;
    private float mPrevX;

    public CustomSwipeToRefresh(Context context, AttributeSet attrs) {
        super(context, attrs);

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPrevX = MotionEvent.obtain(event).getX();
                break;

            case MotionEvent.ACTION_MOVE:
                final float eventX = event.getX();
                float xDiff = Math.abs(eventX - mPrevX);

                if (xDiff > mTouchSlop) {
                    return false;
                }
        }

        return super.onInterceptTouchEvent(event);
    }
}
person Lior Iluz    schedule 02.06.2014
comment
Какво прекрасно решение!! Ти ми спаси ден!! Благодаря ти :) - person Wooseong Kim; 12.09.2014
comment
невероятно Благодаря за чудесното решение! Вероятно никога нямаше да разбера това. Просто трябва да пуснете персонализираната джаджа на мястото на оригиналното оформление за опресняване с плъзгане в xml файла и тя работи! Това вероятно трябва да бъде закърпено в източника на Android! - person Meanman; 05.11.2014
comment
Благодаря, човече, спаси ми деня - person Antwan; 17.11.2015
comment
Благодаря много... спасих проекта си :) - person Godekere; 05.12.2015
comment
Благодаря пич. Помогна :) - person King of Masses; 08.12.2015
comment
Имах подобен проблем. Използвам com.baoyz.swipemenulistview.SwipeMenuListView, вложен вътре в SwipeRefreshLayout, и ако плъзна чисто наляво, той разкрива моя бутон за изтриване за елементи от списъка. Но ако плъзгането ми наляво е дори толкова леко наклонено надолу, оформлението за опресняване с плъзгане надолу улавя жеста и не позволява на слушателите на плъзгане наляво да бъдат извикани напълно. С вашата персонализирана реализация всичко работи! - person Shiprack; 27.01.2016
comment
Загубих цял ден, преди да намеря това решение! - person Viky Leaf; 18.03.2016
comment
Защо просто не използвате mPrevX = event.getX(); вместо mPrevX = MotionEvent.obtain(event).getX(); ? MotionEvent.obtain(event) ще създаде събитие за движение, което трябва да бъде рециклирано, освен ако няма да бъде причинено изтичане на памет. - person Viky Leaf; 04.01.2017
comment
Пропуснахте конструктор super(context, attrsSet) - person Denis Makovsky; 11.09.2017
comment
Благодаря ти. Това трябва да бъде коригирано в изходния код на Android SwipeRefreshLayout. - person ngaspama; 20.08.2018
comment
Благодаря, спасихте деня ми! - person Malik Motani; 20.09.2018
comment
@EdwardvanRaak Не копирах отговора си отникъде и той работи перфектно последния път, когато аз и всички потребители по-горе проверихме. Можете да отговорите с вашето собствено перфектно решение. - person Lior Iluz; 07.10.2019
comment
Актуализирана версия на kotlin без изтичане на памет: gist.github.com/EdwardvanRaak/7d431f83801c72355d31598f4d8 f1c66 - person Edward van Raak; 07.10.2019
comment
@EdwardvanRaak, така че основно сте копирали stackoverflow.com/a/24453194/444324 и сте преобразували в Kotlin. - person Lior Iluz; 07.10.2019
comment
Не... Взех вашия код, премахнах допълнителното MotionEvent.obtain() извикване, което не правеше нищо + предизвика предупреждение за възможно изтичане на памет. След това конвертирах в kotlin и добавих допълнителен код, за да открия дали горната част на recyclerview действително е достигната. Тъй като с текущия ви код това причинява грешка, при която първото превъртане до дъното на recyclerview води до случайно задействане на pull to refresh (след повторно превъртане на списъка нагоре). - person Edward van Raak; 09.10.2019

Ако не запомните факта, че вече сте отказали събитието ACTION_MOVE, в крайна сметка ще го вземете по-късно, ако потребителят се върне близо до първоначалния ви mPrevX.

Просто добавете булево значение, за да го запомните.

public class CustomSwipeToRefresh extends SwipeRefreshLayout {

    private int mTouchSlop;
    private float mPrevX;
    // Indicate if we've already declined the move event
    private boolean mDeclined;

    public CustomSwipeToRefresh(Context context, AttributeSet attrs) {
        super(context, attrs);

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPrevX = MotionEvent.obtain(event).getX();
                mDeclined = false; // New action
                break;

            case MotionEvent.ACTION_MOVE:
                final float eventX = event.getX();
                float xDiff = Math.abs(eventX - mPrevX);

                if (mDeclined || xDiff > mTouchSlop) {
                    mDeclined = true; // Memorize
                    return false;
                }
        }

        return super.onInterceptTouchEvent(event);
    }
}
person Twibit    schedule 27.06.2014
comment
Как да създадете проблема, който описвате? Нямах проблеми с отговора на @LiorIluz. - person shkschneider; 10.02.2015
comment
Това се случва, когато започнете да плъзгате изглед хоризонтално и се върнете към началната си точка. Когато се върнете, хващате събитието. Лесен начин за възпроизвеждане е да плъзнете наляво, плъзнете надолу и плъзнете надясно. - person Twibit; 10.02.2015
comment
Наистина. Благодаря за обяснението, възпроизвеждах го. И вашето решение проработи. Добър улов ;) - person shkschneider; 10.02.2015
comment
Защо просто не използвате mPrevX = event.getX(); вместо mPrevX = MotionEvent.obtain(event).getX(); ? MotionEvent.obtain(event) ще създаде събитие за движение, което трябва да бъде рециклирано, освен ако няма да бъде причинено изтичане на памет. - person Viky Leaf; 13.01.2017
comment
Как използвате този клас в този ред mySwipeRefreshLayout = (SwipeRefreshLayout)this.findViewById(R.id.swipeContainer); Ако заменя SwipeRefreshLayout с CustomSwipeToRefresh, той се срива. - person Sam Tyurenkov; 15.10.2019

Решението, предложено от Lior Iluz със замяна на onInterceptTouchEvent() има сериозен проблем. Ако превъртащият се контейнер със съдържание не е напълно превъртен нагоре, тогава може да не е възможно да активирате плъзгане за опресняване със същия жест за превъртане нагоре. Наистина, когато започнете да превъртате вътрешния контейнер и преместите пръста хоризонтално повече от mTouchSlop неволно (което е 8dp по подразбиране), предложеният CustomSwipeToRefresh отхвърля този жест. Така че потребителят трябва да опита още веднъж, за да започне опресняването. Това може да изглежда странно за потребителя.

Извлякох изходния код на оригиналния SwipeRefreshLayout от библиотеката за поддръжка в моя проект и пренаписах onInterceptTouchEvent(). Новото име на класа е TouchSafeSwipeRefreshLayout

private boolean mPendingActionDown;
private float mInitialDownY;
private float mInitialDownX;
private boolean mGestureDeclined;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ensureTarget();
    final int action = ev.getActionMasked();
    int pointerIndex;

    if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
        mReturningToStart = false;
    }

    if (!isEnabled() || mReturningToStart || mRefreshing ) {
        // Fail fast if we're not in a state where a swipe is possible
        if (D) Log.e(LOG_TAG, "Fail because of not enabled OR refreshing OR returning to start. "+motionEventToShortText(ev));
        return false;
    }

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());
            mActivePointerId = ev.getPointerId(0);

            if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) >= 0) {

                if (mNestedScrollInProgress || canChildScrollUp()) {
                    if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. Set pending DOWN=true. "+motionEventToShortText(ev));
                    mPendingActionDown = true;
                } else {
                    mInitialDownX = ev.getX(pointerIndex);
                    mInitialDownY = ev.getY(pointerIndex);
                }
            }
            return false;

        case MotionEvent.ACTION_MOVE:
            if (mActivePointerId == INVALID_POINTER) {
                if (D) Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                return false;
            } else if (mGestureDeclined) {
                if (D) Log.e(LOG_TAG, "Gesture was declined previously because of horizontal swipe");
                return false;
            } else if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) < 0) {
                return false;
            } else if (mNestedScrollInProgress || canChildScrollUp()) {
                if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. "+motionEventToShortText(ev));
                return false;
            } else if (mPendingActionDown) {
                // This is the 1-st Move after content stops scrolling.
                // Consider this Move as Down (a start of new gesture)
                if (D) Log.e(LOG_TAG, "Consider this move as down - setup initial X/Y."+motionEventToShortText(ev));
                mPendingActionDown = false;
                mInitialDownX = ev.getX(pointerIndex);
                mInitialDownY = ev.getY(pointerIndex);
                return false;
            } else if (Math.abs(ev.getX(pointerIndex) - mInitialDownX) > mTouchSlop) {
                mGestureDeclined = true;
                if (D) Log.e(LOG_TAG, "Decline gesture because of horizontal swipe");
                return false;
            }

            final float y = ev.getY(pointerIndex);
            startDragging(y);
            if (!mIsBeingDragged) {
                if (D) Log.d(LOG_TAG, "Waiting for dY to start dragging. "+motionEventToShortText(ev));
            } else {
                if (D) Log.d(LOG_TAG, "Dragging started! "+motionEventToShortText(ev));
            }
            break;

        case MotionEvent.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged = false;
            mGestureDeclined = false;
            mPendingActionDown = false;
            mActivePointerId = INVALID_POINTER;
            break;
    }

    return mIsBeingDragged;
}

Вижте моя примерен проект в Github.

person Stanislav Perchenko    schedule 15.10.2018

Ако използвате Tim Roes EnhancedListView

Вижте този проблеми. Бях много полезен за мен, защото те добавят функция, която открива кога плъзгането започва и кога плъзгането завършва.

Когато плъзгането започне, деактивирам SwipeRefreshLayout и когато плъзгането приключи, активирам swipeRefreshLayout.

person JND    schedule 29.08.2014

Ето какво направих:

class HorizontalScrollViewWithDragListener
    @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : HorizontalScrollView(context, attrs) {
    var draggingState: Boolean = false
        set(value) {
            if (field != value) {
                field = value
                listener?.invoke(value)
            }
        }

    var listener: ((draggingState: Boolean) -> Unit)? = null

    override fun onInterceptTouchEvent(ev: MotionEvent) =
        super.onInterceptTouchEvent(ev)
            .also { draggingState = it }


    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(ev: MotionEvent) =
        super.onTouchEvent(ev)
            .also {
                if(ev.action == MotionEvent.ACTION_UP) {
                    draggingState = false
                }
            }
}

Тогава просто правя това в кода за настройка:

   myScrollView.listener = { refreshView.isEnabled = !it }
person Dale King    schedule 16.07.2020