HorizontalScrollView в ScrollView Touch Handling

226

У меня есть ScrollView, который окружает весь мой макет, так что весь экран можно прокручивать. Первый элемент, который у меня есть в этом ScrollView - это блок HorizontalScrollView, в котором есть функции, которые можно прокручивать по горизонтали. Я добавил ontouchlistener в представление horizontalscroll, чтобы обрабатывать сенсорные события и заставлять представление «привязываться» к ближайшему изображению в событии ACTION_UP.

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

Все это прекрасно работает, за исключением одной проблемы: мне нужно провести пальцем слева направо почти идеально по горизонтали, чтобы ACTION_UP когда-либо регистрировался. Если я проведу по крайней мере вертикально (как мне кажется, многие люди склонны делать это на своих телефонах при перелистывании из стороны в сторону), я получу ACTION_CANCEL вместо ACTION_UP. Моя теория заключается в том, что это происходит потому, что представление горизонтального прокрутки находится внутри просмотра прокрутки, а представление прокрутки захватывает вертикальное касание, чтобы обеспечить вертикальную прокрутку.

Как я могу отключить сенсорные события для просмотра с прокруткой прямо из моего горизонтального просмотра с прокруткой, но при этом разрешить нормальную вертикальную прокрутку в других местах просмотра с прокруткой?

Вот пример моего кода:

   public class HomeFeatureLayout extends HorizontalScrollView {
    private ArrayList<ListItem> items = null;
    private GestureDetector gestureDetector;
    View.OnTouchListener gestureListener;
    private static final int SWIPE_MIN_DISTANCE = 5;
    private static final int SWIPE_THRESHOLD_VELOCITY = 300;
    private int activeFeature = 0;

    public HomeFeatureLayout(Context context, ArrayList<ListItem> items){
        super(context);
        setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
        setFadingEdgeLength(0);
        this.setHorizontalScrollBarEnabled(false);
        this.setVerticalScrollBarEnabled(false);
        LinearLayout internalWrapper = new LinearLayout(context);
        internalWrapper.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
        internalWrapper.setOrientation(LinearLayout.HORIZONTAL);
        addView(internalWrapper);
        this.items = items;
        for(int i = 0; i< items.size();i++){
            LinearLayout featureLayout = (LinearLayout) View.inflate(this.getContext(),R.layout.homefeature,null);
            TextView header = (TextView) featureLayout.findViewById(R.id.featureheader);
            ImageView image = (ImageView) featureLayout.findViewById(R.id.featureimage);
            TextView title = (TextView) featureLayout.findViewById(R.id.featuretitle);
            title.setTag(items.get(i).GetLinkURL());
            TextView date = (TextView) featureLayout.findViewById(R.id.featuredate);
            header.setText("FEATURED");
            Image cachedImage = new Image(this.getContext(), items.get(i).GetImageURL());
            image.setImageDrawable(cachedImage.getImage());
            title.setText(items.get(i).GetTitle());
            date.setText(items.get(i).GetDate());
            internalWrapper.addView(featureLayout);
        }
        gestureDetector = new GestureDetector(new MyGestureDetector());
        setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (gestureDetector.onTouchEvent(event)) {
                    return true;
                }
                else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL ){
                    int scrollX = getScrollX();
                    int featureWidth = getMeasuredWidth();
                    activeFeature = ((scrollX + (featureWidth/2))/featureWidth);
                    int scrollTo = activeFeature*featureWidth;
                    smoothScrollTo(scrollTo, 0);
                    return true;
                }
                else{
                    return false;
                }
            }
        });
    }

    class MyGestureDetector extends SimpleOnGestureListener {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            try {
                //right to left 
                if(e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
                    activeFeature = (activeFeature < (items.size() - 1))? activeFeature + 1:items.size() -1;
                    smoothScrollTo(activeFeature*getMeasuredWidth(), 0);
                    return true;
                }  
                //left to right
                else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
                    activeFeature = (activeFeature > 0)? activeFeature - 1:0;
                    smoothScrollTo(activeFeature*getMeasuredWidth(), 0);
                    return true;
                }
            } catch (Exception e) {
                // nothing
            }
            return false;
        }
    }
}
Joel
источник
Я попробовал все методы в этом посте, но ни один из них не работает для меня. Я использую MeetMe's HorizontalListViewбиблиотеку.
Кочевник
Здесь есть статья с похожим кодом ( HomeFeatureLayout extends HorizontalScrollView) velir.com/blog/index.php/2010/11/17/… Есть несколько дополнительных комментариев о том, что происходит при создании пользовательского класса прокрутки.
CJBS

Ответы:

280

Обновление: я понял это. В моем ScrollView мне нужно было переопределить метод onInterceptTouchEvent, чтобы перехватывать событие касания только в том случае, если движение Y - это> движение X. Кажется, что поведение ScrollView по умолчанию состоит в том, чтобы перехватывать событие касания всякий раз, когда есть ЛЮБОЕ движение Y. Таким образом, с исправлением ScrollView будет перехватывать событие, только если пользователь намеренно прокручивает в направлении Y и в этом случае передает ACTION_CANCEL детям.

Вот код для моего класса Scroll View, который содержит HorizontalScrollView:

public class CustomScrollView extends ScrollView {
    private GestureDetector mGestureDetector;

    public CustomScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mGestureDetector = new GestureDetector(context, new YScrollDetector());
        setFadingEdgeLength(0);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev) && mGestureDetector.onTouchEvent(ev);
    }

    // Return false if we're scrolling in the x direction  
    class YScrollDetector extends SimpleOnGestureListener {
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {             
            return Math.abs(distanceY) > Math.abs(distanceX);
        }
    }
}
Joel
источник
я использую то же самое, но во время прокрутки в направлении x я получаю исключение NULL POINTER EXCEPTION в публичном логическом onInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector.onTouchEvent (ev); } Я использовал свиток внутри горизонтально раст Scrollview
Vipin Sahu
@All спасибо , что он работает, я забыл инициализировать детектор жест в Scrollview конструктор
Vipin Sahu
Как я должен использовать этот код для реализации ViewPager внутри ScrollView
Harsha MV
У меня были некоторые проблемы с этим кодом, когда у меня всегда был расширенный вид сетки, иногда он не прокручивался. Мне пришлось переопределить метод onDown (...) в YScrollDetector, чтобы всегда возвращать true, как это предлагается в документации (как здесь developer.android.com/training/custom-views/… ). Это решило мою проблему.
Неманья Ковачевич
3
Я только что столкнулся с небольшой ошибкой, о которой стоит упомянуть. Я считаю, что код в onInterceptTouchEvent должен разделить два логических вызова, чтобы гарантировать, что mGestureDetector.onTouchEvent(ev)вызов будет вызван. Как и сейчас, он не будет вызван, если super.onInterceptTouchEvent(ev)ложь. Я только что натолкнулся на случай, когда кликабельные дети в просмотре прокрутки могут получить сенсорные события, и onScroll вообще не будет вызываться. В противном случае, спасибо, отличный ответ!
GLee
176

Спасибо, Джоэл, за подсказку, как решить эту проблему.

Я упростил код (без необходимости в GestureDetector ) для достижения того же эффекта:

public class VerticalScrollView extends ScrollView {
    private float xDistance, yDistance, lastX, lastY;

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                xDistance = yDistance = 0f;
                lastX = ev.getX();
                lastY = ev.getY();
                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);
    }
}
neevek
источник
отлично, спасибо за этот рефакторинг. У меня возникали проблемы с вышеупомянутым подходом при прокрутке до конца listView, поскольку любое поведение прикосновения к дочерним элементам начало не перехватываться независимо от соотношения движения Y / X. Weird!
Дори
1
Спасибо! Также работал с ViewPager внутри ListView, с пользовательским ListView.
Шариф Шаик
2
Просто заменил принятый ответ этим, и теперь он работает намного лучше для меня. Спасибо!
Дэвид Скотт
1
@VipinSahu, чтобы указать направление движения касания, вы можете взять дельту текущей координаты X и lastX, если оно больше 0, касание перемещается слева направо, в противном случае справа налево. А затем вы сохраните текущий X как lastX для следующего расчета.
neevek
1
как насчет горизонтальной прокрутки?
Зин Вин Хетт
60

Я думаю, что нашел более простое решение, только при этом используется подкласс ViewPager вместо (его родительского) ScrollView.

ОБНОВЛЕНИЕ 2013-07-16 : Я также добавил переопределение для onTouchEvent. Это могло бы помочь с вопросами, упомянутыми в комментариях, хотя YMMV.

public class UninterceptableViewPager extends ViewPager {

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean ret = super.onInterceptTouchEvent(ev);
        if (ret)
            getParent().requestDisallowInterceptTouchEvent(true);
        return ret;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean ret = super.onTouchEvent(ev);
        if (ret)
            getParent().requestDisallowInterceptTouchEvent(true);
        return ret;
    }
}

Это похоже на технику, используемую в android.widget.Gallery onScroll () . Это объясняется в презентации Google I / O 2013 « Написание пользовательских представлений для Android» .

Обновление 2013-12-10 : аналогичный подход также описан в посте Кирилла Гручникова о (тогда) приложении Android Market .

Гиоргос Килафас
источник
boolean ret = super.onInterceptTouchEvent (ev); только когда-либо возвращал ложь для меня. Использование на UninterceptableViewPager в Scrollview
scottyab
Это не работает для меня, хотя мне нравится это простота. Я использую ScrollViewс, LinearLayoutв котором UninterceptableViewPagerнаходится. В самом деле, retвсегда ложно ... Любая подсказка, как это исправить?
Peterdk
@scottyab & @Peterdk: Ну, мой находится в, TableRowкоторый находится внутри, TableLayoutкоторый находится внутри ScrollView(да, я знаю ...), и он работает, как задумано. Возможно, вы могли бы попробовать переопределить onScrollвместо того onInterceptTouchEvent, как это делает Google (строка 1010)
Гиоргос Кайлафас
Я только что жестко запрограммировал ответ как истину, и он отлично работает Ранее он возвращал false, но я верю, что это потому, что скроллвью имеет линейный макет, который содержит всех детей
Amanni
11

Я обнаружил, что иногда ScrollView восстанавливает фокус, а другой теряет фокус. Вы можете предотвратить это, предоставив только один из фокусов scrollView:

    scrollView1= (ScrollView) findViewById(R.id.scrollscroll);
    scrollView1.setAdapter(adapter);
    scrollView1.setOnTouchListener(new View.OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            scrollView1.getParent().requestDisallowInterceptTouchEvent(true);
            return false;
        }
    });
Мариус Веселый
источник
какой адаптер ты проходишь?
Харша М.В.
Я проверил еще раз и понял, что на самом деле я использую не ScrollView, а ViewPager, и я передаю ему FragmentStatePagerAdapter, который включает в себя все изображения для галереи.
Мариус Веселый
8

Это не сработало для меня. Я изменил это, и теперь это работает гладко. Если кому интересно.

public class ScrollViewForNesting extends ScrollView {
    private final int DIRECTION_VERTICAL = 0;
    private final int DIRECTION_HORIZONTAL = 1;
    private final int DIRECTION_NO_VALUE = -1;

    private final int mTouchSlop;
    private int mGestureDirection;

    private float mDistanceX;
    private float mDistanceY;
    private float mLastX;
    private float mLastY;

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

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

    public ScrollViewForNesting(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public ScrollViewForNesting(Context context) {
        this(context,null);
    }    


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {      
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDistanceY = mDistanceX = 0f;
                mLastX = ev.getX();
                mLastY = ev.getY();
                mGestureDirection = DIRECTION_NO_VALUE;
                break;
            case MotionEvent.ACTION_MOVE:
                final float curX = ev.getX();
                final float curY = ev.getY();
                mDistanceX += Math.abs(curX - mLastX);
                mDistanceY += Math.abs(curY - mLastY);
                mLastX = curX;
                mLastY = curY;
                break;
        }

        return super.onInterceptTouchEvent(ev) && shouldIntercept();
    }


    private boolean shouldIntercept(){
        if((mDistanceY > mTouchSlop || mDistanceX > mTouchSlop) && mGestureDirection == DIRECTION_NO_VALUE){
            if(Math.abs(mDistanceY) > Math.abs(mDistanceX)){
                mGestureDirection = DIRECTION_VERTICAL;
            }
            else{
                mGestureDirection = DIRECTION_HORIZONTAL;
            }
        }

        if(mGestureDirection == DIRECTION_VERTICAL){
            return true;
        }
        else{
            return false;
        }
    }
}
snapix
источник
Это ответ на мой проект. У меня есть вид пейджер, который выступает в качестве галереи, можно щелкнуть в прокрутки. Я использую приведенное выше решение, оно работает с горизонтальной прокруткой, но после того, как я щелкну изображение пейджера, которое запускает новое действие и возвращается назад, пейджер не может прокручиваться. Это работает хорошо, ТКС!
longkai
Отлично работает для меня У меня был собственный вид «пролистывание для разблокировки» внутри вида с прокруткой, который доставлял мне ту же проблему. Это решение решило проблему.
гибрид
6

Благодаря Neevek его ответ сработал для меня, но он не блокирует вертикальную прокрутку, когда пользователь начинает прокручивать горизонтальный вид (ViewPager) в горизонтальном направлении, а затем, не поднимая прокрутку пальцем вертикально, начинает прокручивать вид базового контейнера (ScrollView) , Я исправил это, внеся небольшое изменение в код Neevak:

private float xDistance, yDistance, lastX, lastY;

int lastEvent=-1;

boolean isLastEventIntercepted=false;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            xDistance = yDistance = 0f;
            lastX = ev.getX();
            lastY = ev.getY();


            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(isLastEventIntercepted && lastEvent== MotionEvent.ACTION_MOVE){
                return false;
            }

            if(xDistance > yDistance )
                {

                isLastEventIntercepted=true;
                lastEvent = MotionEvent.ACTION_MOVE;
                return false;
                }


    }

    lastEvent=ev.getAction();

    isLastEventIntercepted=false;
    return super.onInterceptTouchEvent(ev);

}
Saqib
источник
5

Это наконец стало частью библиотеки поддержки v4, NestedScrollView . Таким образом, в большинстве случаев, я думаю, больше не нужны локальные хаки.

Эбрахим Бягови
источник
1

Решение Neevek работает лучше, чем решение Joel на устройствах с версией 3.2 и выше. В Android есть ошибка, которая приводит к тому, что java.lang.IllegalArgumentException: pointerIndex выходит за пределы диапазона, если детектор жестов используется внутри scollview. Чтобы продублировать проблему, создайте пользовательский скроллвью, как предложил Джоэл, и поместите пейджер внутри. Если вы перетащите (не поднимайте фигуру) в одном направлении (влево / вправо), а затем в противоположном направлении, вы увидите сбой. Также в решении Джоэла, если вы перетаскиваете пейджер просмотра, перемещая палец по диагонали, как только ваш палец покинет область просмотра содержимого пейджера просмотра, пейджер вернется в прежнее положение. Все эти проблемы больше связаны с внутренним дизайном Android или его отсутствием, чем с реализацией Джоэла, которая сама по себе является частью умного и лаконичного кода.

http://code.google.com/p/android/issues/detail?id=18990

Дон
источник