Android开发自定义Banner实现图片无限轮播效果

之前做过的很多项目都用到了 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
  • ……
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值