ViewPager打造真正意义的无限轮播

  1. 利用ViewPage的PagerTransformer定制页面切换效果
  2. ViewPager动态添加删除及刷新页面
  3. ViewPager打造真正意义的无限轮播
  4. ViewPage 联动效果自带角标
  5. ViewPager禁止滑动和修改滑动速度

1 简述

ViewPage 不仅常用于页面导航切换,也常用来实现轮播图。百度一下,可以找到很多关于轮播图的实现文章。曾翻看过多篇相关文章,get 到一些要点,然而觉得自己实现一下,会更加深刻,如果加上自己独特的思路,也是对自己的一个锻炼,对代码一个积累。实现目标如下图所示:
ViewPager无限轮播

2 实现思路

ViewPager 实现轮播图早已为我们所熟悉,我个人认为,实现方式主要有2种:

  1. PagerAdapter 的 getCount() 返回 Integer.MAX_VALUE ,这个非常大的数字使轮播几乎没有尽头,通过 currentItem 对数据源的大小(size)求余来获取当前页面数据。是一个简单易懂的实现方式,但不是真正意义的无限轮播。而且,在页面数量为 2 时,只能左右滚动,失去无限轮播效果(解决:可以再添加一遍数据源,达到4个页面)。
  2. 通过不断改变 PagerAdapter 的数据源,结合 ViewPager 的刷新,真正实现无限轮播。实现难度稍大。在页面数量为 2 时,也需要再添加一遍数据源。
  • ViewFlipper 也可以实现轮播。

本篇就用第二种方式实现真正意义的无限轮播。

3 具体实现

3.1 实现无限滚动

第1步: 布局
layout/activity_banner.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".banner.BannerActivity">

    <FrameLayout
        android:id="@+id/fl_banner_frame"
        android:layout_width="match_parent"
        android:layout_height="180dp">

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

        <LinearLayout
            android:id="@+id/ll_banner_dot_group"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal|bottom"
            android:layout_marginBottom="6dp"
            android:orientation="horizontal">

        </LinearLayout>

    </FrameLayout>

</LinearLayout>

layout/layout_banner_item.xml

<?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="180dp">

    <ImageView
        android:id="@+id/iv_banner_img"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@color/common_color"
        />

    <TextView
        android:id="@+id/tv_banner_desc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#1b606060"
        android:textColor="@color/color_white"
        android:text="为什么说 5G 是物联网的时代?"
        android:lines="2"
        android:layout_gravity="bottom"
        android:padding="12dp"
        />

</FrameLayout>

第2步: 准备数据

实体类:

public class BannerEntity implements Cloneable {
    
    
    private int id;
    private String imgUrl;
    private String desc;

    public BannerEntity() {
    
    
    }

    public BannerEntity(int id, String imgUrl, String desc) {
    
    
        this.id = id;
        this.imgUrl = imgUrl;
        this.desc = desc;
    }

    public int getId() {
    
    
        return id;
    }

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

    public String getImgUrl() {
    
    
        return imgUrl;
    }

    public void setImgUrl(String imgUrl) {
    
    
        this.imgUrl = imgUrl;
    }

    public String getDesc() {
    
    
        return desc;
    }

    public void setDesc(String desc) {
    
    
        this.desc = desc;
    }

    @Override
    protected BannerEntity clone() {
    
    
        return new BannerEntity(getId(), getImgUrl(), getDesc());
    }
}

初始化数据:

/**
 * 初始化数据时,如果数据量大于2, `mBannerList.size() >  2` ,无需特殊处理;
 * 如果等于2,因为 ViewPager 有离屏缓存,需要将两条数据复制一份添加到 mBannerList 中;
 * 如果等于1,不需要滚动,不作无限轮播处理。
 */
private void initData() {
    
    
    mBannerList = new ArrayList<BannerEntity>();
    int id = 0;
    mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190617/cb8be21b1f7ce2256ffd6c7b8b737a74.png", "为什么说 5G 是物联网的时代?"));
    mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190617/075ff654f74cf80660112b03e48c2896.jpg", "新技术“红”不过十年,半监督学习为什么是个例外?"));
    mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190618/354fc1a74a651de1e0291b4e9261d77c.jpg", "阿里达摩院SIGIR 2019:AI判案1秒钟,人工2小时"));
    mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190522/0e36975c84e6e3fb0e576556a1168330.png", "独家!天才少年 Vitalik:“中国开发者应多关注以太坊!”"));
    mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190618/8aee33b2a4ef11b0fb70bf371484c2ee.jpg", "不是码农,不会敲代码的她,却最懂程序员!| 人物志"));

    //记录原始数据大小,表示指示圆点数量。在下面特殊情况处理之前。
    mOriSize = mBannerList.size();

    //处理等于 2 的情况。
    if (mBannerList.size() == 2) {
    
    
        BannerEntity clone0 = mBannerList.get(0).clone();
        BannerEntity clone1 = mBannerList.get(1).clone();
        mBannerList.add(clone0);
        mBannerList.add(clone1);
    }
}

注意:资源来自 CSDN。如果不可用,可自行替换。

初始化数据时,如果数据量大于2, mBannerList.size() > 2 ,无需特殊处理;如果等于2,因为 ViewPager 有离屏缓存,需要将2条数据再次添加一遍到 mBannerList 中(id 分别为:0, 1, 0, 1),这里用到了克隆;如果等于1,不需要滚动,不作无限轮播处理。

如果要显示第一页,需要从第 1 页开始滚动,如下代码:

@Override
public void initView() {
    
    
    mFlBannerFrame = (FrameLayout) findViewById(R.id.fl_banner_frame);
    mLlBannerDotGroup = (LinearLayout) findViewById(R.id.ll_banner_dot_group);
    mVpBanner = ((ViewPager) findViewById(R.id.vp_banner));
    mPagerAdapter = new BannerPagerAdapter();
    mVpBanner.setAdapter(mPagerAdapter);

    initBanner();
}

/**
 * 初始化轮播图。
 * 
 */
private void initBanner() {
    
    
    mFlBannerFrame.setVisibility(View.VISIBLE);
    mLlBannerDotGroup.setVisibility(View.VISIBLE);
    if (mBannerList.size() > 1) {
    
    
        //根据原始数据大小创建指示圆点
        for (int i = 0; i < mOriSize; i++) {
    
    
            CheckedTextView rbDot = (CheckedTextView) getLayoutInflater().inflate(R.layout.view_banner_dot_round, mLlBannerDotGroup, false);
            mLlBannerDotGroup.addView(rbDot);
            if (i == 0) {
    
    
                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) rbDot.getLayoutParams();
                lp.leftMargin = 0;
            }
        }
        //设置当前页为 1 下标页
        mVpBanner.setCurrentItem(1);
        ((CheckedTextView) mLlBannerDotGroup.getChildAt(0)).setChecked(true);
    } else if (mBannerList.size() == 1) {
    
    
        mVpBanner.setCurrentItem(0);
        mLlBannerDotGroup.setVisibility(View.GONE);
    } else {
    
    
        mFlBannerFrame.setVisibility(View.GONE);
    }
}

如下图所示,我们不用原始数据 mBannerList 直接作为 PagerAdapter 的数据源,而是用一个集合 mDataList 作为数据源,每次切换页面时,由于 ViewPager 显示当前页面的同时会默认缓存左右两个页面,其实理论上,我们只需要从原始数据 mBannerList 中取出当前页面及左右两个页面对应的数据即可。如果要显示第一页,当前页面理论下标为 0,三条数据在mBannerList 中的位置应该为 [4, 0, 1],如果当前页面要显示第二页,页面理论下标为 1, 三条数据在 mBannerList 中的位置为 [0, 1, 2],…,如果当前页面要显示第五页,页面理论下标为 4,三条数据在 mBannerList 中的位置为 [3, 4, 0]。
resetData
我们在每次页面切换后,清空 PagerAdapter 数据 mDataList ,从 mBannerList 中取出对应的三条数据添加到 mDataList,再刷新一下界面,理论上就可以实现无限轮播效果了。

第3步: 实现适配器

    class BannerPagerAdapter extends PagerAdapter {
    
    

        private ArrayList<BannerEntity> mDataList = new ArrayList<>();

        public BannerPagerAdapter() {
    
    
            resetData(0);
        }

        public void resetData(int position) {
    
    
            if (mBannerList.size() > 1) {
    
    
                int leftPos = (mBannerList.size() + position - 1) % mBannerList.size();
                int rightPos = (position + 1) % mBannerList.size();
                mDataList.clear();
                mDataList.add(mBannerList.get(leftPos));
                mDataList.add(mBannerList.get(position));
                mDataList.add(mBannerList.get(rightPos));
            } else if (mBannerList.size() == 1){
    
    
                mDataList.add(mBannerList.get(position));
            }

        }

        public BannerEntity getItem(int position) {
    
    
            return mDataList.get(position);
        }

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

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

        @NonNull
        @Override
        public Object instantiateItem(@NonNull ViewGroup container, int position) {
    
    
            BannerEntity bannerEntity = mDataList.get(position);
            View view = LayoutInflater.from(container.getContext()).inflate(R.layout.layout_banner_item, container, false);
            ImageView ivBannerImg = view.findViewById(R.id.iv_banner_img);
            TextView tvBannerDesc = view.findViewById(R.id.tv_banner_desc);
            Glide.with(BannerActivity.this).load(bannerEntity.getImgUrl()).centerCrop().into(ivBannerImg);
            tvBannerDesc.setText(bannerEntity.getDesc());
            view.setTag(bannerEntity);
            container.addView(view);
            return bannerEntity;
        }

        @Override
        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    
    
            container.removeView(container.findViewWithTag(object));
        }

        public void updateData() {
    
    
            if (mDataList.size() > 1) {
    
    
                //传入 mBannerList 中的位置
                resetData(mBannerList.indexOf(mDataList.get(mVpBanner.getCurrentItem())));
                notifyDataSetChanged();
            }
        }

        @Override
        public int getItemPosition(@NonNull Object object) {
    
    
            if (mDataList.contains(object)) {
    
    
                return mDataList.indexOf(object);
            } else {
    
    
                return POSITION_NONE;
            }
        }
    }

代码中,BannerPagerAdapter 继承 PagerAdapter。
重写了通常的4个方法,内部处理有所不同:

  • getCount()
    返回 mDataList.size()。
  • instantiateItem(ViewGroup, int)
    返回值不是通常的 View ,而是 BannerEntity 对象;而且将 BannerEntity 对象作为 tag 缓存在每个页面的 View 中(view.setTag(bannerEntity);)。
  • isViewFromObject(View, Object)
    需要通过缓存的tag值做判断。
  • destroyItem(ViewGroup, int, Object)
    需要通过对象找到对应的View,再移除不需要的View。

还重写了 1 个不常用的方法:

  • getItemPosition(Object)
    当 adapter 的数据有变化时(增、删、改、换位),需要重写此方法,adapter 的 notifyDataSetChanged() 方法才起作用。一般情况下,我们的ViewPager页面是固定的,PagerAdapter 源码中此方法返回默认值 POSITION_UNCHANGED,调用刷新方法页面不改变。
    如果 adapter 数据有变化,需重写 getItemPosition(Object) ,根据情况返回以下三种值:
    • POSITION_UNCHANGED(默认值:没有变化)
    • POSITION_NONE(位置不存在,可能已删除)
    • [0, {@link #getCount()}) 范围内(当前新位置)

第4步:重置数据源和刷新

public void resetData(int position) {
    
    
    if (mBannerList.size() > 1) {
    
    
        int leftPos = (mBannerList.size() + position - 1) % mBannerList.size();
        int rightPos = (position + 1) % mBannerList.size();
        mDataList.clear();
        mDataList.add(mBannerList.get(leftPos));
        mDataList.add(mBannerList.get(position));
        mDataList.add(mBannerList.get(rightPos));
    } else if (mBannerList.size() == 1){
    
    
        mDataList.add(mBannerList.get(position));
    }
}

resetData

重置数据时,要清空 PagerAdapter 数据 mDataList ,从 mBannerList 中取出对应的三条数据添加到 mDataList。

为 ViewPager 设置 页面变化监听,更新数据并刷新:

    @Override
    public void setListeners() {
    
    
        mVpBanner.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
    
    
            private int mCheckedId = 0;

            @Override
            public void onPageSelected(int position) {
    
    
                super.onPageSelected(position);

                ((CheckedTextView) mLlBannerDotGroup.getChildAt(mCheckedId)).setChecked(false);
                //设置圆点高亮
                int id = mPagerAdapter.getItem(position).getId();
                ((CheckedTextView) mLlBannerDotGroup.getChildAt(id)).setChecked(true);

                mCheckedId = id;
            }

            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    
    
                super.onPageScrolled(position, positionOffset, positionOffsetPixels);
                if (positionOffset == 0) {
    
    
                    mPagerAdapter.updateData();
                }
            }
        });
    }

自此,就实现了ViewPager的无限滚动效果。

ViewPager无限滚动

3.2 添加指示圆点

数据源数据量大于 2 时,圆点数量是数据源大小;等于 2 时, 记录圆点数量为 2, 数据源数据再作特殊处理。见 initData() 方法中代码:

    /**
     * 初始化数据时,如果数据量大于2, `mBannerList.size() >  2` ,无需特殊处理;
     * 如果等于2,因为 ViewPager 有离屏缓存,需要将两条数据复制一份添加到 mBannerList 中;
     * 如果等于1,不需要滚动,不作无限轮播处理。
     */
    private void initData() {
    
    
        mBannerList = new ArrayList<BannerEntity>();
        int id = 0;
        mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190617/cb8be21b1f7ce2256ffd6c7b8b737a74.png", "为什么说 5G 是物联网的时代?"));
        mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190617/075ff654f74cf80660112b03e48c2896.jpg", "新技术“红”不过十年,半监督学习为什么是个例外?"));
        mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190618/354fc1a74a651de1e0291b4e9261d77c.jpg", "阿里达摩院SIGIR 2019:AI判案1秒钟,人工2小时"));
        mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190522/0e36975c84e6e3fb0e576556a1168330.png", "独家!天才少年 Vitalik:“中国开发者应多关注以太坊!”"));
        mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190618/8aee33b2a4ef11b0fb70bf371484c2ee.jpg", "不是码农,不会敲代码的她,却最懂程序员!| 人物志"));

        //记录原始数据大小,表示指示圆点数量。在下面特殊情况处理之前。
        mOriSize = mBannerList.size();

        //处理等于 2 的情况。
        if (mBannerList.size() == 2) {
    
    
            BannerEntity clone0 = mBannerList.get(0).clone();
            BannerEntity clone1 = mBannerList.get(1).clone();
            mBannerList.add(clone0);
            mBannerList.add(clone1);
        }
    }

在 initBanner中初始化圆点,并设置第一个圆点高亮:

    /**
     * 初始化轮播图。
     *
     */
    private void initBanner() {
    
    
        mFlBannerFrame.setVisibility(View.VISIBLE);
        mLlBannerDotGroup.setVisibility(View.VISIBLE);
        if (mBannerList.size() > 1) {
    
    
            //根据原始数据大小创建指示圆点
            for (int i = 0; i < mOriSize; i++) {
    
    
                CheckedTextView rbDot = (CheckedTextView) getLayoutInflater().inflate(R.layout.view_banner_dot_round, mLlBannerDotGroup, false);
                mLlBannerDotGroup.addView(rbDot);
                if (i == 0) {
    
    
                    LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) rbDot.getLayoutParams();
                    lp.leftMargin = 0;
                }
            }
            //设置当前页为 1 下标页
            mVpBanner.setCurrentItem(1);
            //第一个圆点高亮
            ((CheckedTextView) mLlBannerDotGroup.getChildAt(0)).setChecked(true);
        } else if (mBannerList.size() == 1) {
    
    
            mVpBanner.setCurrentItem(0);
            mLlBannerDotGroup.setVisibility(View.GONE);
        } else {
    
    
            mFlBannerFrame.setVisibility(View.GONE);
        }
    }

在 ViewPager 页面改变监听回调方法 onPageSelected 中,切换高亮圆点。

@Override
public void onPageSelected(int position) {
    
    
    super.onPageSelected(position);
    ((CheckedTextView) mLlBannerDotGroup.getChildAt(mCheckedId)).setChecked(false);
    //设置圆点高亮
    int id = mPagerAdapter.getItem(position).getId();
    ((CheckedTextView) mLlBannerDotGroup.getChildAt(id)).setChecked(true);
    mCheckedId = id;
}

用到了一个 CheckedTextView 作为圆点控件,它可以设置 selector 背景。
layout/view_banner_dot_round.xml

<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="8dp"
    android:layout_height="8dp"
    android:layout_marginLeft="10dp"
    android:background="@drawable/selector_banner_dot_round" />

3.3 自动轮播

initBanner() 方法中调用 startAutoScroll(),并在 Activity 的 OnDestroy() 中调用 stopAutoScroll() 停止轮播,防止泄漏。

	//......
	
    private class MHandler extends Handler {
    
    
        @Override
        public void handleMessage(Message msg) {
    
    
            if (msg.what == WHAT_AUTO_SCROLL) {
    
    
                int currentItem = mVpBanner.getCurrentItem();
                mVpBanner.setCurrentItem(currentItem + 1);
                mHandler.sendEmptyMessageDelayed(WHAT_AUTO_SCROLL, 2000);
            }
        }
    }
    
    //......
    
    /**
     * 初始化轮播图。
     */
    private void initBanner() {
    
    
        mFlBannerFrame.setVisibility(View.VISIBLE);
        mLlBannerDotGroup.setVisibility(View.VISIBLE);
        if (mBannerList.size() > 1) {
    
    
            //根据原始数据大小创建指示圆点
            for (int i = 0; i < mOriSize; i++) {
    
    
                CheckedTextView rbDot = (CheckedTextView) getLayoutInflater().inflate(R.layout.view_banner_dot_round, mLlBannerDotGroup, false);
                mLlBannerDotGroup.addView(rbDot);
                if (i == 0) {
    
    
                    LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) rbDot.getLayoutParams();
                    lp.leftMargin = 0;
                }
            }
            //设置当前页为 1 下标页
            mVpBanner.setCurrentItem(1);
            //第一个圆点高亮
            ((CheckedTextView) mLlBannerDotGroup.getChildAt(0)).setChecked(true);

            //开始轮播
            startAutoScroll();
        } else if (mBannerList.size() == 1) {
    
    
            mVpBanner.setCurrentItem(0);
            mLlBannerDotGroup.setVisibility(View.GONE);
        } else {
    
    
            mFlBannerFrame.setVisibility(View.GONE);
        }
    }

	//......

    private void startAutoScroll() {
    
    
        mHandler.sendEmptyMessageDelayed(WHAT_AUTO_SCROLL, 2000);
    }

    private void stopAutoScroll() {
    
    
        mHandler.removeMessages(WHAT_AUTO_SCROLL);
    }
    
    @Override
    protected void onDestroy() {
    
    
        super.onDestroy();
        stopAutoScroll();//在页面销毁时停止轮播,防止泄漏。
    }

3.4 触摸停止和点击跳转

为 mVpBanner 设置触摸监听,在手指按下 ACTION_DOWN 时,记录 x 方向位置,调用 stopAutoScroll() 停止轮播;在手指移动 ACTION_MOVE 时,记录手指 x 方向累计移动距离;在手指抬起 ACTION_UP 时,判断累计移动距离是否小于 20,按下抬起时间间隔是否小于800 毫秒,如果累计距离小于20,时间间隔小于 800 毫秒,认定为点击事件,做点击处理逻辑,之后启动轮播,重置累计移动距离为 0 。ACTION_CANCEL 中启动轮播,重置累计移动距离为 0。

		//......
        mVpBanner.setOnTouchListener(new View.OnTouchListener() {
    
    

            float currX = 0;
            float dx = 0;
            long timeMillis = 0;

            @Override
            public boolean onTouch(View v, MotionEvent event) {
    
    
                switch (event.getAction()) {
    
    
                    case MotionEvent.ACTION_DOWN:
                        currX = event.getX();
                        timeMillis = System.currentTimeMillis();
                        stopAutoScroll();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        float x = event.getX();
                        dx += Math.abs(x - currX);
                        currX = x;
                        break;
                    case MotionEvent.ACTION_UP:
                        if (dx < 20 && (System.currentTimeMillis() - timeMillis) < 800) {
    
    
                            int currentItem = mVpBanner.getCurrentItem();
                            BannerEntity item = mPagerAdapter.getItem(currentItem);
                            Toast.makeText(BannerActivity.this, item.getDesc(), Toast.LENGTH_SHORT).show();
                        }
                        startAutoScroll();
                        dx = 0;
                        break;
                    case MotionEvent.ACTION_CANCEL:
                        dx = 0;
                        startAutoScroll();
                        break;
                }
                return false;
            }
        });
        
	//......

4 总结

虽然轮播图比较常见,但它涉及的要点并不少,要想尽善尽美,也并不是那么容易。涉及到的 PagerAdapter 的刷新问题,触摸事件处理是不太好整的。

完整 BannerActivity 代码如下:

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckedTextView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.bumptech.glide.Glide;
import com.wzhy.viewpagerserial.R;
import com.wzhy.viewpagerserial.base.BaseActivity;

import java.util.ArrayList;

public class BannerActivity extends BaseActivity {
    
    

    private ViewPager mVpBanner;
    private ArrayList<BannerEntity> mBannerList;
    private BannerPagerAdapter mPagerAdapter;
    private int mOriSize;
    private LinearLayout mLlBannerDotGroup;
    private FrameLayout mFlBannerFrame;
    private MHandler mHandler;
    private static final int WHAT_AUTO_SCROLL = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_banner);
        initData();
        initView();
        setListeners();

    }


    private class MHandler extends Handler {
    
    
        @Override
        public void handleMessage(Message msg) {
    
    
            if (msg.what == WHAT_AUTO_SCROLL) {
    
    
                int currentItem = mVpBanner.getCurrentItem();
                mVpBanner.setCurrentItem(currentItem + 1);
                startAutoScroll();
            }
        }
    }


    /**
     * 初始化数据时,如果数据量大于2, `mBannerList.size() >  2` ,无需特殊处理;
     * 如果等于2,因为 ViewPager 有离屏缓存,需要将两条数据复制一份添加到 mBannerList 中;
     * 如果等于1,不需要滚动,不作无限轮播处理。
     */
    private void initData() {
    
    
        mBannerList = new ArrayList<BannerEntity>();
        int id = 0;
        mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190617/cb8be21b1f7ce2256ffd6c7b8b737a74.png", "为什么说 5G 是物联网的时代?"));
        mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190617/075ff654f74cf80660112b03e48c2896.jpg", "新技术“红”不过十年,半监督学习为什么是个例外?"));
        mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190618/354fc1a74a651de1e0291b4e9261d77c.jpg", "阿里达摩院SIGIR 2019:AI判案1秒钟,人工2小时"));
        mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190522/0e36975c84e6e3fb0e576556a1168330.png", "独家!天才少年 Vitalik:“中国开发者应多关注以太坊!”"));
        mBannerList.add(new BannerEntity(id++, "https://csdnimg.cn/feed/20190618/8aee33b2a4ef11b0fb70bf371484c2ee.jpg", "不是码农,不会敲代码的她,却最懂程序员!| 人物志"));

        //记录原始数据大小,表示指示圆点数量。在下面特殊情况处理之前。
        mOriSize = mBannerList.size();

        //处理等于 2 的情况。
        if (mBannerList.size() == 2) {
    
    
            BannerEntity clone0 = mBannerList.get(0).clone();
            BannerEntity clone1 = mBannerList.get(1).clone();
            mBannerList.add(clone0);
            mBannerList.add(clone1);
        }
    }


    @Override
    public void initView() {
    
    

        mHandler = new MHandler();

        mFlBannerFrame = (FrameLayout) findViewById(R.id.fl_banner_frame);
        mLlBannerDotGroup = (LinearLayout) findViewById(R.id.ll_banner_dot_group);
        mVpBanner = ((ViewPager) findViewById(R.id.vp_banner));
        mPagerAdapter = new BannerPagerAdapter();
        mVpBanner.setAdapter(mPagerAdapter);

        initBanner();
    }

    /**
     * 初始化轮播图。
     */
    private void initBanner() {
    
    
        mFlBannerFrame.setVisibility(View.VISIBLE);
        mLlBannerDotGroup.setVisibility(View.VISIBLE);
        if (mBannerList.size() > 1) {
    
    
            //根据原始数据大小创建指示圆点
            for (int i = 0; i < mOriSize; i++) {
    
    
                CheckedTextView rbDot = (CheckedTextView) getLayoutInflater().inflate(R.layout.view_banner_dot_round, mLlBannerDotGroup, false);
                mLlBannerDotGroup.addView(rbDot);
                if (i == 0) {
    
    
                    LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) rbDot.getLayoutParams();
                    lp.leftMargin = 0;
                }
            }
            //设置当前页为 1 下标页
            mVpBanner.setCurrentItem(1);
            //第一个圆点高亮
            ((CheckedTextView) mLlBannerDotGroup.getChildAt(0)).setChecked(true);

            //开始轮播
            startAutoScroll();
        } else if (mBannerList.size() == 1) {
    
    
            mVpBanner.setCurrentItem(0);
            mLlBannerDotGroup.setVisibility(View.GONE);
        } else {
    
    
            mFlBannerFrame.setVisibility(View.GONE);
        }
    }

    class BannerPagerAdapter extends PagerAdapter {
    
    

        private ArrayList<BannerEntity> mDataList = new ArrayList<>();

        public BannerPagerAdapter() {
    
    
            resetData(0);
        }

        public void resetData(int position) {
    
    
            if (mBannerList.size() > 1) {
    
    
                int leftPos = (mBannerList.size() + position - 1) % mBannerList.size();
                int rightPos = (position + 1) % mBannerList.size();
                mDataList.clear();
                mDataList.add(mBannerList.get(leftPos));
                mDataList.add(mBannerList.get(position));
                mDataList.add(mBannerList.get(rightPos));
            } else if (mBannerList.size() == 1) {
    
    
                mDataList.add(mBannerList.get(position));
            }

        }

        public BannerEntity getItem(int position) {
    
    
            return mDataList.get(position);
        }

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

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

        @NonNull
        @Override
        public Object instantiateItem(@NonNull ViewGroup container, int position) {
    
    
            BannerEntity bannerEntity = mDataList.get(position);
            View view = LayoutInflater.from(container.getContext()).inflate(R.layout.layout_banner_item, container, false);
            ImageView ivBannerImg = view.findViewById(R.id.iv_banner_img);
            TextView tvBannerDesc = view.findViewById(R.id.tv_banner_desc);
            Glide.with(BannerActivity.this).load(bannerEntity.getImgUrl()).centerCrop().into(ivBannerImg);
            tvBannerDesc.setText(bannerEntity.getDesc());
            view.setTag(bannerEntity);
            container.addView(view);
            return bannerEntity;
        }

        @Override
        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    
    
            container.removeView(container.findViewWithTag(object));
        }

        public void updateData() {
    
    
            if (mDataList.size() > 1) {
    
    
                //传入 mBannerList 中的位置
                resetData(mBannerList.indexOf(mDataList.get(mVpBanner.getCurrentItem())));
                notifyDataSetChanged();
            }
        }

        @Override
        public int getItemPosition(@NonNull Object object) {
    
    
            if (mDataList.contains(object)) {
    
    
                return mDataList.indexOf(object);
            } else {
    
    
                return POSITION_NONE;
            }
        }
    }


    @SuppressLint("ClickableViewAccessibility")
    @Override
    public void setListeners() {
    
    
        mVpBanner.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
    
    
            private int mCheckedId = 0;

            @Override
            public void onPageSelected(int position) {
    
    
                super.onPageSelected(position);

                ((CheckedTextView) mLlBannerDotGroup.getChildAt(mCheckedId)).setChecked(false);
                //设置圆点高亮
                int id = mPagerAdapter.getItem(position).getId();
                ((CheckedTextView) mLlBannerDotGroup.getChildAt(id)).setChecked(true);

                mCheckedId = id;

            }

            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    
    
                super.onPageScrolled(position, positionOffset, positionOffsetPixels);
                if (positionOffset == 0) {
    
    
                    mPagerAdapter.updateData();
                }
            }
        });

        mVpBanner.setOnTouchListener(new View.OnTouchListener() {
    
    

            float currX = 0;
            float dx = 0;
            long timeMillis = 0;

            @Override
            public boolean onTouch(View v, MotionEvent event) {
    
    
                switch (event.getAction()) {
    
    
                    case MotionEvent.ACTION_DOWN:
                        currX = event.getX();
                        timeMillis = System.currentTimeMillis();
                        stopAutoScroll();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        float x = event.getX();
                        dx += Math.abs(x - currX);
                        currX = x;
                        break;
                    case MotionEvent.ACTION_UP:
                        if (dx < 20 && (System.currentTimeMillis() - timeMillis) < 800) {
    
    
                            int currentItem = mVpBanner.getCurrentItem();
                            BannerEntity item = mPagerAdapter.getItem(currentItem);
                            Toast.makeText(BannerActivity.this, item.getDesc(), Toast.LENGTH_SHORT).show();
                        }
                        startAutoScroll();
                        dx = 0;
                        break;
                    case MotionEvent.ACTION_CANCEL:
                        dx = 0;
                        startAutoScroll();
                        break;
                }
                return false;
            }
        });

    }

    private void startAutoScroll() {
    
    
        mHandler.sendEmptyMessageDelayed(WHAT_AUTO_SCROLL, 2000);
    }

    private void stopAutoScroll() {
    
    
        mHandler.removeMessages(WHAT_AUTO_SCROLL);
    }

    @Override
    public void onClick(View v) {
    
    

    }

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

5 参考

轮播中的插图来自CSDN。

源码:
https://github.com/wangzhengyangNo1/ViewPagerSerialDemo

参考博客:
[1] Android无限广告轮播 - 自定义BannerView
[2] ViewPager动态添加删除及刷新页面
[3] ViewPage 联动效果自带角标
[4] ViewPager动态添加删除及刷新页面

猜你喜欢

转载自blog.csdn.net/wangxiaocheng16/article/details/92830906
今日推荐