ScrollView内部的ViewPager无法正确滚动。

91

我有一个“页面”,上面有许多组件,其内容比设备的高度更长。没问题,只需要将整个布局(整个页面)放在ScrollView中即可。

其中一个组件是ViewPager。它可以正确地呈现,但滑动或飞快切换的响应不正确,会出现抖动并且不能总是正常工作。它似乎与ScrollView混淆了,因此仅在您以完全水平线飞快切换时才能100%正常工作。

如果我删除ScrollView,ViewPager就可以完美响应。

我搜索过并没有发现这是已知缺陷。还有其他人经历过这种情况吗?

  • 平台版本:1.6
  • 兼容性库v4.
  • 设备:HTC Incredible S

以下是一些示例代码供您测试,注释掉ScrollView以查看它的工作原理。

活动:

package com.ss.activities;

import com.ss.R;

import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.View;
import android.widget.TextView;

public class PagerInsideScollViewActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ViewPager vp = (ViewPager) findViewById(R.id.viewpager);
        vp.setAdapter(new MyPagerAdapter(this));
    }
}

class MyPagerAdapter extends PagerAdapter {

    private Context ctx;

    public MyPagerAdapter(Context context) {
        ctx = context;
    }

    @Override
    public int getCount() {
        return 2;
    }

    @Override
    public Object instantiateItem(View collection, int position) {

        TextView tv =  new TextView(ctx);
        tv.setTextSize(50);
        tv.setTextColor(Color.WHITE);
        tv.setText("SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, " +
                "SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, " +
                "SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, " +
                "SMILE DUDE, SMILE DUDE, SMILE DUDE");

        ((ViewPager) collection).addView(tv);

        return tv;

    }

    @Override
    public void destroyItem(View collection, int position, Object view) {
         ((ViewPager) collection).removeView((View) view);
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }

    @Override
    public Parcelable saveState() {
        return null;
    }

    @Override
    public void restoreState(Parcelable arg0, ClassLoader arg1) {
    }

    @Override
    public void startUpdate(View arg0) {
    }

    @Override
    public void finishUpdate(View arg0) {
    }
}

布局:

<?xml version="1.0" encoding="utf-8"?>
    <ScrollView
         xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >

        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:orientation="vertical" >

            <android.support.v4.view.ViewPager
                android:id="@+id/viewpager"
                android:layout_width="fill_parent"
                android:layout_height="300dp" />

        </LinearLayout>

    </ScrollView>

请在此处查看答案:https://dev59.com/Jmox5IYBdhLWcg3wsGWC#31440577 - Andrew Dmytrenko
请在此链接中查看我的答案:https://dev59.com/Ims05IYBdhLWcg3wNPM3#43987753 - Geral Alexander Torres
9个回答

62

我曾经遇到过同样的问题。我的解决方法是在ViewPager开始滚动时调用requestDisallowInterceptTouchEvent函数。

以下是我的代码:

pager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        v.getParent().requestDisallowInterceptTouchEvent(true);
        return false;
    }
});

pager.setOnPageChangeListener(new SimpleOnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        pager.getParent().requestDisallowInterceptTouchEvent(true);
    }
});

1
这基本上能运作。即使触摸是在视图翻页器上开始的,我也希望滚动视图垂直移动,如果移动“主要”是垂直的。 - Nemanja Kovacevic
3
需要在onTouch和onPageScrolled两个方法中都使用requestDisallowInterceptTouchEvent吗?这样做是否过度了?我们能否只在onPageScrollStateChanged方法中使用requestDisallowInterceptTouchEvent来获得相同的结果? - craigrs84
1
我有同样的问题,并找到了这个最佳解决方案,希望这对你有用。https://bitbucket.org/NxAlex/swipes-navigation-demo/src/4db50c3253637185f9b3610fff6f1a9401fd25eb/sample/SwipeNavigationDemo/src/com/visionapps/demo/swipe/view/SmartViewPager.java?at=master - Jitendra Nath
我该如何仍然覆盖onClick事件?它与onTouch冲突了吗? - Kiddo
请使用SimpleOnPageChangeListener。 - Peter
4
这里的pager和mpager是什么?我需要提供什么样的输入? - Sujithrao

31

进一步阅读发现,滚动组件中存在滚动组件的问题。

其中一种解决方案是在包含可滚动组件(在我这里是ViewPager)的区域内“禁用”ScrollView的垂直滚动。

此解决方案的代码详见此处(非常简洁明了!)


那个页面中我应该参考哪个解决方案? - Harsha M V
1
requestDisallowInterceptTouchEvent 是你要找的。 - yahya
2
我有同样的问题,并找到了这个最佳解决方案,希望这对你有用。https://bitbucket.org/NxAlex/swipes-navigation-demo/src/4db50c3253637185f9b3610fff6f1a9401fd25eb/sample/SwipeNavigationDemo/src/com/visionapps/demo/swipe/view/SmartViewPager.java?at=master - Jitendra Nath
感谢提供的链接,但是我在那里找到的解决方案在ViewPager中不能完美地水平滑动,所以我对其进行了一些修改以使其正常工作:https://dev59.com/bGsy5IYBdhLWcg3wsQFj#33696740 - Dmide

15

这里有一个解决方案:

    mPager.setOnTouchListener(new View.OnTouchListener() {

        int dragthreshold = 30;
        int downX;
        int downY;

        @Override
        public boolean onTouch(View v, MotionEvent event) {

            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = (int) event.getRawX();
                downY = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int distanceX = Math.abs((int) event.getRawX() - downX);
                int distanceY = Math.abs((int) event.getRawY() - downY);

                if (distanceY > distanceX && distanceY > dragthreshold) {
                    mPager.getParent().requestDisallowInterceptTouchEvent(false);
                    mScrollView.getParent().requestDisallowInterceptTouchEvent(true);
                } else if (distanceX > distanceY && distanceX > dragthreshold) {
                    mPager.getParent().requestDisallowInterceptTouchEvent(true);
                    mScrollView.getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                mScrollView.getParent().requestDisallowInterceptTouchEvent(false);
                mPager.getParent().requestDisallowInterceptTouchEvent(false);
                break;
            }
            return false;
        }
    });

当按下时,基本上设置X、Y值,并在拖动时计算距离,以确定我们想要前往的方向。尝试调整dragthreshold以针对你的情况进行优化。


我曾经遇到过同样的问题,这是唯一可行的解决方案。干得好... :) - rawcoder064
这对我来说是最好的解决方案。它不会干扰ScrollView的上下滑动。 - Lei Guo
这对于我来说是最佳解决方案。此外,我们可以在该解决方案中配置阈值选项。 - Vinayak
不适用于我,因为滚动视图在到达此取消语句之前拦截了触摸。 - AdrianoCelentano
干得好!如果你想在Wrapcontentviewpager中使用它,那么它也可以很好地工作。 - Silvans Solanki
当我使用日志时,我发现它总是进入下行和上行块。 - elifekiz

4
使用ViewPager,您可以捕获页面更改事件并防止ScrollView拦截导致页面更改的触摸事件。 这很简单,使用ViewGroup.requestDisallowInterceptTouchEvent(boolean)。它还允许用户拖动ScrollView,即使他们在ViewPager上开始拖动,但水平拖动pager仍将正常工作,而不会受到ScrollView的干扰。
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // Add android:id for your ScrollView in your layout
        final ScrollView sv = (ScrollView) findViewById(R.id.scrollview);
        final ViewPager vp = (ViewPager) findViewById(R.id.viewpager);
        vp.setAdapter(new MyPagerAdapter(this));

        // Use a page-change listener to respond to begin-drag events:
        vp.setOnPageChangeListener(new OnPageChangeListener() {
            @Override
            public void onPageSelected(int newState) {
                if (newState == ViewPager.SCROLL_STATE_DRAGGING) {
                    // Prevent the ScrollView from intercepting this event now that the page is changing.
                    // When this drag ends, the ScrollView will start accepting touch events again.
                    sv.requestDisallowInterceptTouchEvent(true);
                }
            }

            @Override
            public void onPageScrolled(int arg0, float arg1, int arg2) {
            }

            @Override
            public void onPageScrollStateChanged(int arg0) {
            }
        });

    }

这对我在Android 2.3.4和4.2.1上使用Android Support v4库有效。

1
应该是onPageScrollStateChanged而不是onPageSelected吗? - craigrs84

2

我从@Michael Herbig那里改编了这个解决方案。它的好处是它适用于任何允许使用setOnTouchListener的视图,不仅仅是ViewPager(例如ViewPagerIndicator),并且它是一个独立的类。

示例用法:

// runStatsPager is a android.support.v4.view.ViewPager;
runStatsPager.setOnTouchListener(new ViewInScrollViewTouchHelper(runStatsPager));

// runStatsPagerIndicator is a com.viewpagerindicator.TitlePageIndicator
runStatsPagerIndicator.setOnTouchListener(new ViewInScrollViewTouchHelper(runStatsPagerIndicator));

而且这个类:

class ViewInScrollViewTouchHelper implements View.OnTouchListener {

    private final ScrollView scrollView;
    private final View viewInScrollView;
    int dragthreshold = 30;
    int downX;
    int downY;

    public ViewInScrollViewTouchHelper(View viewInScrollView) {

        if (viewInScrollView == null) {
            throw new IllegalArgumentException("viewInScrollView cannot be null.");
        }

        ViewParent parent = viewInScrollView.getParent();
        ScrollView scrollView = null;
        do {
            if (parent instanceof ScrollView) {
                scrollView = (ScrollView) parent;
                break;
            }
        } while(parent != null && (parent = parent.getParent()) != null);

        if (scrollView == null) {
            throw new IllegalArgumentException("View does not have a ScrollView in its parent hierarchy.");
        }

        this.scrollView = scrollView;
        this.viewInScrollView = viewInScrollView;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = (int) event.getRawX();
                downY = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int distanceX = Math.abs((int) event.getRawX() - downX);
                int distanceY = Math.abs((int) event.getRawY() - downY);

                if (distanceY > distanceX && distanceY > dragthreshold) {
                    viewInScrollView.getParent().requestDisallowInterceptTouchEvent(false);
                    scrollView.getParent().requestDisallowInterceptTouchEvent(true);
                } else if (distanceX > distanceY && distanceX > dragthreshold) {
                    viewInScrollView.getParent().requestDisallowInterceptTouchEvent(true);
                    scrollView.getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                scrollView.getParent().requestDisallowInterceptTouchEvent(false);
                viewInScrollView.getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        return false;
    }
}

1
public class WrapContentHeightViewPager extends ViewPager {

public WrapContentHeightViewPager(Context context) {
    super(context);
}

public WrapContentHeightViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int height = 0;
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        child.measure(widthMeasureSpec, View.MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));

        int h = child.getMeasuredHeight();
        if (h > height) height = h;
    }

    heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

}

@Override public boolean onTouch(View v, MotionEvent event) { // 在此添加触摸事件处理逻辑 }

    int dragthreshold = 30;
    int downX = 0;
    int downY = 0;

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downX = (int) event.getRawX();
            downY = (int) event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            int distanceX = Math.abs((int) event.getRawX() - downX);
            int distanceY = Math.abs((int) event.getRawY() - downY);

            if (distanceY > distanceX && distanceY > dragthreshold) {
                mViewPager.getParent().requestDisallowInterceptTouchEvent(false);
                mScrollView.getParent().requestDisallowInterceptTouchEvent(true);
            } else if (distanceX > distanceY && distanceX > dragthreshold) {
                mViewPager.getParent().requestDisallowInterceptTouchEvent(true);
                mScrollView.getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            mScrollView.getParent().requestDisallowInterceptTouchEvent(false);
            mViewPager.getParent().requestDisallowInterceptTouchEvent(false);
            break;
    }
    return false;

}

使用上述两个方法,它们可以很好地发挥作用。我也在我的代码中使用了它们。

1
因此,采用这种方法,我让ViewPagerX方向上滚动,而不必担心ScrollView(父级)窃取事件并取消当前滚动。更重要的是,这也使得ViewPager所在的区域可以在Y方向上滚动。
public class CustomViewPager extends ViewPager {

private GestureDetector     mGestureDetector;
View.OnTouchListener        mGestureListener;

public CustomViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
    mGestureDetector = new GestureDetector(context, new Detector());
}

@Override
public boolean onTouchEvent(MotionEvent motionEvent) {

    mGestureDetector.onTouchEvent(motionEvent);
    return super.onTouchEvent(motionEvent);
}

class Detector extends SimpleOnGestureListener {

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {

        requestDisallowInterceptTouchEvent(true);

        return false;
    }
}
}

有点不幸的是在这里使用了子类化ViewPager。我想这可以改为View.OnTouchListener。我先点赞了! - Jon Willis

0

对我来说,这里的解决方案都不太好,因为它们要么修改ViewPager的触摸逻辑,要么修改ScrollView的逻辑。我必须同时实现两个,现在它完美地工作了。

public class TouchGreedyViewPager extends ViewPager {
    private float xDistance, yDistance, lastX, lastY;

    public TouchGreedyViewPager(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) / 3; // favor X events
                lastX = curX;
                lastY = curY;
                if (xDistance > yDistance) {
                    return true;
                }
        }

        return super.onInterceptTouchEvent(ev);
    }
}

public class TouchHumbleScrollView extends ScrollView {
    private float xDistance, yDistance, lastX, lastY;

    public TouchHumbleScrollView(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) / 3; // favor X events
                lastX = curX;
                lastY = curY;
                if (xDistance > yDistance) {
                    return false;
                }
        }

        return super.onInterceptTouchEvent(ev);
    }
}

-1
您可以通过在 PagerAdapter 类中覆盖 instantiateItem 方法并向每个页面添加一个 scrollView 来实现。以下是代码:
@Override
public Object instantiateItem(View collection, int position) {
    ScrollView sc = new ScrollView(cxt);
    sc.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
    sc.setFillViewport(true);
    TextView tv = new TextView(cxt);
    tv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
    tv.setText(pages[position]);
    tv.setPadding(5,5,5,5);         
    sc.addView(tv);
    ((ViewPager) collection).addView(sc);

    return sc;
    }

请在此总结或引用最重要的要点。链接腐败会发生。 - ЯegDwight

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接