Rxjava + ViewPager 打造实用图片轮播

作者: 码无止境 | 来源:发表于2016-09-22 17:34 被阅读2195次

    背景

    说到图片轮播,之前写过一篇文章《造轮子:android自定义专属广告轮播控件》,不过当时是采用ViewFlipper实现图片轮播的,最近开始研究Rxjava技术,发现有个interval的方法,觉得很实用,就打算去实战写一个东西来玩玩。就笔者目前接触的项目,发现图片轮播这种功能,应用非常之多,由此笔者就构想用Rxjava + ViewPaper去写一个通用能自定义能扩展的图片轮播框架。

    目的

    开源项目,打造一个好用的图片轮播框架。

    思路

    相信大家之前都用过ViewPaper,所以此处不多做解释,不清楚的童鞋可以百度或谷歌。Rxjava,之前写过两篇文章:《Rxjava实践之路[入门篇]》《Rxjava实践之路[初级篇]》,不太清楚的童鞋可以去看看,或者百度查其他文章了解了解。大体实现思路如下:

    • ViewPaper实现滑动切换页面
    • Rxjava定时使ViewPaper切换页面
    1132780-f58fe3a5c5a7c281.jpg

    有两个大体思路,接下来我们从细节出发,首先我们考虑以下两个问题:

    • 要不要循环?
    • 要不要自动轮播?

    接着就衍生以下几种可能性:

    • 不循环(肯定不轮播,只能手动切换图片)
    • 循环
      • 自动轮播
      • 手动切换图片
    1132780-c9a67cd151670731.jpg

    然后考虑指示器问题,会衍生以下几个问题:

    • 指示器如何摆放?
    • 指示器图标是否需要自定义?

    由此诞生以下几种可能性:

    • 指示器摆放位置产生靠左,靠右,靠中三种选择
    • 指示器肯定需要能自定义图标,满足大众选择要求嘛~

    接着考虑怎么去实现以上需求,首先我们将ViewPager切换页面划分为两种:

    • 不循环:此时所做工作,跟平常使用ViewPager切换页面无区别,无特殊处理。
    • 循环: 用过ViewPager的童鞋都清楚,ViewPager怎么可以循环呀?这里做了一个巧妙地工作,这也是ViewPaper实现自动轮播原理所在。这块做详细说明,重点来了,大家擦亮眼睛看清啦~~
    1132780-2d7d1306d1b86b65.jpg

    ViewPager实现自动轮播原理说明:

    假如现在有三张图需要自动轮播,图1,图2,图3。那轮播View集合就需要增加两张图,在原图3后面增加图1,在原图1前增加图3,处理过后的轮播View集合顺序是这样的图3、图1、图2、图3、图1。此刻有些童鞋看着有点懵,这样处理有啥用呀?

    1132780-253900d649118aa0.jpg

    处理前后比对:
    处理前轮播View集合顺序:图1、图2、图3。
    处理后轮播View集合顺序:新图3、原图1、原图2、原图3、新图1。

    当向右滑动ViewPager,滑动到最后一个位置即新图1,此时做一个巧妙地跳转,ViewPager有个setCurrentItem(int item, boolean smoothScroll)方法,将smoothScroll置为false,跳转到原图1,因为两图是一样的图,而又看不到滑动效果,所以感觉没变一样。
    当向左滑动ViewPager,滑动到第一个位置即新图3,同样调用setCurrentItem(item,false)等方法跳转到原图3,至此就达到了循环效果了。
    童鞋们,是不是很简单?思路分析到此,接下来代码撸起来~~~

    代码解析

    首先我们需要写一个xml布局文件,里面包括ViewPager(用于展示图片)、指示器(显示当前图片在第几张)、标题(这个作为可选项),代码如下:

    <?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"
        android:background="@android:color/white"
        android:orientation="vertical">
        <android.support.v4.view.ViewPager
            android:id="@+id/vp_cycle"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        <LinearLayout
            android:id="@+id/ly_cycle_indicator"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:paddingBottom="24dp"
            android:gravity="center"
            android:orientation="horizontal" />
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_above="@id/ly_cycle_indicator"
            android:orientation="vertical">
            <TextView
                android:id="@+id/tv_cycle_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:gravity="center"
                android:textColor="@android:color/white"
                android:textSize="20sp" />
        </LinearLayout>
    </RelativeLayout>
    

    接着写初始化view操作,所做的工作包括初始化一个View,初始化该View需要用到的控件并将该View添加为当前轮播自定义View的子View。代码如下:

        /**
         * 初始化view
         * @author leibing
         * @createTime 2016/09/20
         * @lastModify 2016/09/20
         * @param context 上下文
         * @return
         */
        private void initView(Context context) {
            // 指定布局
            View view = LayoutInflater.from(context).inflate(R.layout.widget_cycle_view, null);
            // findView
            mViewPager = (ViewPager) view.findViewById(R.id.vp_cycle);
            mTitle = (TextView) view.findViewById(R.id.tv_cycle_title);
            mIndicatorLy = (LinearLayout) view.findViewById(R.id.ly_cycle_indicator);
            // 添加view到轮播view
            this.addView(view);
        }
    

    然后给该轮播自定义View添加数据源,主要做以下工作:

    • 添加轮播View
    • 添加指示器
    • 设置轮播适配器
    • 开始订阅自动轮播

    代码如下:

        /**
         * 设置数据源
         * @author leibing
         * @createTime 2016/09/20
         * @lastModify 2016/09/20
         * @param mData 轮播view数据源
         * @param defaultPosition 默认显示位置
         * @param defaultImage 默认占位图(图片未加载出来前)
         * @param listener 轮播监听
         * @return
         */
        public void setData(List<CycleModel> mData, int defaultPosition,
                            Drawable defaultImage, CycleViewListener listener){
            // 设置轮播view数据源
            this.mData = mData;
            // 如果数据源不存在或者其大小为0则设置当前布局为不可见
            if (mData == null || mData.size() == 0){
                this.setVisibility(View.GONE);
                return;
            }
            int size = mData.size();
            // 如果默认显示位置超过轮播view数目则默认位置从第一个位置开始
            if (defaultPosition >= size)
                defaultPosition = 0;
            // 轮播view数目为1,则不需要循环
            if (size == 1)
                isCycle = false;
            // 清除mViews数据
            mViews.clear();
            // 添加轮播view
            if (isCycle) {
                // 添加轮播图View,数量为集合数+2
                // 将最后一个View添加进来
                mViews.add(getCycleView(getContext(), mData.get(size - 1).getUrl(), defaultImage));
                for (int i = 0; i < size; i++) {
                    mViews.add(getCycleView(getContext(), mData.get(i).getUrl(), defaultImage));
                }
                // 将第一个View添加进来
                mViews.add(getCycleView(getContext() , mData.get(0).getUrl(), defaultImage));
            } else {
                // 只添加对应数量的View
                for (int i = 0; i < size; i++) {
                    mViews.add(getCycleView(getContext(), mData.get(i).getUrl(), defaultImage));
                }
            }
            // 设置轮播监听
            cycleViewListener = listener;
            // 初始化指示器
            initIndicators(size, getContext());
            // 设置指示器
            setIndicator(defaultPosition);
            // 设置适配器
            setAdapter(mViews, cycleViewListener, size);
            // 如果已经开始轮播订阅,则取消轮播订阅
            cancelSubscription();
            // 开始轮播
            startWheel(size);
        }
    

    订阅轮播代码如下:

        /**
         * 开始轮播
         * @author leibing
         * @createTime 2016/09/21
         * @lastModify 2016/09/21
         * @param size 轮播view数目
         * @return
         */
        private void startWheel(int size){
            if (size < 2 || !isCycle()){
                // 取消轮播
                setWheel(false);
                return;
            }
            // 设置轮播
            setWheel(true);
            // 开始轮播
            mSubscription = Observable.interval(delay, TimeUnit.MILLISECONDS)
                    .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Action1<Long>() {
                        @Override
                        public void call(Long aLong) {
                            if (isWheel && isHasWheel) {
                                mCurrentPosition++;
                                if (mViewPager != null)
                                    mViewPager.setCurrentItem(mCurrentPosition, false);
                            }
                        }
                    });
        }
    

    ViewPager在页面切换做了相关处理,思路里面已经讲了,而且注释也比较清楚,代码如下:

        @Override
        public void onPageSelected(int position) {
            int max = mViews.size() - 1;
            mCurrentPosition = position;
            if (isCycle()) {
                if (position == 0) {
                    // 滚动到mView的1个(界面上的最后一个),将mCurrentPosition设置为max - 1
                    mCurrentPosition = max - 1;
                } else if (position == max) {
                    // 滚动到mView的最后一个(界面上的第一个),将mCurrentPosition设置为1
                    mCurrentPosition = 1;
                }
                position = mCurrentPosition - 1;
            }
            setIndicator(position);
        }
    
        @Override
        public void onPageScrollStateChanged(int state) {
            if (state == 0 && isCycle()) { // viewPager滚动结束
                //跳转到第mCurrentPosition个页面(没有动画效果,实际效果页面上没变化)
                mViewPager.setCurrentItem(mCurrentPosition, false);
            }
        }
    

    然后就做了一些指示器自定义处理,如指示器位置和指示器图标,代码如下:

        /**
         * 设置指示器图片
         * @author leibing
         * @createTime 2016/09/20
         * @lastModify 2016/09/20
         * @param select   选中时的图片
         * @param unselect 未选中时的图片
         * @return
         */
        public void setIndicators(int select, int unselect) {
            mIndicatorSelected = select;
            mIndicatorUnselected = unselect;
        }
        
        
        /**
         * 指示器靠右显示
         * @author leibing
         * @createTime 2016/09/21
         * @lastModify 2016/09/21
         * @param paddingRight 指示器距右边内边距
         * @param paddingBottom 指示器距底部内边距
         * @return
         */
        public void setAlignParentRight(int paddingRight, int paddingBottom){
            if (mIndicatorLy == null)
                return;
    
            // 设置为靠右
            mIndicatorLy.setGravity(Gravity.RIGHT);
            // 设置内边距
            mIndicatorLy.setPadding(0,0,paddingRight,paddingBottom);
            // 重新布局
            mIndicatorLy.requestLayout();
        }
    
        /**
         * 指示器靠右显示
         * @author leibing
         * @createTime 2016/09/21
         * @lastModify 2016/09/21
         * @param paddingLeft 指示器距左边内边距
         * @param paddingBottom 指示器距底部内边距
         * @return
         */
        public void setAlignParentLeft(int paddingLeft, int paddingBottom){
            // 设置为靠左
            mIndicatorLy.setGravity(Gravity.LEFT);
            // 设置内边距
            mIndicatorLy.setPadding(paddingLeft, 0, 0, paddingBottom);
            // 重新布局
            mIndicatorLy.requestLayout();
        }
    
        /**
         * 指示器设置居中显示
         * @author leibing
         * @createTime 2016/09/21
         * @lastModify 2016/09/21
         * @param paddingBottom 指示器距底部内边距
         * @return
         */
        public void setAlignParentCenter(int paddingBottom){
            if (mIndicatorLy == null)
                return;
            // 设置为居中
            mIndicatorLy.setGravity(Gravity.CENTER);
            // 设置内边距
            mIndicatorLy.setPadding(0, 0, 0, paddingBottom);
            // 重新布局
            mIndicatorLy.requestLayout();
        }
    

    最后,对自动轮播时手动滑动做了优化处理,当手指按下或者滑动的过程中停止轮播,手指离开屏幕开始轮播,代码如下:

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            switch (ev.getAction()){
                case MotionEvent.ACTION_MOVE:
                case MotionEvent.ACTION_DOWN:
                    // 手指按下或者滑动的过程中停止轮播
                    setWheel(false);
                    break;
                case MotionEvent.ACTION_UP:
                    // 手指离开屏幕开始轮播
                    setWheel(true);
                    break;
            }
            return super.dispatchTouchEvent(ev);
        }
    

    注意事项

    当前页面不再使用该自定义图片轮播,记得在Activity onDestory方法中取消订阅(为了避免内存泄漏问题),只需调用cancelSubscription()方法,方法代码如下:

    
        /**
         * 取消轮播订阅
         * @author leibing
         * @createTime 2016/09/22
         * @lastModify 2016/09/22
         * @param
         * @return
         */
        public void cancelSubscription(){
            if (mSubscription != null){
                mSubscription.unsubscribe();
                mSubscription = null;
            }
        }
    

    运行效果图如下:

    LbaizxfCycleView.gif

    笔者文笔太糟,欢迎吐槽,如有不对之处,请留言指点~~

    呼吁大家动手实践,一切将会变得很容易~~~

    项目地址:LbaizxfCycleView

    关于作者

    相关文章

      网友评论

      本文标题:Rxjava + ViewPager 打造实用图片轮播

      本文链接:https://www.haomeiwen.com/subject/iwuzettx.html