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

所以我想要的效果就像普通的android主屏幕,你可以从一个屏幕滚动到另一个屏幕,当你抬起手指时,它就会切换到一个屏幕。

这一切都很好,除了一个问题:我需要几乎完美地水平地从左向右滑动ACTION_UP才能注册。如果我至少垂直滑动(我认为许多人倾向于在手机上左右滑动时这样做),我将收到ACTION_CANCEL而不是ACTION_UP。我的理论是,这是因为horizontalscrollview在一个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;
        }
    }
}

当前回答

日期:2021年5月12日

看起来很无聊,但相信我,如果你想在垂直滚动视图中水平滚动任何视图,这是值得的!!

在jetpack中也可以通过创建一个自定义视图并扩展你想要水平滚动的视图;在垂直滚动视图中,并在AndroidView组合中使用该自定义视图(现在,“Jetpack Compose is in 1.0.0-beta06”)

这是最优的解决方案,如果你想要水平自由滚动和垂直自由滚动,而不让垂直滚动条拦截你的触摸,当你在水平滚动视图中垂直滚动时,只允许垂直滚动条拦截触摸:

private class HorizontallyScrollingView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewThatYouWannaScrollHorizontally(context, attrs){
    override fun onTouchEvent(event: MotionEvent?): Boolean {

        // When the user's finger touches the webview and starts moving
        if(event?.action == MotionEvent.ACTION_MOVE){
            // get the velocity tracker object
            val mVelocityTracker = VelocityTracker.obtain();

            // connect the velocity tracker object with the event that we are emitting while we are touching the webview
            mVelocityTracker.addMovement(event)

            // compute the velocity in terms of pixels per 1000 millisecond(i.e 1 second)
            mVelocityTracker.computeCurrentVelocity(1000);

            // compute the Absolute Velocity in X axis
            val xVelocityABS = abs(mVelocityTracker.getXVelocity(event?.getPointerId((event?.actionIndex))));

            // compute the Absolute Velocity in Y axis
            val yVelocityABS = abs(mVelocityTracker.getYVelocity(event?.getPointerId((event?.actionIndex))));

            // If the velocity of x axis is greater than y axis then we'll consider that it's a horizontal scroll and tell the parent layout
            // "Hey parent bro! im scrolling horizontally, this has nothing to do with ur scrollview so stop capturing my event and stay the f*** where u are "
            if(xVelocityABS > yVelocityABS){
                //  So, we'll disallow the parent to listen to any touch events until i have moved my fingers off the screen
                parent.requestDisallowInterceptTouchEvent(true)
            }
        } else if (event?.action == MotionEvent.ACTION_CANCEL || event?.action == MotionEvent.ACTION_UP){
            // If the touch event has been cancelled or the finger is off the screen then reset it (i.e let the parent capture the touch events on webview as well)
            parent.requestDisallowInterceptTouchEvent(false)
        }
        return super.onTouchEvent(event)
    }
}

这里,viewthatyouwannascrollhorizontal是你想要水平滚动的视图,当你水平滚动时,你不希望垂直滚动条捕捉到触摸并认为“哦!用户在垂直滚动所以parent。requestdisallowintercepttouchevent (true)基本上会说垂直滚动条"hey you!不要捕捉任何触摸,因为用户是水平滚动的”

当用户完成水平滚动并试图垂直滚动置于垂直滚动条内的水平滚动条时,它会看到Y轴的触摸速度大于X轴,这表明用户没有水平滚动而水平滚动的东西会说“嘿,你!家长,你听到了吗?用户正在垂直滚动通过我,现在你可以拦截触摸,并在垂直滚动中显示我下面的东西。”

其他回答

日期:2021年5月12日

看起来很无聊,但相信我,如果你想在垂直滚动视图中水平滚动任何视图,这是值得的!!

在jetpack中也可以通过创建一个自定义视图并扩展你想要水平滚动的视图;在垂直滚动视图中,并在AndroidView组合中使用该自定义视图(现在,“Jetpack Compose is in 1.0.0-beta06”)

这是最优的解决方案,如果你想要水平自由滚动和垂直自由滚动,而不让垂直滚动条拦截你的触摸,当你在水平滚动视图中垂直滚动时,只允许垂直滚动条拦截触摸:

private class HorizontallyScrollingView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewThatYouWannaScrollHorizontally(context, attrs){
    override fun onTouchEvent(event: MotionEvent?): Boolean {

        // When the user's finger touches the webview and starts moving
        if(event?.action == MotionEvent.ACTION_MOVE){
            // get the velocity tracker object
            val mVelocityTracker = VelocityTracker.obtain();

            // connect the velocity tracker object with the event that we are emitting while we are touching the webview
            mVelocityTracker.addMovement(event)

            // compute the velocity in terms of pixels per 1000 millisecond(i.e 1 second)
            mVelocityTracker.computeCurrentVelocity(1000);

            // compute the Absolute Velocity in X axis
            val xVelocityABS = abs(mVelocityTracker.getXVelocity(event?.getPointerId((event?.actionIndex))));

            // compute the Absolute Velocity in Y axis
            val yVelocityABS = abs(mVelocityTracker.getYVelocity(event?.getPointerId((event?.actionIndex))));

            // If the velocity of x axis is greater than y axis then we'll consider that it's a horizontal scroll and tell the parent layout
            // "Hey parent bro! im scrolling horizontally, this has nothing to do with ur scrollview so stop capturing my event and stay the f*** where u are "
            if(xVelocityABS > yVelocityABS){
                //  So, we'll disallow the parent to listen to any touch events until i have moved my fingers off the screen
                parent.requestDisallowInterceptTouchEvent(true)
            }
        } else if (event?.action == MotionEvent.ACTION_CANCEL || event?.action == MotionEvent.ACTION_UP){
            // If the touch event has been cancelled or the finger is off the screen then reset it (i.e let the parent capture the touch events on webview as well)
            parent.requestDisallowInterceptTouchEvent(false)
        }
        return super.onTouchEvent(event)
    }
}

这里,viewthatyouwannascrollhorizontal是你想要水平滚动的视图,当你水平滚动时,你不希望垂直滚动条捕捉到触摸并认为“哦!用户在垂直滚动所以parent。requestdisallowintercepttouchevent (true)基本上会说垂直滚动条"hey you!不要捕捉任何触摸,因为用户是水平滚动的”

当用户完成水平滚动并试图垂直滚动置于垂直滚动条内的水平滚动条时,它会看到Y轴的触摸速度大于X轴,这表明用户没有水平滚动而水平滚动的东西会说“嘿,你!家长,你听到了吗?用户正在垂直滚动通过我,现在你可以拦截触摸,并在垂直滚动中显示我下面的东西。”

我想我找到了一个更简单的解决方案,只是这使用了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中使用的技术。画廊onScroll()。 谷歌I/O 2013演示文稿为Android编写自定义视图进一步解释了这一点。

更新2013-12-10:Kirill Grouchnikov在一篇关于(当时的)Android Market应用的文章中也描述了类似的方法。

这最终成为支持v4库NestedScrollView的一部分。所以,我猜在大多数情况下不再需要本地黑客了。

我发现有时一个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;
        }
    });

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