- 利用ViewPage的PagerTransformer定制页面切换效果
- ViewPager动态添加删除及刷新页面
- ViewPager打造真正意义的无限轮播
- ViewPage 联动效果自带角标
- ViewPager禁止滑动和修改滑动速度
1 简述
ViewPage 不仅常用于页面导航切换,也常用来实现轮播图。百度一下,可以找到很多关于轮播图的实现文章。曾翻看过多篇相关文章,get 到一些要点,然而觉得自己实现一下,会更加深刻,如果加上自己独特的思路,也是对自己的一个锻炼,对代码一个积累。实现目标如下图所示:
2 实现思路
ViewPager 实现轮播图早已为我们所熟悉,我个人认为,实现方式主要有2种:
- 让
PagerAdapter
的 getCount() 返回Integer.MAX_VALUE
,这个非常大的数字使轮播几乎没有尽头,通过 currentItem 对数据源的大小(size)求余来获取当前页面数据。是一个简单易懂的实现方式,但不是真正意义的无限轮播。而且,在页面数量为 2 时,只能左右滚动,失去无限轮播效果(解决:可以再添加一遍数据源,达到4个页面)。- 通过不断改变
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]。
我们在每次页面切换后,清空 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));
}
}
重置数据时,要清空 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的无限滚动效果。
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动态添加删除及刷新页面