ScrollView Touch 처리 내의 HorizontalScrollView

전체 화면을 스크롤 할 수 있도록 전체 레이아웃을 둘러싼 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;
        }
    }
}


답변

업데이트 : 나는 이것을 알아 냈습니다. 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);
        }
    }
}

답변

이 문제를 해결하는 방법에 대한 힌트를 주신 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);
    }
}

답변

더 간단한 해결책을 찾았습니다. 이것 만 (상위) 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 마켓 앱에 대해 설명되어 있습니다 .


답변

하나의 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;
        }
    });

답변

그것은 나를 위해 잘 작동하지 않았다. 나는 그것을 바꾸었고 이제는 원활하게 작동합니다. 관심이 있다면

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

답변

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

}

답변

이것은 마침내 지원 v4 라이브러리 인 NestedScrollView 의 일부가되었습니다 . 따라서 대부분의 경우 로컬 해킹이 더 이상 필요하지 않습니다.