ScrollView Touch 처리 내의 HorizontalScrollView


226

전체 화면을 스크롤 할 수 있도록 전체 레이아웃을 둘러싼 ScrollView가 있습니다. 이 ScrollView에있는 첫 번째 요소는 가로로 스크롤 할 수있는 기능이있는 HorizontalScrollView 블록입니다. 터치 이벤트를 처리하고보기가 ACTION_UP 이벤트에서 가장 가까운 이미지에 "스냅"되도록 ontouchlistener를 horizontalscrollview에 추가했습니다.

그래서 내가 가고있는 효과는 주식 안드로이드 홈 화면과 같습니다. 여기서 한 화면에서 다른 화면으로 스크롤 할 수 있으며 손가락을 들어 올리면 한 화면에 스냅됩니다.

이것은 하나의 문제를 제외하고는 모두 잘 작동합니다 .ACTION_UP이 등록되도록 왼쪽에서 오른쪽으로 거의 완벽하게 가로로 스 와이프해야합니다. 내가 세로로 스 와이프하면 (많은 사람들이 좌우로 스 와이프 할 때 휴대 전화에서하는 경향이 있다고 생각합니다), ACTION_UP 대신 ACTION_CANCEL을받습니다. 내 이론은 가로 스크롤보기가 스크롤보기 내에 있고 스크롤보기가 세로 스크롤을 허용하기 위해 세로 터치를 가로 채고 있기 때문입니다.

가로 스크롤보기 내에서 스크롤보기의 터치 이벤트를 비활성화하고 스크롤보기의 다른 곳에서 일반 세로 스크롤을 허용하는 방법은 무엇입니까?

내 코드 샘플은 다음과 같습니다.

   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도서관을 사용하고 있습니다.
유목민

velir.com/blog/index.php/2010/11/17/ 와 비슷한 코드가있는 기사가 HomeFeatureLayout extends HorizontalScrollView있습니다 ... 사용자 정의 스크롤 클래스가 구성 될 때 무슨 일이 일어나고 있는지에 대한 추가 의견이 있습니다.
CJBS

답변:


280

업데이트 : 나는 이것을 알아 냈습니다. ScrollView에서 Y 모션이> X 모션 인 경우 터치 이벤트 만 가로 채기 위해 onInterceptTouchEvent 메서드를 재정의해야했습니다. ScrollView의 기본 동작은 Y 모션이있을 때마다 터치 이벤트를 가로 채는 것 같습니다. 따라서 수정으로 ScrollView는 사용자가 의도적으로 Y 방향으로 스크롤하는 경우에만 이벤트를 가로 채고 그 경우 ACTION_CANCEL을 자식에게 전달합니다.

HorizontalScrollView를 포함하는 내 Scroll View 클래스의 코드는 다음과 같습니다.

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); } 스크롤보기에서 가로 스크롤을 사용했습니다
Vipin Sahu

이 일하고 @All 덕분에, 내가있는 ScrollView 생성자에서 제스처 검출기를 초기화하는 것을 잊지
비핀 Sahu에게

ScrollView 내부에 ViewPager를 구현하기 위해이 코드를 어떻게 사용해야합니까
Harsha MV

항상 확장 된 그리드보기가있을 때이 코드에 문제가 있었으며 때로는 스크롤되지 않습니다. YScrollDetector에서 onDown (...) 메소드를 재정 의하여 문서 전체에서 제안 된대로 항상 true를 반환해야했습니다 (예 : developer.android.com/training/custom-views/… ) 이렇게하면 내 문제가 해결되었습니다.
Nemanja Kovacevic

3
방금 언급 할 작은 버그가 발생했습니다. onInterceptTouchEvent의 코드는 두 개의 부울 호출을 분리하여 호출되도록 보장해야 mGestureDetector.onTouchEvent(ev)합니다. 지금과 같이 super.onInterceptTouchEvent(ev)거짓 이면 호출되지 않습니다 . 스크롤보기에서 클릭 가능한 어린이가 터치 이벤트를 가져올 수 있고 onScroll이 전혀 호출되지 않는 경우가 발생했습니다. 그렇지 않으면 감사합니다.
GLee

176

이 문제를 해결하는 방법에 대한 힌트를 주신 Joel에게 감사합니다.

동일한 효과를 얻기 위해 코드를 단순화했습니다 ( 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);
    }
}

이 리 팩터에 감사드립니다. Y / X 이동 비율에 관계없이 자식 요소에 대한 터치 동작이 가로 채지 않기 시작하여 listView의 맨 아래로 스크롤 할 때 위의 접근 방식에 문제가 발생했습니다. 기묘한!
Dori

1
감사! 또한 사용자 정의 ListView와 함께 ListView 내부의 ViewPager와 함께 작업했습니다.
Sharief Shaik

2
수락 된 답변을 이것으로 바꾸고 지금은 훨씬 나아지고 있습니다. 감사!
David Scott

1
@VipinSahu, 터치 이동 방향을 알려면 현재 X 좌표와 lastX의 델타를 취할 수 있습니다 .0보다 크면 터치가 왼쪽에서 오른쪽으로, 그렇지 않으면 오른쪽에서 왼쪽으로 이동합니다. 그리고 다음 계산을 위해 현재 X를 lastX로 저장합니다.
neevek

1
가로 스크롤보기는 어떻습니까?
Zin Win Htet

60

더 간단한 해결책을 찾았습니다. 이것 만 (상위) ScrollView 대신 ViewPager의 하위 클래스를 사용합니다.

업데이트 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 업데이트 : 비슷한 접근 방식은 Kirill Grouchnikov의 게시물에서 Android 마켓 앱에 대해 설명되어 있습니다 .


부울 ret = super.onInterceptTouchEvent (ev); 오직 나를 위해 거짓을 반환했습니다. Scrollview에서
UninterceptableViewPager

나는 그것이 단순하다는 것을 좋아하지만 나에게는 효과가 없다. 나는를 사용 ScrollView로모그래퍼 LinearLayout(가)있는 UninterceptableViewPager배치됩니다. 사실, ret항상 거짓입니다 ... 이것을 고치는 방법에 대한 단서가 있습니까?
Peterdk

@scottyab 및 @Peterdk : 글쎄, 내가에 TableRow내부에있는 TableLayout돌며있는이 ScrollView의도 한대로 (그래, 나도 알아 ...), 그리고 그것을 일하고있어. 어쩌면 당신은 무시 시도 할 수 onScroll대신 onInterceptTouchEvent처럼, 구글은 않습니다 (라인 1010)
Giorgos Kylafas

ret을 true로 하드 코딩했으며 정상적으로 작동합니다. 그것은 이전에 거짓을 반환했지만
스크롤보기

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

어떤 어댑터를 전달하고 있습니까?
Harsha MV

다시 확인하고 실제로 사용하는 ScrollView가 아니라 ViewPager이며 갤러리의 모든 이미지가 포함 된 FragmentStatePagerAdapter를 전달하고 있음을 깨달았습니다.
Marius Hilarious

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

}


1

Neevek의 솔루션은 3.2 이상을 실행하는 장치에서 Joel의 솔루션보다 잘 작동합니다. 안드로이드에 java.lang.IllegalArgumentException : scollview 내에서 제스처 탐지기가 사용되는 경우 pointerIndex가 범위를 벗어난 버그가 있습니다. 이 문제를 복제하려면 Joel이 제안한대로 사용자 지정 scollview를 구현하고 뷰 호출기를 안에 넣으십시오. 한 방향 (왼쪽 / 오른쪽)으로 끌고 (그림을 올리지 마십시오) 반대 방향으로 끌면 충돌이 나타납니다. 또한 Joel의 솔루션에서 손가락을 대각선으로 움직여보기 호출기를 드래그하면 손가락이보기 호출기의 컨텐츠보기 영역을 벗어나면 호출기가 이전 위치로 돌아갑니다. 이 모든 문제는 Joel의 구현보다 Android의 내부 디자인 또는 부족과 관련이 있습니다.이 자체는 똑똑하고 간결한 코드입니다.

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.