NgangScrollView trong ScrollView Touch Xử lý


226

Tôi có một ScrollView bao quanh toàn bộ bố cục của mình để toàn bộ màn hình có thể cuộn được. Phần tử đầu tiên tôi có trong ScrollView này là một khối ngangScrollView có các tính năng có thể cuộn qua chiều ngang. Tôi đã thêm một ontouchlistener vào heftalscrollview để xử lý các sự kiện chạm và buộc chế độ xem "chụp" vào hình ảnh gần nhất trong sự kiện ACTION_UP.

Vì vậy, hiệu ứng tôi sẽ làm giống như màn hình chính của android, nơi bạn có thể cuộn từ màn hình này sang màn hình khác và nó chạm vào một màn hình khi bạn nhấc ngón tay lên.

Tất cả điều này hoạt động tuyệt vời ngoại trừ một vấn đề: Tôi cần vuốt từ trái sang phải gần như hoàn toàn theo chiều ngang để ACTION_UP được đăng ký. Nếu tôi vuốt theo chiều dọc ở mức tối thiểu (mà tôi nghĩ nhiều người có xu hướng làm trên điện thoại của họ khi vuốt sang bên), tôi sẽ nhận được ACTION_CANCEL thay vì ACTION_UP. Lý thuyết của tôi là điều này là do heftalscrollview nằm trong một scrollview và scrollview đang chiếm quyền điều khiển theo chiều dọc để cho phép cuộn dọc.

Làm cách nào tôi có thể vô hiệu hóa các sự kiện cảm ứng cho cuộn xem từ ngay trong cuộn xem ngang của mình, nhưng vẫn cho phép cuộn dọc bình thường ở nơi khác trong cuộn xem?

Đây là một mẫu mã của tôi:

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

Tôi đã thử tất cả các phương pháp trong bài viết này, nhưng không có phương pháp nào phù hợp với tôi. Tôi đang sử dụng MeetMe's HorizontalListViewthư viện.
Người du mục

Có một bài viết với một số mã tương tự ( HomeFeatureLayout extends HorizontalScrollView) ở đây velir.com/blog/index.php/2010/11/17/NH Có một số nhận xét bổ sung về những gì đang diễn ra khi lớp cuộn tùy chỉnh được tạo.
CJBS

Câu trả lời:


280

Cập nhật: Tôi đã tìm ra điều này. Trên ScrollView của tôi, tôi cần ghi đè phương thức onInterceptTouchEvent để chỉ chặn sự kiện chạm nếu chuyển động Y là> chuyển động X. Có vẻ như hành vi mặc định của ScrollView là chặn sự kiện chạm bất cứ khi nào có bất kỳ chuyển động nào. Vì vậy, với bản sửa lỗi, ScrollView sẽ chỉ chặn sự kiện nếu người dùng cố tình cuộn theo hướng Y và trong trường hợp đó, hãy chuyển ACTION_CANCEL cho trẻ em.

Đây là mã cho lớp Scroll View của tôi có chứa verticalScrollView:

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

Tôi đang sử dụng tương tự nhưng tại thời điểm cuộn theo hướng x, tôi đang gặp ngoại lệ NULL POINTER EXCEPTION tại boolean onInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector } Tôi đã sử dụng cuộn ngang trong cuộn xem
Vipin Sahu

@ Tất cả cảm ơn vì nó hoạt động, tôi quên khởi tạo trình phát hiện cử chỉ trong trình tạo cuộn của trình xem
Vipin Sahu

Tôi nên sử dụng mã này như thế nào để triển khai ViewPager bên trong ScrollView
Harsha MV

Tôi đã gặp một số vấn đề với mã này khi tôi có chế độ xem lưới luôn mở rộng trong đó, đôi khi nó không cuộn được. Tôi đã phải ghi đè phương thức onDown (...) trong YScrollDetector để luôn trả về đúng như được đề xuất trong toàn bộ tài liệu (như ở đây developer.android.com/training/custom-view/ít ) Điều này đã giải quyết vấn đề của tôi.
Nemanja Kovacevic

3
Tôi vừa gặp phải một lỗi nhỏ đáng nói. Tôi tin rằng mã trong onInterceptTouchEvent nên tách ra hai cuộc gọi boolean, để đảm bảo rằng nó mGestureDetector.onTouchEvent(ev)sẽ được gọi. Như bây giờ, nó sẽ không được gọi nếu super.onInterceptTouchEvent(ev)là sai. Tôi vừa gặp phải trường hợp trẻ em có thể nhấp trong cuộn xem có thể lấy các sự kiện chạm và onScroll sẽ không được gọi. Nếu không, cảm ơn, câu trả lời tuyệt vời!
GLee

176

Cảm ơn bạn Joel đã cho tôi manh mối về cách giải quyết vấn đề này.

Tôi đã đơn giản hóa mã (không cần GestureDetector ) để đạt được hiệu quả tương tự:

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

tuyệt vời, cảm ơn cho refactor này. Tôi đã gặp vấn đề với cách tiếp cận ở trên khi cuộn xuống dưới cùng của listView khi mọi hành vi chạm vào các phần tử con bắt đầu không bị chặn cho dù tỷ lệ chuyển động Y / X là bao nhiêu. Kỳ dị!
Dori

1
Cảm ơn! Cũng đã làm việc với ViewPager bên trong ListView, với ListView tùy chỉnh.
Sharief Shaik

2
Chỉ cần thay thế câu trả lời được chấp nhận bằng câu này và nó hoạt động tốt hơn nhiều đối với tôi bây giờ. Cảm ơn!
David Scott

1
@VipinSahu, để cho biết hướng di chuyển của cảm ứng, bạn có thể lấy delta của tọa độ X hiện tại và lastX, nếu nó lớn hơn 0, chạm sẽ di chuyển từ trái sang phải, nếu không phải sang trái. Và sau đó bạn lưu X hiện tại làm LastX để tính toán tiếp theo.
neevek

1
Điều gì về scrollview ngang?
Zin Win Htet

60

Tôi nghĩ rằng tôi đã tìm thấy một giải pháp đơn giản hơn, chỉ có điều này sử dụng một lớp con của ViewPager thay vì ScrollView (cha mẹ của nó).

CẬP NHẬT 2013-07-16 : Tôi cũng đã thêm một ghi đè onTouchEvent. Nó có thể có thể giúp với các vấn đề được đề cập trong các ý kiến, mặc dù 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;
    }
}

Điều này tương tự như kỹ thuật được sử dụng trong android.widget.Gallery's onScroll () . Nó được giải thích thêm bằng bài thuyết trình Google I / O 2013 Viết Chế độ xem tùy chỉnh cho Android .

Cập nhật 2013-12-10 : Một cách tiếp cận tương tự cũng được mô tả trong một bài đăng từ Kirill Grouchnikov về ứng dụng Android (sau đó) .


boolean ret = super.onInterceptTouchEvent (ev); chỉ bao giờ trả lại sai cho tôi. Sử dụng trên UninterceptableViewPager trong Scrollview
scottyab

Điều này không làm việc cho tôi, mặc dù tôi thích nó đơn giản. Tôi sử dụng một ScrollViewvới một LinearLayouttrong đó UninterceptableViewPagerđược đặt. Thật vậy, retluôn luôn là sai ... Bất kỳ manh mối làm thế nào để khắc phục điều này?
Peterdk

@scottyab & @Peterdk: Chà, cái của tôi nằm trong TableRowcái nằm bên TableLayouttrong cái ScrollView(vâng, tôi biết ...), và nó hoạt động như dự định. Có lẽ bạn có thể thử ghi đè onScrollthay vì onInterceptTouchEvent, như Google thực hiện (dòng 1010)
Giorgos Kylafas

Tôi vừa mới mã hóa ret là đúng và nó hoạt động tốt. Nó đã trở lại sai trước đó nhưng tôi tin đó là vì scrollview có bố cục tuyến tính chứa tất cả trẻ em
Amanni

11

Tôi đã phát hiện ra rằng đôi khi một ScrollView lấy lại tiêu điểm và cái còn lại mất tiêu điểm. Bạn có thể ngăn chặn điều đó, bằng cách chỉ cấp một trong các tiêu điểm 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;
        }
    });

bạn đang chuyển qua bộ chuyển đổi nào?
MV Harsha

Tôi đã kiểm tra lại và nhận ra rằng đó thực sự không phải là một ScrollView mà tôi đang sử dụng, mà là ViewPager và tôi đang truyền cho nó một FragmentStatePagerAd CHƯƠNG bao gồm tất cả các hình ảnh cho thư viện.
Marius H vui nhộn

8

Nó không hoạt động tốt với tôi. Tôi đã thay đổi nó và bây giờ nó hoạt động trơn tru. Nếu ai quan tâm.

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

Đây là câu trả lời cho dự án của tôi. Tôi có một máy nhắn tin xem hoạt động như một bộ sưu tập có thể được nhấp vào trong một cuộn xem. Tôi sử dụng giải pháp cung cấp ở trên, nó hoạt động trên cuộn ngang, nhưng sau khi tôi nhấp vào hình ảnh của máy nhắn tin bắt đầu một hoạt động mới và quay lại, máy nhắn tin không thể cuộn. Điều này hoạt động tốt, tks!
longkai

Hoạt động hoàn hảo cho tôi. Tôi đã có chế độ xem "vuốt để mở khóa" tùy chỉnh bên trong chế độ xem cuộn, điều này cũng gây cho tôi cùng một rắc rối. Giải pháp này đã giải quyết được vấn đề.
lai

6

Nhờ Neevek, câu trả lời của anh ấy đã có tác dụng với tôi nhưng nó không khóa cuộn dọc khi người dùng bắt đầu cuộn chế độ xem ngang (ViewPager) theo hướng ngang và sau đó không nhấc ngón tay theo chiều dọc, nó bắt đầu cuộn chế độ xem bên dưới (ScrollView) . Tôi đã sửa nó bằng cách thay đổi một chút trong mã của 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

Điều này cuối cùng đã trở thành một phần của thư viện v4 hỗ trợ, NestedScrollView . Vì vậy, không còn các bản hack cục bộ nữa là cần thiết cho hầu hết các trường hợp tôi đoán.


1

Giải pháp của Neevek hoạt động tốt hơn Joel trên các thiết bị chạy 3.2 trở lên. Có một lỗi trong Android sẽ gây ra java.lang.IllegalArgumentException: con trỏ ra khỏi phạm vi nếu một trình phát hiện cử chỉ được sử dụng trong một scollview. Để sao chép vấn đề, hãy triển khai một scollview tùy chỉnh như Joel đề xuất và đặt một máy nhắn tin xem bên trong. Nếu bạn kéo (không nâng hình của bạn) sang một hướng (trái / phải) và sau đó sang hướng ngược lại, bạn sẽ thấy sự cố. Ngoài ra, trong giải pháp của Joel, nếu bạn kéo máy nhắn tin bằng cách di chuyển ngón tay theo đường chéo, một khi ngón tay của bạn rời khỏi khu vực xem nội dung của trình xem, máy nhắn tin sẽ quay trở lại vị trí trước đó. Tất cả những vấn đề này liên quan nhiều đến thiết kế bên trong của Android hoặc thiếu nó hơn là việc triển khai của Joel, bản thân nó là một đoạn mã thông minh và súc tích.

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.