之前做过的很多项目都用到了 Banner,不过每次项目做完都忘了总结导致每次要实现 Banner 效果都要上网查,网上的文章真的是鱼龙混杂,因此今天花点时间好好总结一下实现 Banner 的原理。
首先我们来看看要实现的效果:
实现 Banner 的思路很简单,其本质就是封装了一个 ViewPager,然后用定时任务来控制 ViewPager 的位置,再给 ViewPager 设置一个监听器,当页面改变时控制下面指示器的变化即可。
因此我们分为两个大的步骤:用 ViewPager 实现轮播效果 和 添加底部圆形指示器。
一、使用 ViewPager 实现图片无限轮播效果
因为要使用 ViewPager(对 ViewPager 用法不太熟悉的同学可以参考一下启舰大佬的文章),因此适配器是少不了的,首先我们创建一个适配器:
public class BannerPagerAdapter extends PagerAdapter {
private static final int ITEM_COUNT = 3; //要滑动的Item的个数
private List<View> viewList; //要滑动的Item
public BannerPagerAdapter(List<View> viewList) {
this.viewList = viewList;
}
@Override
public int getCount() {
return viewList.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = viewList.get(position);
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView(viewList.get(position));
}
}
这一段代码是实现一个适配器的基本用法,没什么难度就不谈了。接下来我们创建一个 BannerView 类来将上面的 ViewPager 封装起来:
public class BannerView extends RelativeLayout {
private static final int ITEM_COUNT = 3; //要滑动的Item的个数
private Context context;
//ViewPager相关
private ViewPager viewPager; //ViewPager
private List<View> viewList; //要轮播的图片
private List<Integer> imgResourceList; //轮播图片的资源Id
private RotationHandler rotationHandler;
/**
* 构造函数
*/
public BannerView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
/**
* 初始化
*/
private void init(){
initViewPager();
}
/**
* 初始化ViewPager
*/
private void initViewPager(){
//创建ViewPager
viewPager = new ViewPager(context);
viewList = new ArrayList<>();
//创建图片资源集合并添加元素
imgResourceList = new ArrayList<>();
imgResourceList.add(R.drawable.img3);
imgResourceList.add(R.drawable.img1);
imgResourceList.add(R.drawable.img2);
imgResourceList.add(R.drawable.img3);
imgResourceList.add(R.drawable.img1);
//向viewList添加图片
for (int i=0; i<imgResourceList.size(); i++){
ImageView imageView = new ImageView(context);
imageView.setImageResource(imgResourceList.get(i));
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
viewList.add(imageView);
}
//为ViewPager添加适配器
viewPager.setAdapter(new BannerPagerAdapter(viewList));
//将ViewPager添加到BannerView中
addView(viewPager, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
//初始位置
viewPager.setCurrentItem(1, false);
//ViewPager页面监听
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageSelected(int position) {
Log.d("hahaha", "position = " + position);
if (position == viewList.size()-1){
viewPager.setCurrentItem(1, false);
} else if (position == 0){
viewPager.setCurrentItem(viewList.size() - 2, false);
}
}
@Override
public void onPageScrollStateChanged(int state) {}
});
//创建Handler
rotationHandler = new RotationHandler(this);
rotationHandler.sendEmptyMessageDelayed(1, 2000);
}
/**
* Handler实现定时任务
*/
private static class RotationHandler extends Handler {
private WeakReference<BannerView> bannerReference; //弱引用放置内存泄漏
public RotationHandler(BannerView bannerView) {
bannerReference = new WeakReference<>(bannerView);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
sendEmptyMessageDelayed(1, 2000);
BannerView bannerView = bannerReference.get();
bannerView.viewPager.setCurrentItem(bannerView.viewPager.getCurrentItem() + 1);
}
}
}
上面的代码稍微有点多,不过难度不是很大。解释一下,因为我们的 Banner 既有图片又有指示器,因此要把两者组合起来,所以我们选择让 BannerView 继承自系统布局 RelativeLayout。然后在构造函数中调用了 init 函数,其中又调用了 initViewPager 方法对 ViewPager 进行初始化。
在 initViewPager 方法中,首先我们 new 出了 ViewPager 和 List,并将图片添加到 List 中:
//创建ViewPager
viewPager = new ViewPager(context);
viewList = new ArrayList<>();
//创建图片资源集合并添加元素
imgResourceList = new ArrayList<>();
imgResourceList.add(R.drawable.img3);
imgResourceList.add(R.drawable.img1);
imgResourceList.add(R.drawable.img2);
imgResourceList.add(R.drawable.img3);
imgResourceList.add(R.drawable.img1);
//向viewList添加图片
for (int i=0; i<imgResourceList.size(); i++){
ImageView imageView = new ImageView(context);
imageView.setImageResource(imgResourceList.get(i));
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
viewList.add(imageView);
}
这里需要注意一下,虽然我们只要轮播显示 3 张图片,但是在 imgResourceList 中却添加了 5 个 resource,这是因为我们要实现轮播效果,如果只添加了 3 个图片那么在 3 到 1 进行切换的时候就会出现回退情况。其左右滑动的示意图如下:
有了这个示意图之后,接下来的代码就很容易理解了:
//为ViewPager添加适配器
viewPager.setAdapter(new BannerPagerAdapter(viewList));
//将ViewPager添加到BannerView中
addView(viewPager, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
//初始位置
viewPager.setCurrentItem(1, false);
//ViewPager页面监听
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageSelected(int position) {
if (position == viewList.size()-1){ //如果滑到了右边界
viewPager.setCurrentItem(1, false);
} else if (position == 0){ //如果滑到了左边界
viewPager.setCurrentItem(viewList.size() - 2, false);
}
}
@Override
public void onPageScrollStateChanged(int state) {}
});
核心就是在 onPageSelected 回调中进行判断,如果到了边界则快速切换回正确位置。接下来的代码就是通过 Handler 发送延时消息,因为我们在 handleMessage 方法中调用了 sendEmptyMessageDelayed 方法,因此每次处理之后又会发送一条延时消息(不断循环)。
private void initViewPager(){
……
//创建Handler
rotationHandler = new RotationHandler(this);
rotationHandler.sendEmptyMessageDelayed(1, 2000);
}
private static class RotationHandler extends Handler {
private WeakReference<BannerView> bannerReference; //弱引用放置内存泄漏
public RotationHandler(BannerView bannerView) {
bannerReference = new WeakReference<>(bannerView);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
sendEmptyMessageDelayed(1, 2000); //2s发一次
BannerView bannerView = bannerReference.get();
//每次受到消息将viewpager往后滑一页
bannerView.viewPager.setCurrentItem(bannerView.viewPager.getCurrentItem() + 1);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
rotationHandler.removeCallbacksAndMessages(null);
}
这里需要注意的是,为了防止内存泄漏我们让 Handler 持有 BannerView 的弱引用,而且在 BannerView 从 Window 中被剥离时撤回所有消息。我们运行一下代码看看效果:
发现已经能够实现自动轮播功能了。
二、添加圆形指示器
为 Banner 底部添加圆形指示器的做法也非常简单,我们新增一个 initDot 方法如下:
private LinearLayout dotLayout; //水平线性布局
private void init(){
initViewPager();
initDot();
}
/**
* 初始化指示器
*/
private void initDot(){
dotLayout = new LinearLayout(context);
dotLayout.setOrientation(LinearLayout.HORIZONTAL); //水平布局
//添加圆点
for (int i=0; i<ITEM_COUNT; i++){
ImageView dotImage = new ImageView(context);
if (i == 0){
dotImage.setImageResource(R.drawable.dot_white);
} else {
dotImage.setImageResource(R.drawable.dot_gray);
}
//为ImageView创建布局参数
MarginLayoutParams layoutParams = new MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
//设置边距
layoutParams.leftMargin = 10;
layoutParams.rightMargin = 10;
//动态
dotLayout.addView(dotImage, layoutParams);
}
//为dotLayout设置布局参数
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.bottomMargin = 30;
layoutParams.addRule(CENTER_HORIZONTAL);
layoutParams.addRule(ALIGN_PARENT_BOTTOM);
addView(dotLayout, layoutParams);
}
上面的代码也非常的简单,因为指示器是水品线性排列,因此我们 new 出一个水平线性布局 dotLayout 并动态的为其添加 3 个子 View,最后将 doLayout 添加到 BannerView中。
除此之外,我们还要在 ViewPager 的 onPageSelected 回调中进行监听,当页面发生变化时改变指示器的颜色:
for (int i=0; i<ITEM_COUNT; i++){
ImageView imageView = (ImageView) dotLayout.getChildAt(i);
if (i == viewPager.getCurrentItem() - 1){
//当前页面设为白色圆点
imageView.setImageResource(R.drawable.dot_white);
} else {
//其他页面灰色圆点
imageView.setImageResource(R.drawable.dot_gray);
}
}
这样我们的 BannerView 就完成了,接下来在布局文件中添加这个控件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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=".MainActivity">
<com.bannertest.BannerView
android:layout_width="match_parent"
android:layout_height="250dp"/>
</RelativeLayout>
再次运行,就得到了开篇的效果。
BannerView 的所有代码如下:
public class BannerView extends RelativeLayout {
private static final int ITEM_COUNT = 3; //要滑动的Item的个数
private Context context;
//ViewPager相关
private ViewPager viewPager;
private List<View> viewList; //要轮播的图片
private List<Integer> imgResourceList; //轮播图片的资源Id
private RotationHandler rotationHandler;
//指示器相关
private LinearLayout dotLayout; //水平线性布局
/**
* 构造函数
*/
public BannerView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
/**
* 初始化
*/
private void init(){
initViewPager();
initDot();
}
/**
* 初始化ViewPager
*/
private void initViewPager(){
//创建ViewPager
viewPager = new ViewPager(context);
viewList = new ArrayList<>();
//创建图片资源集合并添加元素
imgResourceList = new ArrayList<>();
imgResourceList.add(R.drawable.img3);
imgResourceList.add(R.drawable.img1);
imgResourceList.add(R.drawable.img2);
imgResourceList.add(R.drawable.img3);
imgResourceList.add(R.drawable.img1);
//向viewList添加图片
for (int i=0; i<imgResourceList.size(); i++){
ImageView imageView = new ImageView(context);
imageView.setImageResource(imgResourceList.get(i));
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
viewList.add(imageView);
}
//为ViewPager添加适配器
viewPager.setAdapter(new BannerPagerAdapter(viewList));
//将ViewPager添加到BannerView中
addView(viewPager, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
//初始位置
viewPager.setCurrentItem(1, false);
//ViewPager页面监听
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageSelected(int position) {
Log.d("hahaha", "position = " + position);
if (position == viewList.size()-1){
viewPager.setCurrentItem(1, false);
} else if (position == 0){
viewPager.setCurrentItem(viewList.size() - 2, false);
}
for (int i=0; i<ITEM_COUNT; i++){
ImageView imageView = (ImageView) dotLayout.getChildAt(i);
if (i == viewPager.getCurrentItem() - 1){
imageView.setImageResource(R.drawable.dot_white);
} else {
imageView.setImageResource(R.drawable.dot_gray);
}
}
}
@Override
public void onPageScrollStateChanged(int state) {}
});
//创建Handler
rotationHandler = new RotationHandler(this);
rotationHandler.sendEmptyMessageDelayed(1, 2000);
}
/**
* 初始化指示器
*/
private void initDot(){
dotLayout = new LinearLayout(context);
dotLayout.setOrientation(LinearLayout.HORIZONTAL); //水平布局
//添加圆点
for (int i=0; i<ITEM_COUNT; i++){
ImageView dotImage = new ImageView(context);
if (i == 0){
dotImage.setImageResource(R.drawable.dot_white);
} else {
dotImage.setImageResource(R.drawable.dot_gray);
}
MarginLayoutParams layoutParams = new MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.leftMargin = 10;
layoutParams.rightMargin = 10;
dotLayout.addView(dotImage, layoutParams);
}
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.bottomMargin = 30;
layoutParams.addRule(CENTER_HORIZONTAL);
layoutParams.addRule(ALIGN_PARENT_BOTTOM);
addView(dotLayout, layoutParams);
}
/**
* Handler实现定时任务
*/
private static class RotationHandler extends Handler {
private WeakReference<BannerView> bannerReference; //弱引用放置内存泄漏
public RotationHandler(BannerView bannerView) {
bannerReference = new WeakReference<>(bannerView);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
sendEmptyMessageDelayed(1, 2000);
BannerView bannerView = bannerReference.get();
bannerView.viewPager.setCurrentItem(bannerView.viewPager.getCurrentItem() + 1);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
rotationHandler.removeCallbacksAndMessages(null);
}
}
总结:本文的代码实现了一个简易的 BannerView ,学习其思想已经足够用了。如果想将其用到项目中去,还需要对其进行进一步完善:
- 使用自定义属性,让开发者可以在 xml 中指定轮播图的 Item
- 底部的指示器在滑动时可以有更花哨的效果
- 让网络加载的图片可以作为滑动的 Item
- ……