天天看点

自定义ViewPager实现图片自动轮播无限循环

重要的事情说三遍!!!

请移步此篇文章《打造一个丝滑般自动轮播无限循环Android库》!

请移步此篇文章《打造一个丝滑般自动轮播无限循环Android库》!!

请移步此篇文章《打造一个丝滑般自动轮播无限循环Android库》!!!

-----------------------------------------------------------------------------------2019.09更新

本篇文章写于2016年7月,在后续版本中CircleViewPager进行了多次重构及优化,并在 2.0.0版本中改名为BannerViewPager,文章中代码跟Github源码差别比较大,因此欢迎大家到Github参考最新代码。(https://github.com/zhpanvip/CircleViewPager)

-----------------------------------------------------------------------------------2019.06更新

Viewpager图片自动轮播无限循环是Android项目中经常用到的功能,功能实现起来也比较简单,但会出现不少问题。因此很多情况下做出来的效果并不太让人满意,甚至有些上线的项目自动轮播上也会出现一些bug。比如切换过程中出现空白页面,有些甚至在滑动过程中造成程序崩溃。本文内容对ViewPager进行封装,实现了可循环轮播的CirclSeViewPager,** 该控件有较强的可扩展性,可接受任意类型的集合数据,可以自定义任意的轮播页面样式。页面切换也比较流畅。**

先看效果图:

自定义ViewPager实现图片自动轮播无限循环

接下来将通过以下几个小节对CircleViewPager做具体的分析。

  • 如何使用CircleViewPager
  • CircleViewPager的实现思路
  • CircleViewPager具体实现
  • CircleViewPager实现自动轮播
  • CircleViewPager页面点击事件

如果只是想实现无限轮播的效果那么只需看第一节即可,源码可在文章末尾下载。

一、如何使用CircleViewPager.。

CircleViewPager的使用非常简单,只需要几行代码就可实现,使用方法如下:

1.gradle中添加依赖

2.在xml文件中添加如下代码:

<com.zhpan.viewpager.view.CircleViewPager
        android:id="@+id/viewpager2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginEnd="35dp"
        android:layout_marginStart="35dp"
        app:interval="5000" />
           

3.CircleViewPager属性

//  是否显示指示器
        mViewpager.isShowIndicator(true);
		//  设置指示器位置
        mViewpager.setIndicatorGravity(CircleViewPager.IndicatorGravity.END);
        //  设置指示器圆点半径
        mViewpager.setIndicatorRadius(6);
		//  设置圆点指示器颜色		
		mViewPager.setIndicatorColor(getResources().getColor(R.color.colorAccent),
                getResources().getColor(R.color.colorPrimary));          
        //  设置是否无限循环
        mViewpager.setCanLoop(true);        
        //  设置是否自动轮播
        mViewpager.setAutoPlay(true);
        //  设置图片切换时间间隔
        mViewpager.setInterval(3000);
        //  设置页面点击事件
        mViewpager.setOnPageClickListener(new CircleViewPager.OnPageClickListener() {
            @Override
            public void onPageClick(int position) {
                List<DataBean> list = mViewpager.getList();
                Toast.makeText(MainActivity.this, "点击了" + list.get(position).getDescribe(), Toast.LENGTH_SHORT).show();
            }
        });
        //  设置数据
        mViewpager.setPages(mList, new HolderCreator<ViewHolder>() {
            @Override
            public ViewHolder createViewHolder() {
                return new MyViewHolder();
            }
        });
           

4.自定义ViewHolder

public class MyViewHolder implements ViewHolder<String> {
            private ImageView mImageView;

            @Override
            public View createView(Context context) {
                // 返回页面布局文件
                View view = LayoutInflater.from(context).inflate(R.layout.banner_item, null);
                mImageView = (ImageView) view.findViewById(R.id.banner_image);
                return view;
            }

            @Override
            public void onBind(Context context, int position, String data) {
	            // 数据绑定
		        ImageLoaderUtil.loadImg(mImageView, (String) data);
            }
        }
           

5.为防止内存泄露在onDestory()中停止图片轮播

@Override
    protected void onDestroy() {
        super.onDestroy();
        mViewpager.stopLoop();
    }
           

二、CircleViewPager的实现思路

页面循环切换最容易出现问题的地方就是在最后一页向第一页切换或者第一页向最后一页切换时,在这个切换过程中很容易出现空白页面。怎么解决这个问题?

CircleViewPager的实现的思路是在第一张图片前和最后一张图片后分别添加一个ImageView,最前边的ImageView背景设置为最后一张图片,最后一个ImageView背景设置第一张图片。当我们判断滑动到最后一个ImageView时则设置ViewPager.setCurrentItem(1),让其自动切换到第一张图片,这样在从最后一页切换到第一页时由于图片是用的同一张图片,所以就会使切换效果显得很流畅自然。同理,当向左滑动到第0个ImageView时用ViewPager.setCurrentItem(length)自动切换到倒数第二张图片,第0个ImageView和倒数第二个ImageView图片相同,这样就使滑动效果显得很自然。

三、CircleViewPager具体实现

1.添加自定义属性。以在value目录下创建attrs.xml文件,文件中我们可以定义一些用到的属性,attrs.xml中定义的属性如下:

<resources>
    <declare-styleable name="MyViewPager">
        <!--选中时的圆点图片-->
        <attr name="lightDotRes" format="reference"/>
        <!--未选中时的圆点图片-->
        <attr name="darkDotRes" format="reference"/>
        <!--圆点半径-->
        <attr name="dotWidth" format="dimension"/>
        <!--页面切换时间间隔-->
        <attr name="interval" format="integer"/>
    </declare-styleable>
</resources>
           

2.新建CircleViewPager的布局文件view_pager_layout.xml,代码如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/vp_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <LinearLayout
        android:id="@+id/ll_main_dot"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/vp_main"
        android:layout_marginBottom="10dp"
        android:gravity="center_horizontal"
        android:orientation="horizontal" />
</RelativeLayout>
           

3.定义CircleViewPager类并继承FrameLayout,并在构造方法中初始化数据,代码如下:

public CircleViewPager(Context context) {
        super(context);
        init(null);
    }

    public CircleViewPager(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        init(attrs);
    }

    public CircleViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (changed) {
            initData();
            setIndicatorImage();
            setViewPager();
            setIndicatorLocation();
        }
    }

    private void init(AttributeSet attrs) {
        if (attrs != null) {
            TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircleViewPager);
            mLightIndicator = typedArray.getResourceId(R.styleable.CircleViewPager_lightDotRes, R.drawable.red_dot);
            mDarkIndicator = typedArray.getResourceId(R.styleable.CircleViewPager_darkDotRes, R.drawable.red_dot_night);
            mDotWidth = typedArray.getDimension(R.styleable.CircleViewPager_dotWidth, 20);
            interval = typedArray.getInteger(R.styleable.CircleViewPager_interval, 3000);
            typedArray.recycle();
        }
         mView = LayoutInflater.from(getContext()).inflate(R.layout.view_pager_layout, this);
        mLlDot = (LinearLayout) mView.findViewById(R.id.ll_main_dot);
        mViewPager = (ViewPager) mView.findViewById(R.id.vp_main);
        mList = new ArrayList<>();
        mListAdd = new ArrayList<>();
        mIvDotList = new ArrayList<>();
    }
           

4.重写onLayout()方法,根据图片URL集合创建图片对应的ImageVIew和小圆点对应的ImageView.

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (changed) {
            initData();
            setIndicatorImage();
            setViewPager();
            setIndicatorLocation();
        }
    }
    
    //  根据mList数据集构造mListAdd
    private void initData() {
        if (mList.size() == 0) {
            mView.setVisibility(GONE);
        } else if (mList.size() == 1) {
            mListAdd.add(mList.get(0));
        } else if (mList.size() > 1) {
            for (int i = 0; i < mList.size() + 2; i++) {
                if (i == 0) {   //  判断当i=0为该处的mList的最后一个数据作为mListAdd的第一个数据
                    mListAdd.add(mList.get(mList.size() - 1));
                } else if (i == mList.size() + 1) {   //  判断当i=mList.size()+1时将mList的第一个数据作为mListAdd的最后一个数据
                    mListAdd.add(mList.get(0));
                } else {  //  其他情况
                    mListAdd.add(mList.get(i - 1));
                }
            }
        }
    }

   //  设置轮播小圆点
    private void setIndicatorImage() {
        //  设置LinearLayout的子控件的宽高,这里单位是像素。
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams((int) mDotWidth, (int) mDotWidth);
        params.rightMargin = (int) (mDotWidth / 1.5);
        if (mList.size() > 1) {
            //  for循环创建mUrlList.size()个ImageView(小圆点)
            for (int i = 0; i < mList.size(); i++) {
                ImageView imageViewDot = new ImageView(getContext());
                imageViewDot.setLayoutParams(params);
                //  设置小圆点的背景为暗红图片
                imageViewDot.setBackgroundResource(mDarkIndicator);
                mLlDot.addView(imageViewDot);
                mIvDotList.add(imageViewDot);
            }
        }
        //设置第一个小圆点图片背景为红色
        if (mList.size() > 1) {
            mIvDotList.get(dotPosition).setBackgroundResource(mLightIndicator);
        }
    }
           

5.为ViewPager适配数据

private void setViewPager() {
        CirclePagerAdapter<T> adapter = new CirclePagerAdapter<>(mListAdd, this, holderCreator);
        mViewPager.setAdapter(adapter);
        mViewPager.setCurrentItem(currentPosition);

        setPageChangeListener();
        startLoop();
        setTouchListener();
        if (showIndicator) {
            mLlDot.setVisibility(VISIBLE);
        } else {
            mLlDot.setVisibility(GONE);
        }
    }
           

6.接下来为ViewPager添加页面改变的监听事件。

//  ViewPager页面改变监听
    private void setPageChangeListener() {
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
			}

            @Override
            public void onPageSelected(int position) {
                pageSelected(position);
            }

            @Override
            public void onPageScrollStateChanged(int state) {
                //  当state为SCROLL_STATE_IDLE即没有滑动的状态时切换页面
                if (state == ViewPager.SCROLL_STATE_IDLE) {
                    mViewPager.setCurrentItem(currentPosition, false);
                }
            }
        });
    }

private void pageSelected(int position) {
        if (position == 0) {    //判断当切换到第0个页面时把currentPosition设置为list.size(),即倒数第二个位置,小圆点位置为length-1
            currentPosition = mList.size();
            dotPosition = mList.size() - 1;
        } else if (position == mList.size() + 1) {    //当切换到最后一个页面时currentPosition设置为第一个位置,小圆点位置为0
            currentPosition = 1;
            dotPosition = 0;
        } else {
            currentPosition = position;
            dotPosition = position - 1;
        }
        //  把之前的小圆点设置背景为暗红,当前小圆点设置为红色
        mIvDotList.get(prePosition).setBackgroundResource(mDarkIndicator);
        mIvDotList.get(dotPosition).setBackgroundResource(mLightIndicator);
        prePosition = dotPosition;
    }
           

至此,ViewPager已经可以实现滑动,并且圆点也会跟随页面滑动而改变。

四、CircleViewPager实现自动轮播

1.自动轮播的实现,在第三节第5步的setViewPager()方法中调用下面startLoop()方法即可开启自动轮播

Handler mHandler = new Handler();
    Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            if (mViewPager.getChildCount() > 1) {
                mHandler.postDelayed(this, interval);
                currentPosition++;
                mViewPager.setCurrentItem(currentPosition, true);
            }
        }
    };

private void startLoop() {
        if (!isLoop && mViewPager != null) {
            mHandler.postDelayed(mRunnable, interval);// 每两秒执行一次runnable.
            isLoop = true;
        }
    }
           

2.自动轮播实现后发现会有些问题,即在手动滑动页面时,页面仍然会自动切换。这样体验效果是非常不好的,因此我们需要在手动滑动时停止自动轮播,当手动滑动结束时再开启自动轮播。因此我们可以重写ViewPager的onTouch事件进行处理,当触发ACTION_DOWN和ACTION_MOVE时停止自动轮播,当触发ACTION_UP和ACTION_CANCEL时再开启自动轮播。代码实现如下:

//  设置触摸事件,当滑动或者触摸时停止自动轮播
    private void setTouchListener() {
        mViewPager.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                    case MotionEvent.ACTION_MOVE:
                        isLoop = true;
                        stopLoop();
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        isLoop = false;
                        startLoop();
                    default:
                        break;
                }
                return false;
            }
        });
    }

	private void startLoop() {
        if (!isLoop && mViewPager != null) {
            mHandler.postDelayed(mRunnable, interval);// 每interval秒执行一次runnable.
            isLoop = true;
        }
    }
    
	public void stopLoop() {
        if (isLoop && mViewPager != null) {
            mHandler.removeCallbacks(mRunnable);
            isLoop = false;
        }
    }
           

至此,CircleViewPager的核心功能已经实现。但是页面的点击事件还未进行处理。接下来将在第五节中实现CircleViewPager的页面点击事件。

五、CircleViewPager页面点击事件。

1.页面的点击事件是通过点击ImageView触发的,因此我们可以首先考虑给页面的ImageView设置点击事件的监听。在哪里设置点击事件?无疑在Adapter中设置是比较简单的,因此我们在CircleViewPager的Adapter中对ImageView添加点击事件监听。如下:

@Override
    public Object instantiateItem(final ViewGroup container, final int position) {
        View view = getView(position, container);
        container.addView(view);
        return view;
    }
    
	//  根据图片URL创建对应的ImageView并添加到集合
    private View getView(final int position, ViewGroup container) {
        ViewHolder holder = holderCreator.createViewHolder();
        if (holder == null) {
            throw new RuntimeException("can not return a null holder");
        }
        View view = holder.createView(container.getContext());
        if (list != null && list.size() > 0) {
            holder.onBind(container.getContext(), position, list.get(position));
        }
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                viewPager.imageClick(position - 1);
            }
        });
        return view;
    }
           

我们事先在CircleViewPager中定义imageClick()方法,在实例化Adapter的时候将CircleViewPager自身传递进来,然后就可以在Adapter调用imageClick()方法。即图片被点击的时候会触发到CircleViewPager中的imageClick()方法。

2.模仿Android中View的监听事件来为CircleViewPager设置点击页面的事件监听。在CircleViewPager中定义OnPageClickListener接口,并在接口中定义pageClickListener(int position)的抽象方法。然后在CircleViewPager中定义OnPageClickListener的成员变量mOnPageClickListener,并为其设置set()方法。然后在imageClick()方法中调用mOnPageClickListener.pageClickListener(int position)。代码实现如下:

private OnPageClickListener mOnPageClickListener;

		public void setOnPageClickListener(OnPageClickListener onPageClickListener) {
        this.mOnPageClickListener = onPageClickListener;
    }
	
    //  adapter中图片点击的回掉方法
    public void imageClick(int position) {
        mOnPageClickListener.pageClickListener(position);
    }

	 //  监听页面点击的接口
	 public interface OnPageClickListener {
        void pageClickListener(int position);
    }
           

3.MainActivity中设置页面点击的监听。通过CicleViewPager.setOnPageClickListener实现对页面点击的监听。

mViewpager.setOnPageClickListener(new CircleViewPager.OnPageClickListener() {
            @Override
            public void pageClickListener(int position) {
                Toast.makeText(MainActivity.this, "点击了第"+position+"个美眉 \nURL:"+mViewpager.getUrlList().get(position), Toast.LENGTH_SHORT).show();
            }
        });
           

源码下载