首页 文章

ScrollView Touch Handling中的HorizontalScrollView

提问于
浏览
221

我有一个ScrollView环绕我的整个布局,以便整个屏幕可滚动 . 我在这个ScrollView中的第一个元素是一个HorizontalScrollView块,它具有可以水平滚动的功能 . 我在horizontalscrollview中添加了一个ontouchlistener来处理触摸事件并强制视图“捕捉”到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;
    }
}

}

8 回答

  • 60

    更新:我想通了 . 在我的ScrollView上,我需要覆盖onInterceptTouchEvent方法,以便仅在Y运动> X运动时拦截触摸事件 . 看起来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);
            }
        }
    }
    
  • 11

    谢谢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);
        }
    }
    
  • 5

    我想我找到了一个更简单的解决方案,只有这个使用ViewPager的子类而不是(它的父)ScrollView .

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

    这类似于the technique used in android.widget.Gallery's onScroll() . Google I / O 2013演示文稿Writing Custom Views for Android进一步解释了这一点 .

    Update 2013-12-10a post from Kirill Grouchnikov about the (then) Android Market app中也描述了类似的方法 .

  • 6

    我发现有些时候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;
            }
        });
    
  • 278

    它对我来说效果不佳 . 我改变了它,现在它运作顺利 . 如果有人有兴趣 .

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

    感谢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);
    
    }
    
  • 8

    这最终成为支持v4库的一部分,NestedScrollView . 因此,对于我猜的大多数情况,不再需要本地黑客攻击 .

  • 1

    Neevek的解决方案比运行3.2及更高版本的设备上的Joel更好 . Android中存在一个错误,如果在scollview中使用手势检测器,则会导致java.lang.IllegalArgumentException:pointerIndex超出范围 . 要复制该问题,请按照Joel的建议实现自定义scollview,并在其中放置一个视图寻呼机 . 如果你(不要抬起你的身影)向一个方向(左/右)然后向相反方向拖动,你会看到崩溃 . 同样在Joel的解决方案中,如果您通过对角移动手指来拖动视图寻呼机,一旦您的手指离开视图寻呼机的内容视图区域,寻呼机将弹回其先前的位置 . 所有这些问题都与Android的内部设计有关,或者与Joel的实现相比缺乏,这本身就是一段智能和简洁的代码 .

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

相关问题