ScrollView触摸处理中的Horizo​​ntalScrollView


226

我有一个ScrollView围绕着我的整个布局,因此整个屏幕都是可滚动的。我在此ScrollView中拥有的第一个元素是Horizo​​ntalScrollView块,它具有可以水平滚动的功能。我已经在水平滚动视图中添加了ontouchlistener来处理触摸事件,并强制视图“捕捉”到ACTION_UP事件上最接近的图像。

因此,我想要的效果就像普通的android主屏幕,您可以在其中滚动到另一屏幕,并且在松开手指时可以捕捉到一个屏幕。

除一个问题外,所有其他方法都很好用:我需要几乎完全水平地从左向右滑动才能使ACTION_UP注册。如果我至少在垂直方向上滑动(我认为很多人在左右滑动时倾向于在手机上滑动),那么我会收到ACTION_CANCEL而不是ACTION_UP。我的理论是,这是因为horizo​​ntalscrollview在scrollview中,并且scrollview劫持了垂直触摸以允许垂直滚动。

如何仅从水平滚动视图中禁用滚动视图的触摸事件,但仍允许在滚动视图中的其他位置进行正常的垂直滚动?

这是我的代码示例:

   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图书馆。
Nomad

velir.com/blog/index.php/2010/11/17/…上 有一篇文章,其中包含类似的代码(HomeFeatureLayout extends HorizontalScrollView)… 关于自定义滚动类的组成,还有一些其他注释。
CJBS 2014年

Answers:


280

更新:我想通了。在我的ScrollView上,我需要重写onInterceptTouchEvent方法,以仅在Y动作> X动作时拦截触摸事件。似乎ScrollView的默认行为是在有任何Y运动时都拦截触摸事件。因此,使用此修复程序,如果用户有意在Y方向上滚动并且在这种情况下将ACTION_CANCEL传递给子级,则ScrollView将仅拦截事件。

这是我的Scroll View类的代码,其中包含Horizo​​ntalScrollView:

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方向滚动时,在公共布尔值onInterceptTouchEvent(MotionEvent ev)上出现异常NULL POINTER EXCEPTION异常。}我在scrollview中使用了horizo​​ntall滚动
Vipin Sahu

@All感谢它的工作,我忘记了在scrollview构造函数中初始化手势检测器
Vipin Sahu

我应如何使用此代码在ScrollView中实现ViewPager
Harsha MV

当我有一个始终扩展的网格视图时,此代码存在一些问题,有时它不会滚动。我必须重写YScrollDetector中的onDown(...)方法,以始终按照整个文档中的建议返回true(例如,这里developer.android.com/training/custom-views/…),这解决了我的问题。
Nemanja Kovacevic

3
我只是遇到了一个小错误,值得一提。我相信onInterceptTouchEvent中的代码应拆分两个布尔调用,以确保mGestureDetector.onTouchEvent(ev)将被调用。到目前为止,如果super.onInterceptTouchEvent(ev)为false ,则不会调用它。我只是遇到一种情况,滚动视图中的可单击子级可以捕获触摸事件,而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);
    }
}

非常好,感谢您的重构。当滚动到listView的底部时,我遇到了上述方法的问题,因为无论Y / X移动比率如何,子元素上的任何触摸行为都不会被截获。奇怪的!
Dori 2012年

1
谢谢!还可以在带有自定义ListView的ListView中使用ViewPager。
Sharief Shaik 2012年

2
刚刚用这个替换了可接受的答案,现在对我来说效果更好。谢谢!
大卫·斯科特

1
@VipinSahu,要告诉触摸移动的方向,可以获取当前X坐标和lastX的增量,如果它大于0,则触摸从左向右移动,否则从右向左移动。然后,将当前X保存为lastX,以进行下一次计算。
neevek 2012年

1
水平滚动视图如何?
Zin Win Htet 2014年

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日更新Kirill Grouchnikov在有关(当时)Android Market应用程序的帖子中也描述了类似的方法。


布尔ret = super.onInterceptTouchEvent(ev); 只为我返回假。在Scrollview中使用UninterceptableViewPager
scottyab

这对我不起作用,尽管我喜欢它的简单性。我将放在其中的ScrollView搭配使用。确实,总是错误的……有什么线索可以解决这个问题?LinearLayoutUninterceptableViewPagerret
彼得德克

@scottyab&@Peterdk:好吧,我的是在一个TableRow里面,一个在一个里面,在一个TableLayout里面ScrollView(是的,我知道...),并且它按预期工作。也许您可以尝试改写onScroll而不是onInterceptTouchEventGoogle那样重写(第1010行)
Giorgos Kylafas,2013年

我只是将ret硬编码为true,并且工作正常。它早些时候返回了false,但是我认为这是因为scrollview具有线性布局,可以容纳所有孩子
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;
        }
    });

您要传递什么适配器?
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 2014年

非常适合我。我在滚动视图中有一个自定义的“滑动解锁”视图,这给我带来了同样的麻烦。此解决方案解决了该问题。
混合

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更好。如果在scollview中使用手势检测器,Android中有一个错误会导致java.lang.IllegalArgumentException:pointerIndex超出范围。要重现该问题,请按照Joel的建议实施自定义scollview,然后在其中放入视图分页器。如果向一个方向(向左/向右)拖动(不要抬高图形)然后向相反方向拖动,则会看到崩溃。同样在Joel的解决方案中,如果您通过沿对角移动手指拖动视图寻呼机,则一旦手指离开视图寻呼机的内容查看区域,该寻呼机就会弹回到其先前位置。所有这些问题更多是与Android的内部设计有关,而不是与Joel的实现有关,而Joel的实现本身就是一段巧妙而简洁的代码。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.