安卓:垂直ViewPager

136

有没有一种方法可以创建一个不会水平滚动而是垂直滚动的 ViewPager?!


1
有一个新的非官方开源实现垂直ViewPager:公告:https://plus.google.com/107777704046743444096/posts/1FTJfrvnY8w 代码:https://github.com/LambergaR/VerticalViewPager/ - Vasily Sochinsky
Antoine Merle的垂直视图翻页器实现:https://github.com/castorflex/VerticalViewPager - micnoy
有一个基于19支持库的新实现:https://github.com/castorflex/VerticalViewPager - Vasily Sochinsky
这与 https://github.com/castorflex/VerticalViewPager 不同,尽管名称相似。 - Flimm
1
这里有一个名为ViewPager2的控件,可以在此处查看演示:https://dev59.com/eVQJ5IYBdhLWcg3wERqZ#54643817 - AskNilesh
15个回答

231

您可以使用ViewPager.PageTransformer来模拟垂直的 ViewPager。为了实现用垂直而不是水平拖动进行滚动,您需要覆盖 ViewPager 的默认触摸事件,并在处理它们之前交换 MotionEvent 的坐标,例如:

/**
 * Uses a combination of a PageTransformer and swapping X & Y coordinates
 * of touch events to create the illusion of a vertically scrolling ViewPager. 
 * 
 * Requires API 11+
 * 
 */
public class VerticalViewPager extends ViewPager {

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

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

    private void init() {
        // The majority of the magic happens here
        setPageTransformer(true, new VerticalPageTransformer());
        // The easiest way to get rid of the overscroll drawing that happens on the left and right
        setOverScrollMode(OVER_SCROLL_NEVER);
    }

    private class VerticalPageTransformer implements ViewPager.PageTransformer {

        @Override
        public void transformPage(View view, float position) {

            if (position < -1) { // [-Infinity,-1)
                // This page is way off-screen to the left.
                view.setAlpha(0);

            } else if (position <= 1) { // [-1,1]
                view.setAlpha(1);

                // Counteract the default slide transition
                view.setTranslationX(view.getWidth() * -position);

                //set Y position to swipe in from top
                float yPosition = position * view.getHeight();
                view.setTranslationY(yPosition);

            } else { // (1,+Infinity]
                // This page is way off-screen to the right.
                view.setAlpha(0);
            }
        }
    }

    /**
     * Swaps the X and Y coordinates of your touch event.
     */
    private MotionEvent swapXY(MotionEvent ev) {
        float width = getWidth();
        float height = getHeight();

        float newX = (ev.getY() / height) * width;
        float newY = (ev.getX() / width) * height;

        ev.setLocation(newX, newY);

        return ev;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev){
        boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
        swapXY(ev); // return touch coordinates to original reference frame for any child views
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(swapXY(ev));
    }

}

当然,您可以根据自己的需求调整这些设置。 最终看起来会像这样:

VerticalViewPager演示


4
@Der Golem这个怎样使用布局? - bShah
当宽度未设置为父元素的宽度时,这个例子会出现问题吗? - Matthias Van Parijs
1
@Brett 在这个例子中,我想在左右滑动时执行一些操作。怎么做? - Prabs
1
@Brett,它按预期工作,转换是垂直进行的。但问题是我必须向右/向左滑动才能进行转换,在向上/向下滑动时没有转换发生。 - Karan Khurana
3
@Brett,我一直在使用你的解决方案。但是现在在安卓9.0系统设备上出现了滑动问题。有没有其他人遇到同样的问题? - Jishant
显示剩余19条评论

27

垂直ViewPager

这里其他的答案都有一些问题。即使是推荐的开源项目也没有合并最近的拉取请求和错误修复。然后我发现了一个VerticalViewPager,来自谷歌他们在一些桌面时钟应用程序中使用。我相信使用谷歌的实现比在这里流传的其他答案更可靠。(谷歌的版本类似于当前排名第一的答案,但更为全面。)

完整示例

这个示例与我的基本ViewPager示例几乎完全相同,只有一些小的改动来利用VerticalViewPager类。

enter image description here

XML

添加主活动和每个页面(片段)的xml布局。请注意,我们使用VerticalViewPager而不是标准的ViewPager。我将在下面包含代码。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.verticalviewpager.MainActivity">

    <com.example.myapp.VerticalViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

fragment_one.xml

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

    <TextView
        android:id="@+id/textview"
        android:textSize="30sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

</RelativeLayout>

代码

这个VerticalViewPager的代码不是我的,只是从谷歌源代码中剥离出来的精简版(没有注释)。查看它以获取最新版本。那里的注释也有助于理解正在发生的事情。

VerticalViewPager.java

import android.support.v4.view.ViewPager;    

public class VerticalViewPager extends ViewPager {

    public VerticalViewPager(Context context) {
        this(context, null);
    }

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

    @Override
    public boolean canScrollHorizontally(int direction) {
        return false;
    }

    @Override
    public boolean canScrollVertically(int direction) {
        return super.canScrollHorizontally(direction);
    }

    private void init() {
        setPageTransformer(true, new VerticalPageTransformer());
        setOverScrollMode(View.OVER_SCROLL_NEVER);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final boolean toIntercept = super.onInterceptTouchEvent(flipXY(ev));
        flipXY(ev);
        return toIntercept;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final boolean toHandle = super.onTouchEvent(flipXY(ev));
        flipXY(ev);
        return toHandle;
    }

    private MotionEvent flipXY(MotionEvent ev) {
        final float width = getWidth();
        final float height = getHeight();
        final float x = (ev.getY() / height) * width;
        final float y = (ev.getX() / width) * height;
        ev.setLocation(x, y);
        return ev;
    }

    private static final class VerticalPageTransformer implements ViewPager.PageTransformer {
        @Override
        public void transformPage(View view, float position) {
            final int pageWidth = view.getWidth();
            final int pageHeight = view.getHeight();
            if (position < -1) {
                view.setAlpha(0);
            } else if (position <= 1) {
                view.setAlpha(1);
                view.setTranslationX(pageWidth * -position);
                float yPosition = position * pageHeight;
                view.setTranslationY(yPosition);
            } else {
                view.setAlpha(0);
            }
        }
    }
}

MainActivity.java

我只是将 VerticalViewPager 替换成了 ViewPager

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;

public class MainActivity extends AppCompatActivity {

    static final int NUMBER_OF_PAGES = 2;

    MyAdapter mAdapter;
    VerticalViewPager mPager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mAdapter = new MyAdapter(getSupportFragmentManager());
        mPager = findViewById(R.id.viewpager);
        mPager.setAdapter(mAdapter);
    }

    public static class MyAdapter extends FragmentPagerAdapter {
        public MyAdapter(FragmentManager fm) {
            super(fm);
        }

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

        @Override
        public Fragment getItem(int position) {

            switch (position) {
                case 0:
                    return FragmentOne.newInstance(0, Color.WHITE);
                case 1:
                    // return a different Fragment class here
                    // if you want want a completely different layout
                    return FragmentOne.newInstance(1, Color.CYAN);
                default:
                    return null;
            }
        }
    }

    public static class FragmentOne extends Fragment {

        private static final String MY_NUM_KEY = "num";
        private static final String MY_COLOR_KEY = "color";

        private int mNum;
        private int mColor;

        // You can modify the parameters to pass in whatever you want
        static FragmentOne newInstance(int num, int color) {
            FragmentOne f = new FragmentOne();
            Bundle args = new Bundle();
            args.putInt(MY_NUM_KEY, num);
            args.putInt(MY_COLOR_KEY, color);
            f.setArguments(args);
            return f;
        }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mNum = getArguments() != null ? getArguments().getInt(MY_NUM_KEY) : 0;
            mColor = getArguments() != null ? getArguments().getInt(MY_COLOR_KEY) : Color.BLACK;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            View v = inflater.inflate(R.layout.fragment_one, container, false);
            v.setBackgroundColor(mColor);
            TextView textView = v.findViewById(R.id.textview);
            textView.setText("Page " + mNum);
            return v;
        }
    }
}

完成

您应该能够运行应用程序并在上面的动画中看到结果。

另请参阅


3
代码运行正常,只需要一个小帮助。当前代码中,我们需要从底部向上滑动才能获取下一页,但我希望即使是小的滑动也可以获取下一页。 - AMIT
@AMIT,你找到解决方法了吗?我也遇到了同样的问题。 - Yupi
我们也可以添加一个TabHost吗? - Syed Arsalan Kazmi

21

我有一个适用于我的两步解决方案。

  1. PagerAdapteronInstantiateItem()中,创建视图并将其旋转-90度:

view.setRotation(-90f)

如果您正在使用FragmentPagerAdapter,那么:

objFragment.getView().setRotation(-90)
  • ViewPager 视图旋转90度:

    objViewPager.setRotation(90)
    
  • 至少对我的要求来说效果很好。
    1. Works like a charm at least for my requirement.


    4
    这个解决方案有效,但你仍然需要水平滑动,而viewPager现在是垂直滚动。 - leochab
    @Kulai,我不太清楚在这里该怎么做,请你能否稍微解释一下?根据我的理解,我们需要在public Object instantiateItem(ViewGroup container, int position)中执行container.setRotation(-90f)。 - varun
    1
    这样做起来非常顺利!尽管视图在顶部和底部被裁剪,但我无法弄清原因 :-/ - Nivaldo Bondança
    2
    对于正方形图像,它可以正常工作,但对于长方形图像则不行。当矩形被旋转时,尺寸会出错。 - Kulai
    2
    我不确定是谁点赞了这个回答。这不是VerticalViewPager,而是将ViewPager旋转90度后的效果,手势和过渡效果完全相反。这并不自然。 - droidev

    15

    如果有人在尝试让垂直的ViewPager在横向布局中正常工作,只需按照以下步骤进行:

    垂直ViewPager

    public class VerticalViewPager extends ViewPager {
        public VerticalViewPager(Context context) {
            super(context);
            init();
        }
    
        public VerticalViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        private void init() {
            setPageTransformer(true, new VerticalPageTransformer());
            setOverScrollMode(OVER_SCROLL_NEVER);
        }
    
        private class VerticalPageTransformer implements ViewPager.PageTransformer {
    
            @Override
            public void transformPage(View view, float position) {
    
                if (position < -1) {
                    view.setAlpha(0);
                } else if (position <= 1) {
                    view.setAlpha(1);
    
                    view.setTranslationX(view.getWidth() * -position);
    
                    float yPosition = position * view.getHeight();
                    view.setTranslationY(yPosition);
                } else {
                    view.setAlpha(0);
                }
            }
        }
    
        private MotionEvent swapXY(MotionEvent ev) {
            float width = getWidth();
            float height = getHeight();
    
            float newX = (ev.getY() / height) * width;
            float newY = (ev.getX() / width) * height;
    
            ev.setLocation(newX, newY);
    
            return ev;
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
            swapXY(ev);
            return intercepted;
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            return super.onTouchEvent(swapXY(ev));
        }
    
    }
    

    水平ViewPager

    public class HorizontalViewPager extends ViewPager {
        private GestureDetector xScrollDetector;
    
        public HorizontalViewPager(Context context) {
            super(context);
        }
    
        public HorizontalViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
            xScrollDetector = new GestureDetector(getContext(), new XScrollDetector());
        }
    
        class XScrollDetector extends GestureDetector.SimpleOnGestureListener {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                return Math.abs(distanceX) > Math.abs(distanceY);
            }
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (xScrollDetector.onTouchEvent(ev)) {
                super.onInterceptTouchEvent(ev);
                return true;
            }
    
            return super.onInterceptTouchEvent(ev);
        }
    
    }
    

    我该如何将这个水平和垂直的视图翻页器添加到我的视图中? - user1810931
    从教程开始 - http://www.vogella.com/tutorials/AndroidCustomViews/article.html - Divers
    如何在我的视图中实现上述水平和垂直视图分页器类?代码片段可以帮助。 - user1810931
    ViewPager已经是一个视图,我不明白你在问什么。 - Divers
    我正在使用一个名为“Material Calendar View”的库(https://github.com/prolificinteractive/material-calendarview),它已经包含了水平的ViewPager,而我想在其中添加垂直的ViewPager。 - user1810931
    显示剩余4条评论

    8

    一个小小的扩展这个答案帮助我实现了我的需求(垂直视图翻页器以类似于Inshorts - News in 60 words的方式进行动画)

    public class VerticalViewPager extends ViewPager {
    
    public VerticalViewPager(Context context) {
        super(context);
        init();
    }
    
    public VerticalViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    
    private void init() {
        // The majority of the magic happens here
        setPageTransformer(true, new VerticalPageTransformer());
        // The easiest way to get rid of the overscroll drawing that happens on the left and right
        setOverScrollMode(OVER_SCROLL_NEVER);
    }
    
    private class VerticalPageTransformer implements ViewPager.PageTransformer {
     private static final float MIN_SCALE = 0.75f;
        @Override
        public void transformPage(View view, float position) {
    
            if (position < -1) { // [-Infinity,-1)
                // This page is way off-screen to the left.
                view.setAlpha(0);
    
            }  else if (position <= 0) { // [-1,0]
                // Use the default slide transition when moving to the left page
                view.setAlpha(1);
                // Counteract the default slide transition
                view.setTranslationX(view.getWidth() * -position);
    
                //set Y position to swipe in from top
                float yPosition = position * view.getHeight();
                view.setTranslationY(yPosition);
                view.setScaleX(1);
                view.setScaleY(1);
    
            } else if (position <= 1) { // [0,1]
                view.setAlpha(1);
    
                // Counteract the default slide transition
                view.setTranslationX(view.getWidth() * -position);
    
    
                // Scale the page down (between MIN_SCALE and 1)
                float scaleFactor = MIN_SCALE
                        + (1 - MIN_SCALE) * (1 - Math.abs(position));
                view.setScaleX(scaleFactor);
                view.setScaleY(scaleFactor);
    
            } else { // (1,+Infinity]
                // This page is way off-screen to the right.
                view.setAlpha(0);
            }
    
        }
    
    }
    
    /**
     * Swaps the X and Y coordinates of your touch event.
     */
    private MotionEvent swapXY(MotionEvent ev) {
        float width = getWidth();
        float height = getHeight();
    
        float newX = (ev.getY() / height) * width;
        float newY = (ev.getX() / width) * height;
    
        ev.setLocation(newX, newY);
    
        return ev;
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev){
        boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
        swapXY(ev); // return touch coordinates to original reference frame for any child views
        return intercepted;
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(swapXY(ev));
    }
    }
    

    我希望这对其他人有所帮助。

    1
    MIN_SCALE 的理想值是多少? - divyenduz
    你有没有发现快速向上滑动时过渡不够流畅的问题? - Praneeth
    为什么有人会快速向上滑动呢? - Amrut Bidri
    只是一个澄清的注释:这个解决方案改变了动画方式,新页面不是从下面而来,而是从旧页面后面而来。所以基本上你不会有页面并排放置,而是一叠页面,彼此叠放在一起。 因此,这个解决方案可能不是原始问题所要尝试的。 - Benjamin Basmaci

    4
    有一些开源项目声称可以实现这个功能。以下是它们的链接:

    这些项目已被作者标记为过时:

    还有一件事:

    • deskclock应用程序的源代码中,也有一个名为VerticalViewPager.java的文件,但似乎没有得到官方支持,因为我找不到相关的文档资料。

    3

    @Brett@Salman666的答案基础上进行了改进,以正确地将坐标(X,Y)转换为(Y,X),因为设备显示屏是矩形的,相关内容与it技术有关:

    ...

    /**
     * Swaps the X and Y coordinates of your touch event
     */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(swapXY(ev));
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(swapXY(ev));
    }
    
    private MotionEvent swapXY(MotionEvent ev) {
    
        //Get display dimensions
        float displayWidth=this.getWidth();
        float displayHeight=this.getHeight();
    
        //Get current touch position
        float posX=ev.getX();
        float posY=ev.getY();
    
        //Transform (X,Y) into (Y,X) taking display dimensions into account
        float newPosX=(posY/displayHeight)*displayWidth;
        float newPosY=(1-posX/displayWidth)*displayHeight;
    
        //swap the x and y coords of the touch event
        ev.setLocation(newPosX, newPosY);
    
        return ev;
    }
    

    然而,仍需采取措施来改善触摸屏的响应性。这个问题可能与@msdark在@Salman666的回答中提到的有关。


    我在onInterceptTouchEvent中始终返回true,并修改了键绑定,使用DPAD时触发上/下滚动而不是左/右滚动,这样做有所改进:https://gist.github.com/zevektor/fbacf4f4f6c39fd6e409 - Vektor88
    @Vektor88 始终返回 true 会阻止内部视图接收触摸事件.. 最好的解决方案是像 Brett 的答案一样.. - Mohammad Zekrallah

    3

    2
    需要记住的是,DirectionalViewPager 已经有一段时间没有更新了,因此缺少支持库中 ViewPager 的一些新功能,例如 setPageMargin(int)。但通常情况下它应该能够胜任工作。如果不能,也不难获取 ViewPager 的源代码并交换所有 x/y 和 width/height 逻辑。 - MH.
    我已经考虑了DirectionalViewPager,但正如MH所说,它没有更新(其开发者认为它已被弃用)!令人难以置信的是,Android开发人员没有向社区提供垂直的ViewPager,只有水平的。我对Android还比较新,将x / y替换为垂直的ViewPager对我来说并不容易...我更喜欢一个已经构建好的解决方案...无论如何,谢谢。 - user1709805
    2
    嗨,你成功实现了垂直ViewPager吗?谢谢。 - Paul

    2
    import android.content.Context;
    import android.support.v4.view.ViewPager;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    
    public class VerticalViewPager extends ViewPager {
    
        public VerticalViewPager(Context context) {
            super(context);
            init();
        }
    
    
        public VerticalViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
        private void init() {
    
            setPageTransformer(true, new VerticalPageTransformer());
    
            setOverScrollMode(OVER_SCROLL_NEVER);
        }
    
        private class VerticalPageTransformer implements PageTransformer {
    
            @Override
            public void transformPage(View view, float position) {
                int pageWidth = view.getWidth();
                int pageHeight = view.getHeight();
    
                if (position < -1) {
    
                    view.setAlpha(0);
    
                } else if (position <= 1) {
                    view.setAlpha(1);
    
    
                    view.setTranslationX(pageWidth * -position);
    
    
                    float yPosition = position * pageHeight;
                    view.setTranslationY(yPosition);
    
                } else {
    
                    view.setAlpha(0);
                }
            }
        }
            @Override
        public boolean onTouchEvent(MotionEvent ev) {
    
            ev.setLocation(ev.getY(), ev.getX());
    
            return super.onTouchEvent(ev);
        }
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            ev.setLocation(ev.getY(), ev.getX());
            return super.onInterceptTouchEvent(ev);
        }
    }
    

    1
    这个很好用,但是我失去了元素的touchEvent或者左滑/划动事件... - matiasfha

    1
    我最终创建了一个新的 DirectionalViewPager,它可以垂直或水平滚动,因为我在这里看到的所有解决方案都有缺陷:
    1. 现有的 VerticalViewPager 实现(由 castorflex 和 LambergaR 提供) - 它们基于非常旧的支持库版本。
    2. 坐标交换的变换技巧 - 还是会从左/右边显示 overscroller。 - 页面甩动无法正常工作,因为即使坐标已经交换,VelocityTracker.computeCurrentVelocity 仍然使用 X 轴计算速度,可能是因为内部使用了忽略坐标交换的本机调用。
    3. 视图旋转 - 需要每个子视图也被旋转的 hack。 - 如果你想读取其他东西的坐标,你必须交换轴。

    但我认为它不支持PageTransformer,因为DirectionalViewPager没有覆盖viewpager。 - Gary Chen

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