折腾ViewPager的轮播特效

/ 0评 / 0

前言

轮播图特效在开发过程中还是很常用的,下面我就来介绍下怎么一步步将ViewPager折腾出我们想要的轮播效果,包括自动滚动,自定义滚动速度,无限滚动以及点击左右两边切换页等问题。虽然网上很多博客也都介绍了,但是往往不全并且很多都是错的,所以我也会详细介绍下在折腾过程中发现的一些小问题,给出一个比较完美的方案。

首先看看最终效果图如下。下面就开始介绍如何一步步完成此效果。

 ViewPager的多页显示

因为ViewPager往往是只显示一页的,可是上面的效果在一屏上面却显示了三张,这个主要是使用了android:clipChildren="false"属性,此属性默认为true,功能是限制子布局显示在"自己的尺寸中",当我们设置为false,则表示,允许父布局允许子控件将画面绘制到父布局的尺寸中,所以我们的基础布局文件如下。

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

    <android.support.v4.view.ViewPager
        android:id="@+id/main_view_pager"
        android:layout_width="match_parent"
        android:layout_height="130dp"
        android:layout_marginLeft="40dp"
        android:layout_marginRight="40dp"
        android:clipChildren="false"/>

</FrameLayout>

将父布局FrameLayout的android:clipChildren设置为false,同时给ViewPager设置一个左右margin,我们知道,这个margin里面ViewPager是无法绘制自己的,可是我们这样设置以后,则ViewPager则将自己绘制到了margin中,这样看起来就是一个屏幕上面显示了三页。

细心的小伙伴可以看到我给FrameLayout同时设置了一个android:layerType="software",这句话的作用主要是因为在不支持硬件加速的设备上面滑动界面刷新可能出问题。

然后就是通过代码设置ViewPager的部分属性

//设置page之间的间距为40px
mViewPager.setPageMargin(40);
//设置ViewPager缓存3页
mViewPager.setOffscreenPageLimit(3);

mViewPager.setOffscreenPageLimit(3);的作用主要是为了让滚动效果更加的流畅以及消除闪烁的效果,不加这句话的效果如下图。

ViewPager点击左右滑动

通过上面简单的几步,我们已经可以让ViewPager在一页上面显示多张了,可是这里有一个问题,就是我们只能在中间的一页进行左右滑动,两边无法滑动以及点击,这是由于ViewPager真正占据的位置只有中间的一部分,左右我们可见的那部分是属于父布局的,所有我们点击左右两边的事件其实都被父布局给消费了,这样一分析,解决方案也就出来了,那就是拦截父布局的Touch事件即可。

((ViewGroup) mViewPager.getParent()).setOnTouchListener(new OnTouchListener() {

    float x;

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

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            x = ev.getX();
        }
        //如果是点击事件,那么需要处理下,判断是否点在左右两边
        if (ev.getAction() == MotionEvent.ACTION_UP && Math.abs(ev.getX() - x) < 20) {
            View view = viewOfClickOnScreen(ev);
            if (view != null) {
                // int index = mViewPager.indexOfChild(view);
                int index = mAdapter.indexView(view);
                if (index != mViewPager.getCurrentItem()) {
                    mViewPager.setCurrentItem(index);
                    return true;
                }
            }
        }
        return mViewPager.dispatchTouchEvent(ev);
    }
});

首先通过getParent()方法就可以获取到ViewPager的父布局,然后处理其Touch事件,默认的通过mViewPager.dispatchTouchEvent(ev);将Touch事件传递给ViewPager处理,同时可以看到,我在手指抬起的时候判断了是否为点击事件,如果是点击事件,那么父布局就将此事件给消费掉,同时根据点击的位置去左右滑动Page。

/**
 * 判断当前点击的位置在ViewPager的哪一个View上面
 */
private View viewOfClickOnScreen(MotionEvent ev) {
    int childCount = mViewPager.getChildCount();
    int[] location = new int[2];
    for (int i = 0; i < childCount; i++) {
        View v = mViewPager.getChildAt(i);
        v.getLocationOnScreen(location);

        int minX = location[0];
        int minY = mViewPager.getTop();

        int maxX = location[0] + v.getWidth();
        int maxY = mViewPager.getBottom();

        float x = ev.getX();
        float y = ev.getY();

        if ((x > minX && x < maxX) && (y > minY && y < maxY)) {
            return v;
        }
    }
    return null;
}

上面的代码则是判断当前点击事件点击在ViewPager的哪一页上,然后通过mViewPager.indexOfChild(view);方法则可以获取到此View的position,不过在实验过程中发现,此问题有坑!!!我的处理是保存所有的Page对应的View,然后进行匹配,距离的可以查看源码。

ViewPager的无限循环滚动

根据百度的结果,主要有两种处理方式,分别为将ViewPager的页数设置为无限大(Integer.MAX_VALUE),然后进行取模操作,不过个人尝试了下,有两个缺陷,第一,在首页无法左滑,第二容易出现异常照成崩溃。第二种方式是在ViewPager的正常数据的首尾分别添加一个假数据,本博客主要讨论的第二种方案。

首先,假设正常数据位ABC,那么我们首尾添加一个数据,将数据源变为CABCA,当我们滚动到最后的一个A的时候,我们通过mViewPager.setCurrentItem(index);将当前页定位到第一个A,当我们滚动到第一个C的时候,定位到最后一个C,那么在用户看来,就是一个无限滚动了。这里需要注意的是,ViewPager自身有一个滚动动画,所以我们需要在ViewPager滚动动画执行完毕以后再进行页面跳转。

由于我们这里一页显示了三个,所以我们需要首尾都添加两个数据,这个根据自身的逻辑进行处理。动态跳转的代码如下。

mViewPager.setOnPageChangeListener(new OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int position) {

    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if (state != ViewPager.SCROLL_STATE_IDLE) {
            return;
        }
        int item = mViewPager.getCurrentItem();
        // 当前在最后一个
        if (item == mViewPager.getAdapter().getCount() - 2) {
            mViewPager.setCurrentItem(2, false);
        }
        //当前在第一个
        if (item == 1) {
            mViewPager.setCurrentItem(mViewPager.getAdapter().getCount() - 4, false);
        }
    }
});

ViewPager的滚动动画

我们想定义ViewPager的滚动动画,主要是通过mViewPager.setPageTransformer(true, this);方法,实现ViewPager.PageTransformer即可,其只有一个回调函数public void transformPage(View view, float position),我们通过position对view设置对应的动画即可。position的取值范围以及含义如下。

[-Infinity,-1)  当前页滚动到完全不可见的位置
[-1,0]          当前页完全可见(0)滚动到不可见(-1)
(0,1]           下一页从不可见(1)到可见(0)
(1,+Infinity]   下一页完全不可见的位置

更多具体的信息请查看官方文档,因为官方文档讲的超级详细:https://developer.android.com/training/animation/screen-slide.html

ViewPager的自动滚动

经过上面几步,自动滚动其实已经很简单了,开启一个线程,然后不停的设置当前位置即可,代码如下,唯一需要注意的是,mViewPager.setCurrentItem需要在主线程中调用

new Thread(new Runnable() {

    @Override
    public void run() {
        while (true) {
            runOnUiThread(new Runnable() {

                @Override
                public void run() {
                    //这里不需要担心越界问题,因为到了最后几个会自动回到前面,造成无限循环的假象
                    //TODO 小米机型上面测试的时候,activity不可见以后,再回来动画停止了
                    mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);
                }
            });
            //这个时间不能太短,不然可能切换位置的动画还没走完
            SystemClock.sleep(3000);
        }
    }
}).start();

自定义ViewPager滚动速度

如果我们使用ViewPager的自动滚动,其实可以很清楚的看到,系统自带的滚动速度较快,效果不是很好,所以我们需要修改下ViewPager的滚动速度,由于ViewPager的滚动是通过Scroller实现的,并且系统没有响应API去设置,所以我们直接通过反射的方式将ViewPager的Scroller给设置为我们自己的,自定义Scroller如下。

public class ViewPagerScroller extends Scroller {
    private int mScrollDuration = 2000; // 滑动速度

    /**
     * 设置速度速度
     *
     * @param duration
     */
    public void setScrollDuration(int duration) {
        this.mScrollDuration = duration;
    }

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

    public ViewPagerScroller(Context context, android.view.animation.Interpolator interpolator) {
        super(context, interpolator);
    }

    public ViewPagerScroller(Context context, android.view.animation.Interpolator interpolator, boolean flywheel) {
        super(context, interpolator, flywheel);
    }

    @Override
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        super.startScroll(startX, startY, dx, dy, mScrollDuration);
    }

    @Override
    public void startScroll(int startX, int startY, int dx, int dy) {
        super.startScroll(startX, startY, dx, dy, mScrollDuration);
    }

    public void initViewPagerScroll(ViewPager viewPager) {
        try {
            Field mScroller = ViewPager.class.getDeclaredField("mScroller");
            mScroller.setAccessible(true);
            mScroller.set(viewPager, this);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用也很简单,只需要三句代码即可

ViewPagerScroller scroller = new ViewPagerScroller(this);
scroller.setScrollDuration(2000);
scroller.initViewPagerScroll(mViewPager);

多折腾一会

我在第一节中介绍了,一屏上面显示多个Page是通过Margin来实现的,从而引发了下面两个问题,点击左右无效、左右无法滑动两个问题,原因就是margin部分不算在ViewPager里面,那么如果我们使用Padding呢?

有这么一个属性android:clipChildren="false",功能类似于android:clipChildren="false",不过前者表示允许子控件绘制在padding中,既然可以,那么直接开整。当然整的代码就不贴了,可以在源码中找找,这里只贴出折腾的结果。

不能通过padding实现!!!不能通过padding实现!!!不能通过padding实现!!!虽然使用padding能完美的规避点击左右无效、左右无法滑动两个问题但是在设置动画的时候就出现了问题,首先,通过padding实现的position的取值范围发生了改变,其次,position不保证一定能出现边界值,比如0、1、-1等,所以padding夭折在动画这一关。

结语

通过上面的操作,一个比较完善的ViewPager轮播动画就实现了,大致上还是很简单的,Demo源码如下:

GitHub:https://github.com/CB2Git/BlogDemoRepository/tree/master/XTuone

发表评论

您的电子邮箱地址不会被公开。