HorizontalScrollView в ScrollView Touch Handling


226

У мене є ScrollView, який оточує весь мій макет, щоб весь екран можна прокручувати. Перший елемент, який я маю в цьому ScrollView, - це блок HorizontalScrollView, який має функції, які можна прокручувати по горизонталі. До горизонтального перегляду горизонтального прокрутки я додав онтоутворювача, щоб обробляти події, що торкаються, і змушувати погляд "прив'язуватися" до найближчого зображення події ACTION_UP.

Таким чином, ефект, на який я збираюся, - це як домашній екран Android, де ви можете прокручувати один на інший, і він піднімає один екран, коли ви піднімаєте палець.

Це все чудово працює за винятком однієї проблеми: мені потрібно провести пальцем вліво-вправо майже ідеально по горизонталі, щоб 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;
        }
    }
}

Я спробував усі методи в цій публікації, але жоден з них не працює на мене. Я використовую 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);
        }
    }
}

я використовую це те саме, але під час прокрутки у напрямку x я отримую виключення NULL POINTER EXCEPTION у загальнодоступному булевому режимі onInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector.onTouchEvent (ev); } Я використовував сувій всередині горизонтально ро Scrollview
Vipin Sahu

@All спасибі , що він працює, я забув форматувати детектор жест в Scrollview конструктор
Vipin Sahu

Як я повинен використовувати цей код для реалізації ViewPager всередині ScrollView
Харша М. В.

У мене виникли проблеми з цим кодом, коли я мав у ньому завжди розширений вигляд сітки, іноді він не прокручувався. Мені довелося переохочувати метод onDown (...) у YScrollDetector, щоб завжди повертати правду, як було запропоновано в документації (наприклад, тут developer.android.com/training/custom-views/… ) Це вирішило мою проблему.
Nemanja Kovacevic

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

відмінно, дякую за цей рефактор. У мене виникали проблеми з вищезазначеним підходом під час прокрутки до нижньої частини listView, оскільки будь-яка сенсорна поведінка над дочірніми елементами почала не перехоплюватися незалежно від співвідношення руху Y / X. Дивно!
Дорі

1
Дякую! Також працював із ViewPager всередині ListView, із спеціальним ListView.
Sharief Shaik

2
Просто замінив прийняту відповідь на це, і для мене це працює набагато краще. Дякую!
Девід Скотт

1
@VipinSahu, щоб вказати напрямок руху дотику, ви можете взяти дельту поточної координати X і lastX, якщо вона більша за 0, дотик рухається зліва направо, інакше справа наліво. І тоді ви зберігаєте поточний X як lastX для наступного обчислення.
neevek

1
як щодо горизонтального прокрутки?
Zin Win Htet

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 .


булева ret = super.onInterceptTouchEvent (ev); тільки коли-небудь повертав помилкове для мене. Використання UninterceptableViewPager у Scrollview
scottyab

Це не працює для мене, хоча мені подобається, що це простота. Я використовую a ScrollViewз a, LinearLayoutв якому UninterceptableViewPagerрозміщено. Дійсно,ret завжди помилково ... Будь-яка підказка, як це виправити?
Peterdk

@scottyab & @Peterdk: Добре, моє знаходиться в приміщенні, TableRowяке знаходиться всередині a, TableLayoutяке знаходиться всередині ScrollView(так, я знаю ...), і воно працює за призначенням. Можливо, ви можете спробувати перекрутитиonScroll замість цього onInterceptTouchEvent, як Google це робить (рядок 1010)
Giorgos Kylafas

Я просто жорстко закодований як справжній, і він працює чудово. Він повертався помилковим раніше, але я вважаю, що це тому, що у прокрутки є лінійна схема, яка вміщує всіх дітей
Аманні

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

Це відповідь для мого проекту. У мене є пейджер перегляду, який виступає в ролі галереї, можна натиснути на прокрутку. Я використовую наведене вище рішення, воно працює на горизонтальній прокрутці, але після того, як я натискаю зображення пейджера, який починає нову діяльність і повертається назад, пейджер не може прокрутити. Це добре працює, tks!
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);

}

5

Це нарешті стало частиною підтримки v4-бібліотеки, NestedScrollView . Отже, більше я не думаю, що локальні хаки потрібні для більшості випадків.


1

Рішення Neevek працює краще, ніж Джоель, на пристроях під керуванням 3.2 і вище. В Android є помилка, яка спричинить java.lang.IllegalArgumentException: pointerIndex поза межами діапазону, якщо детектор жестів буде використаний всередині огляду. Щоб дублювати проблему, застосуйте користувальницький показ огляду, як запропонував Джоель, і покладіть всередину пейджер перегляду. Якщо ви перетягнете (не піднімайте фігуру) в одну сторону (вліво / вправо), а потім в протилежну сторону, ви побачите збій. Також у рішенні Джоеля, якщо перетягнути пейджер перегляду, переміщуючи пальцем по діагоналі, як тільки палець покине область перегляду вмісту пейджера, пейджер повернеться до попереднього положення. Усі ці питання пов'язані більше з внутрішнім дизайном Android або його відсутністю, ніж з реалізацією Джоела, що саме є частиною розумного та стислого коду.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.