ChatGPT解决这个技术问题 Extra ChatGPT

ScrollView 触摸处理中的 HorizontalScrollView

我有一个 ScrollView 围绕我的整个布局,以便整个屏幕是可滚动的。我在这个 ScrollView 中的第一个元素是一个 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;
        }
    }
}
我已经尝试了这篇文章中的所有方法,但它们都不适合我。我正在使用 MeetMe's HorizontalListView 库。
这里有一篇包含一些类似代码 (HomeFeatureLayout extends HorizontalScrollView) 的文章velir.com/blog/index.php/2010/11/17/…还有一些关于自定义滚动类组成时发生了什么的附加评论。

S
Shubham Chaudhary

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

这是包含 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 方向滚动时,我在 public boolean onInterceptTouchEvent(MotionEvent ev) { return super.onInterceptTouchEvent(ev)&& mGestureDetector.onTouchEvent(ev) 处收到异常 NULL POINTER EXCEPTION;我在滚动视图中使用了水平滚动
@All 谢谢它的工作,我忘记在滚动视图构造函数中初始化手势检测器
我应该如何使用此代码在 ScrollView 中实现 ViewPager
当我在其中有一个始终展开的网格视图时,我在这段代码中遇到了一些问题,有时它不会滚动。我必须重写 YScrollDetector 中的 onDown(...) 方法,以始终按照文档中的建议返回 true(如此处 developer.android.com/training/custom-views/…)这解决了我的问题。
我刚刚遇到了一个值得一提的小错误。我相信 onInterceptTouchEvent 中的代码应该分开两个布尔调用,以保证 mGestureDetector.onTouchEvent(ev) 将被调用。就像现在一样,如果 super.onInterceptTouchEvent(ev) 为假,它将不会被调用。我刚刚遇到了这样一种情况,滚动视图中的可点击子项可以获取触摸事件,而 onScroll 根本不会被调用。否则,谢谢,很好的答案!
S
Suragch

感谢 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 移动比率如何,对子元素的任何触摸行为都开始不会被拦截。诡异的!
谢谢!还与 ListView 内的 ViewPager 一起使用,带有自定义 ListView。
刚刚用这个替换了接受的答案,它现在对我来说效果更好。谢谢!
@VipinSahu,要告诉触摸移动的方向,可以取当前X坐标和lastX的delta,如果大于0,则触摸从左向右移动,否则从右向左移动。然后将当前 X 保存为 lastX 以供下次计算。
水平滚动视图呢?
G
Giorgos Kylafas

我想我找到了一个更简单的解决方案,只有它使用 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;
    }
}

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

2013 年 12 月 10 日更新a post from Kirill Grouchnikov about the (then) Android Market app 中也描述了类似的方法。


boolean ret = super.onInterceptTouchEvent(ev);只为我返回错误。在滚动视图中使用 UninterceptableViewPager
这对我不起作用,尽管我喜欢它的简单性。我使用带有 UninterceptableViewPagerLinearLayoutScrollView。确实,ret 总是错误的......任何线索如何解决这个问题?
@scottyab & @Peterdk:嗯,我在一个 TableRow 里面,它在一个 TableLayout 里面,它在一个 ScrollView 里面(是的,我知道......),它按预期工作。也许您可以尝试覆盖 onScroll 而不是 onInterceptTouchEvent,例如 Google does it(第 1010 行)
我刚刚将 ret 硬编码为 true,它工作正常。它早些时候返回 false 但我相信这是因为滚动视图有一个线性布局,可以容纳所有的孩子
M
Marius Hilarious

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

你在通过什么适配器?
我再次检查并意识到它实际上不是我正在使用的 ScrollView,而是一个 ViewPager,我正在向它传递一个 FragmentStatePagerAdapter,其中包含画廊的所有图像。
S
Suragch

这对我来说效果不佳。我改变了它,现在它工作顺利。如果有人感兴趣。

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!
非常适合我。我在滚动视图中有一个自定义的“滑动解锁”视图,这给了我同样的麻烦。这个解决方案解决了这个问题。
S
Saqib

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

}

E
Ebrahim Byagowi

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


D
Don

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

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


R
Rishad Baniya

日期:2021 年 - 5 月 12 日

看起来很乱..但相信我,如果您想在垂直滚动视图中水平滚动任何视图黄油光滑,那么值得花时间!

通过制作自定义视图并扩展您想要水平滚动的视图,也可以在 jetpack compose 中工作;在垂直滚动视图中并在可组合的 AndroidView 中使用该自定义视图(现在,“Jetpack Compose 在 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)
    }
}

这里,ViewThatYouWannaScrollHorizontally 是您要水平滚动的视图,当您水平滚动时,您不希望垂直滚动条捕捉触摸并认为 “哦!用户正在垂直滚动所以 parent.requestDisallowInterceptTouchEvent(true) 基本上会说垂直滚动条 “嘿你!不要捕获任何触摸,因为用户正在水平滚动”

在用户完成水平滚动并尝试通过放置在垂直滚动条内的水平滚动条垂直滚动之后,它会看到 Y 轴上的触摸速度大于 X 轴,这表明用户没有水平滚动并且水平滚动的东西会说“嘿你!父母,你听到了吗?..用户正在垂直滚动我,现在你可以拦截触摸并在垂直滚动中显示我下方的东西”