ViewPager内部嵌套ViewPager

44

我想创建一个包含三个项目的ViewPager,其中每个项目的视图是另一个包含两个项目的ViewPager。用户可以像这样滑动项目:

ViewPager1[0] ViewPager2[0]
ViewPager1[0] ViewPager2[1]
ViewPager1[1] ViewPager2[0]
ViewPager1[1] ViewPager2[1]
ViewPager1[2] ViewPager2[0]
ViewPager1[2] ViewPager2[1]

那怎么可能呢?


4
由于通常情况下,无法在其他可滚动的元素中添加可滚动元素,因为它们会争夺触摸事件,所以这种方法不太可能奏效。 - CommonsWare
3
是的,这就是我问的原因 :)。 - xpepermint
我的回答有帮到你吗? - Shereef Marzouk
有很多事情你可以强制框架去做,但它们被称为反模式,应该避免使用。它们不具备未来的可扩展性。请重新考虑你的方法 :) - AZ_
请检查我的答案 https://dev59.com/aYPba4cB1Zd3GeqPoy8g#25800572 - Abhishek V
9个回答

59

在父ViewPager中覆盖canScroll:

@Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
   if(v != this && v instanceof ViewPager) {
      return true;
   }
   return super.canScroll(v, checkV, dx, x, y);
}

+1,谢谢你节省了我的时间。我需要相同的解决方案来滚动ViewPager中的Gallery。它做到了。 - Noundla Sandeep
谢谢。我使用了其中的一部分回答了自己的问题,其中我在ViewPager页面中有ScrollViews,在ScrollViews内部有一个ViewPager。如果有人需要这样做:https://dev59.com/umQn5IYBdhLWcg3wJ0a5#17069353 - egfconnor
10
可以的,如果子ViewPager在末尾,滚动父级。原文已经可以实现功能,但我想做成这样。 - tasomaniac

28

试试这个:

public class CustomViewPager extends ViewPager {
    private int childId;

    public CustomViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
     @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (childId > 0) {          
            ViewPager pager = (ViewPager)findViewById(childId);

            if (pager != null) {           
                pager.requestDisallowInterceptTouchEvent(true);
            }

        }

        return super.onInterceptTouchEvent(event);
    }

    public void setChildId(int id) {
        this.childId = id;
    }
}

8
掌握onInterceptTouchEvent方法是实现所有嵌套滚动视图的关键。 - Nuthatch
1
非常好用。如果您在xml中构建了您的视图,请确保将android.support.v4.view.ViewPager更改为->mypackage.CustomViewPager。唯一的问题是第一个触摸事件不会切换,所以... - MinceMan
难道不是必须传递父级(而不是子级)的pager id吗?此外,为了使其工作,我必须像这样获取pager: ViewPager pager =(ViewPager)((Activity)mContext).findViewById(mParentPagerId);否则非常感谢您,它为我节省了几个小时! - Simon
我在我的mainActivity中有一个viewPager,其中包含4个片段,并且我实现了customViewPager以禁用滑动,它运行良好。但是我的第一个片段内部也有一个viewPager,我也为其实现了customViewPager,但它仍然可以滑动?有什么想法吗?提前感谢! - ShahNewazKhan
1
通过为 Fragment 1 中的嵌套 viewPager 设置 setPagingEnabled(false) 解决了此问题。 - ShahNewazKhan

12

我花了很长时间才让一个ViewPager在另一个ViewPager中工作,并通过“Android Noob”在这里找到了解决方案。非常感谢!

我也想分享我的解决方案。我添加了一个功能,可以在达到内部ViewPager中最后一个(最右边)元素时将滑动管理切换到周围的ViewPager。为了防止故障,我还保存了最后一个元素的第一个滑动方向:即如果您首先向左滑动,则最小的向右滑动不会重置滚动状态。

public class GalleryViewPager extends ViewPager {

    /** the last x position */
    private float   lastX;

    /** if the first swipe was from left to right (->), dont listen to swipes from the right */
    private boolean slidingLeft;

    /** if the first swipe was from right to left (<-), dont listen to swipes from the left */
    private boolean slidingRight;

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

    public GalleryViewPager(final Context context) {
        super(context);
    }

    @Override
    public boolean onTouchEvent(final MotionEvent ev) {
        final int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:

                // Disallow parent ViewPager to intercept touch events.
                this.getParent().requestDisallowInterceptTouchEvent(true);

                // save the current x position
                this.lastX = ev.getX();

                break;

            case MotionEvent.ACTION_UP:
                // Allow parent ViewPager to intercept touch events.
                this.getParent().requestDisallowInterceptTouchEvent(false);

                // save the current x position
                this.lastX = ev.getX();

                // reset swipe actions
                this.slidingLeft = false;
                this.slidingRight = false;

                break;

            case MotionEvent.ACTION_MOVE:
                /*
                 * if this is the first item, scrolling from left to
                 * right should navigate in the surrounding ViewPager
                 */
                if (this.getCurrentItem() == 0) {
                    // swiping from left to right (->)?
                    if (this.lastX <= ev.getX() && !this.slidingRight) {
                        // make the parent touch interception active -> parent pager can swipe
                        this.getParent().requestDisallowInterceptTouchEvent(false);
                    } else {
                        /*
                         * if the first swipe was from right to left, dont listen to swipes
                         * from left to right. this fixes glitches where the user first swipes
                         * right, then left and the scrolling state gets reset
                         */
                        this.slidingRight = true;

                        // save the current x position
                        this.lastX = ev.getX();
                        this.getParent().requestDisallowInterceptTouchEvent(true);
                    }
                } else
                /*
                 * if this is the last item, scrolling from right to
                 * left should navigate in the surrounding ViewPager
                 */
                if (this.getCurrentItem() == this.getAdapter().getCount() - 1) {
                    // swiping from right to left (<-)?
                    if (this.lastX >= ev.getX() && !this.slidingLeft) {
                        // make the parent touch interception active -> parent pager can swipe
                        this.getParent().requestDisallowInterceptTouchEvent(false);
                    } else {
                        /*
                         * if the first swipe was from left to right, dont listen to swipes
                         * from right to left. this fixes glitches where the user first swipes
                         * left, then right and the scrolling state gets reset
                         */
                        this.slidingLeft = true;

                        // save the current x position
                        this.lastX = ev.getX();
                        this.getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }

                break;
        }

        super.onTouchEvent(ev);
        return true;
    }

}

希望这能对未来的某个人有所帮助!


我该如何为ViewPager页面内的项目设置OnClickListener?现在设置一个会禁用ViewPager的所有滑动。 - egfconnor
我曾经遇到过同样的问题,并尝试在ViewPager内部创建自定义点击监听器。这是我的做法:我在ACTION_DOWN事件中保存了x和y坐标,然后在移动时检查当前的x或y值是否与起始位置相差太远。如果是,我将一个布尔值设置为false。在ACTION_UP事件中,我会检查该布尔值,如果为true,则表示点击,如果为false,则表示滚动。如果你有其他解决方案,我很乐意听取 :) - gtRfnkN
嗨,我也有类似的问题。我在另一个视图页面中有一个视图页面。为了启用子视图页面中的页面更改,我使用了上面的代码,它运行良好。但是,我在子视图页面中有一个按钮。如果用户尝试通过触摸按钮来滑动子视图页面,则父视图页面会更改而不是子视图页面。请给我一些建议。 - Raj

12
如果子ViewPager在结尾处,滚动父ViewPager。
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
    if(v != this && v instanceof ViewPager) {
        int currentItem = ((ViewPager) v).getCurrentItem();
        int countItem = ((ViewPager) v).getAdapter().getCount();
        if((currentItem==(countItem-1) && dx<0) || (currentItem==0 && dx>0)){
            return false;
        }
        return true;
    }
    return super.canScroll(v, checkV, dx, x, y);
}

9

1
这对于我来说是解决ViewPager2内可滚动元素的完美方案。谢谢@Westy92。 - Prashant Jajal
1
这是处理嵌套滚动的最佳方式<3,谢谢。 - M.Fakhri
1
你救了我免于战争。 - Bitwise DEVS
这是一个相当不错的解决方案,但在内部viewpager滑动时仍存在渲染问题,如果有3个项目,则始终跳过第2个项目,并且滚动跳转到第1个和第3个项目。 - Priya Sindkar
当与https://github.com/ongakuer/CircleIndicator一起使用时 - Priya Sindkar
1
将CircleIndicator与NestedScrollableHost包装现在可以正常工作,没有问题。 - Priya Sindkar

5

首先按照以下方式创建自定义ViewPager类:

public class CustomViewPager extends ViewPager {

    public CustomViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if(v instanceof ViewPager) {
            return true;
        }
        return super.canScroll(v, checkV, dx, x, y);
    }
}

方法返回(布尔值)canScroll 将告诉您 ViewPager 更改页面所需的水平手势是否需要在片段的右侧或左侧边界(true),或者是否适用于完整的片段屏幕(false)。 例如,如果您只希望第一个片段使用右边界移动到下一个片段,因为第一个片段具有另一个水平滚动事件,则可通过以下代码覆盖 canScroll 方法:

@Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
    if(v instanceof ViewPager) {
        int currentItem = ((ViewPager) v).getCurrentItem();
        if((currentItem==0)){
            return true;
        }
        return false;
    }
    return super.canScroll(v, checkV, dx, x, y);
}

最后一步是在主类中使用你的CustomViewPager类:
ViewPager myPager= (CustomViewPager)myContext.findViewById(R.id.myCustomViewPager);

还有XML:

<my.cool.package.name.CustomViewPager
        android:id="@+id/myCustomViewPager"
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

0
我不明白为什么你不只创建一个 view pager,并将实例化项逻辑用于从不同的源获取数据,这将使你达到同样的目标。我没有看到需要两个 viewpagers 的情况。
示例
ViewPager1[0] ViewPager2[0] = page 0  (position/2) = 0
ViewPager1[0] ViewPager2[1] = page 1  ((position-1)/2) = 0
ViewPager1[1] ViewPager2[0] = page 2  (position/2) = 1
ViewPager1[1] ViewPager2[1] = page 3 ((position-1)/2) = 1
ViewPager1[2] ViewPager2[0] = page 4  (position/2) = 2
ViewPager1[2] ViewPager2[1] = page 5 ((position-1)/2) = 2

在代码中:

@Override
public Object instantiateItem(View collection, int position) {
    LayoutInflater inflater = THISCLASSNAME.this.getLayoutInflater();
    View v = null;
    if(position%2 == 0) {
         // viewpager 1 code
         int vp1pos = position/2;
         v = inlater.inflate(R.layout.somelayout, collection, false);
         Button b = (Button)v.findViewById(R.id.somebutton);
         b.setText(array1[vp1pos]);
    } else {
         int vp2pos = (position-1)/2;
         v = inlater.inflate(R.layout.somelayout, collection, false);
         Button b = (Button)v.findViewById(R.id.somebutton);
         b.setText(array2[vp2pos]);
    }

    ((DirectionalViewPager) collection).addView(v, 0);

    return v;
}

这样你就可以拥有实际上的两个 viewpagers 逻辑,你可以根据需要进行更多的自定义,我只是给你提供了一些想法。

P.S. 我在这里编写代码,如果有字符大小写错误或拼写错误,请原谅。

希望这可以帮助到你,如果你有更具体的需求并需要更多帮助,请在我的答案下添加评论,我会进行修改。


0

我刚刚测试了这个案例,你可以不需要额外的工作就能完成它,以下是我的演示

public class MainActivity extends AppCompatActivity {

    public static final String TAG = "TAG";
    ViewPager parentPager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        initViews();
        initData();

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
    }

    private void initViews() {
        parentPager = (ViewPager) findViewById(R.id.parent_pager);
    }

    private void initData() {
        List<ViewPager> pagers = new ArrayList<ViewPager>();
        for(int j = 0; j < 3; j++) {
            List<LinearLayout> list = new ArrayList<LinearLayout>();
            for (int i = 0; i < 5; i++) {
                LinearLayout layout = new LinearLayout(this);
                TextView textView = new TextView(this);
                textView.setText("This is the" + i + "th page in PagerItem" + j);
                layout.addView(textView);
                textView.setGravity(Gravity.CENTER);
                LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) textView.getLayoutParams();
                params.gravity = Gravity.CENTER;
                list.add(layout);
            }
            MyViewPagerAdapter adapter = new MyViewPagerAdapter(list);
            final ViewPager childPager = (ViewPager) LayoutInflater.from(this).inflate(R.layout.child_layout, null).findViewById(R.id.child_pager);
            childPager.setAdapter(adapter);
            childPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
                @Override
                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                    Log.d(TAG, "onPageScrolled: position: " + position + ",   positionOffset: " + positionOffset);
                }

                @Override
                public void onPageSelected(int position) {

                }

                @Override
                public void onPageScrollStateChanged(int state) {

                }
            });
            pagers.add(childPager);
        }
        MyParentViewPagerAdapter parentAdapter = new MyParentViewPagerAdapter(pagers);
        parentPager.setAdapter(parentAdapter);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    class MyViewPagerAdapter extends PagerAdapter {

        private List<LinearLayout> data;

        public MyViewPagerAdapter(List<LinearLayout> data) {
            this.data = data;
        }

        @Override
        public int getCount() {
            return data.size();
        }

        @Override
        public int getItemPosition(Object object) {
            return data.indexOf(object);
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            LinearLayout linearLayout = data.get(position);
            container.addView(linearLayout);
            return data.get(position);
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            LinearLayout layout = data.get(position);
            container.removeView(layout);
            layout = null;
        }

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

    class MyParentViewPagerAdapter extends PagerAdapter {

        private List<ViewPager> data;

        public MyParentViewPagerAdapter(List<ViewPager> data) {
            this.data = data;
        }

        @Override
        public int getCount() {
            return data.size();
        }

        @Override
        public int getItemPosition(Object object) {
            return data.indexOf(object);
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            ViewPager pager = data.get(position);
            if(pager.getParent() != null) {
                ((ViewGroup) pager.getParent()).removeView(pager);
            }
            container.addView(pager);
            return data.get(position);
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            ViewPager pager = data.get(position);
            container.removeView(pager);
            pager = null;
        }

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

这个 XML 很简单,在我的主布局中有一个外部的 ViewPager,而内部的 ViewPager 则在另一个 LinearLayout 中。


0
我通过创建两个自定义的ViewPager继承者来解决这个任务。在我的情况下,它们是OuterViewPager和InnerViewPager。
public class InnerViewPager extends ViewPager
{
    private int mPrevMoveX;

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

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        switch (event.getAction())
        {
            case MotionEvent.ACTION_DOWN:
                mPrevMoveX = (int) event.getX();

                return super.onTouchEvent(event);

            case MotionEvent.ACTION_MOVE:
                int distanceX = mPrevMoveX - (int) event.getX();
                mPrevMoveX = (int) event.getX();

                boolean canScrollLeft = true;
                boolean canScrollRight = true;

                if(getCurrentItem() == getAdapter().getCount() - 1)
                {
                    canScrollLeft = false;
                }

                if(getCurrentItem() == 0)
                {
                    canScrollRight = false;
                }

                if(distanceX > 0)
                {
                    return canScrollRight;
                }
                else
                {
                    return canScrollLeft;
                }
        }

        return super.onInterceptTouchEvent(event);
    }

    public boolean onTouchEvent(MotionEvent event)
    {
        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                mPrevMoveX = (int) event.getX();

                return super.onTouchEvent(event);

            case MotionEvent.ACTION_MOVE:
                int distanceX = mPrevMoveX - (int) event.getX();
                mPrevMoveX = (int) event.getX();

                boolean canScrollLeft = true;
                boolean canScrollRight = true;

                if(getCurrentItem() == getAdapter().getCount() - 1)
                {
                    canScrollLeft = false;
                }

                if(getCurrentItem() == 0)
                {
                    canScrollRight = false;
                }

                if(distanceX > 0)
                {
                    super.onTouchEvent(event);
                    return canScrollLeft;
                }
                else
                {
                    super.onTouchEvent(event);
                    return canScrollRight;
                }
        }

        return super.onTouchEvent(event);
    }
}


public class OuterViewPager extends ViewPager
{
    private int mPrevMoveX;


    public OuterViewPager(Context context)
    {
        super(context);
        init();
    }

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

    private void init()
    {
        setOnPageChangeListener(new CustomPageChangeListener());
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev)
    {
        switch (ev.getAction())
        {
            case MotionEvent.ACTION_DOWN:
                mPrevMoveX = (int) ev.getX();
                return super.onInterceptTouchEvent(ev);

            case MotionEvent.ACTION_MOVE:

                /*there you should get currentInnerPager - instance of InnerPager on current page of instance of OuterPager*/

                int distanceX = mPrevMoveX - (int) ev.getX();
                mPrevMoveX = (int) ev.getX();

                boolean canScrollLeft = true;
                boolean canScrollRight = true;

                if(currentInnerPager.getCurrentItem() == currentInnerPager.getAdapter().getCount() - 1)
                {
                    canScrollLeft = false;
                }

                if(currentInnerPager.getCurrentItem() == 0)
                {
                    canScrollRight = false;
                }

                if(distanceX > 0)
                {
                    return !canScrollLeft;
                }
                else
                {
                    return !canScrollRight;
                }
        }

        return super.onInterceptTouchEvent(ev);
    }
}

当内部分页器在最后一页时,外部分页器开始向左滚动。反之亦然。


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