自定义View之轮播控件BannerView

11人阅读 评论(0) 收藏 举报
分类:

一、介绍

在项目中使用的自动轮播控件一直是网上别人做的,在出现问题的时候去看代码细节扫雷就非常浪费时间。于是痛定思痛自己造个轮子。
这个控件在app中使用非常频繁,并且原理也不复杂,就是在前后各加一页。相信每一个android开发者都会做这个东西。
功能介绍:
1.无限自动轮播。
2.指示器(下方的小点点)
3.滚动动画时间可调
4.拖拽的时候停止轮播
实际效果图

全部代码和示例代码已经上传到GitHub上了:
https://github.com/CuteWen/BannerView
有兴趣的可以下过来看看。

二、实现

首先要自定义一个View去继承ViewPager
然后我们自动轮播实现的关键其实都在PagerAdapter里面,我们可以自己封装一个PagerAdapter,但是自己封装的Adapter就会让使用者在写逻辑的时候要了解你的adapter封装到什么程度了,放出哪些方法,个人不太喜欢那样子,所以我这里使用了装饰者模式来扩展使用者写好的Adapter,这样使用的时候只要写一个最普通的PagerAdapter 就可以附加上自动轮播的功能了。

注:不太懂装饰者模式的同学可以去这里看一下,里面讲解的挺好的。
https://www.cnblogs.com/chenxing818/p/4705919.html

1.包装类

思考一下我们需要包装的功能,其实也就是要将页数+2,主要就是getCount这个方法了,另外在里面也要写好两个适配器之间的position转化的方法,统一调用这些方法可以避免逻辑的混乱。
下面就是我们的包装类了。

/**
     * 适配器的包装类---------------------------------------------------------
     */
    private class BannerAdapterWrapper extends PagerAdapter {
        private PagerAdapter pagerAdapter;

        public BannerAdapterWrapper(PagerAdapter pagerAdapter) {
            this.pagerAdapter = pagerAdapter;
        }

        @Override
        public int getCount() {
            return pagerAdapter.getCount() > 1 ? pagerAdapter.getCount() + 2 : pagerAdapter.getCount();
        }

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

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            return pagerAdapter.instantiateItem(container, bannerToAdapterPosition(position));
        }

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

        /**
         * 展示出的position和实际的position 转换
         */
        public int bannerToAdapterPosition(int position) {
            int adapterCount = pagerAdapter.getCount();
            if (adapterCount <= 1) return 0;
            int adapterPosition = (position - 1) % adapterCount;
            if (adapterPosition < 0) adapterPosition += adapterCount;
            return adapterPosition;
        }

        public int toWrapperPosition(int position) {
            return position + 1;
        }
    }

主要做了:
1.getCount的上限加了2 也就是前后各多一页的作用。
2.写了两个适配器之间的position之间的转换方法方便调用。

2.暗度陈仓(AdapterWrapper)之后的善后工作

看一下setAdapter方法:

 /**
     * 设置适配器的时候做初始化工作
     */
    @Override
    public void setAdapter(PagerAdapter adapter) {
        this.adapter = adapter;
        //注册原适配器刷新时的监听
        this.adapter.registerDataSetObserver(new BannerPagerObserver());
        //初始化包装适配器
        bannerAdapterWrapper = new BannerAdapterWrapper(adapter);
        //实际配置的adapter是包装后的适配器
        super.setAdapter(bannerAdapterWrapper);
        //注册适配器的监听 (这个在后文介绍)
        addOnPageChangeListener(new BannerPageChangeListener());
        //初始化handler处理定时事件 (这个在后文介绍)
        looperHandler = new LooperHandler(this);
    }

这里注册了一个DataSetObserver,这个平时用到的还比较少,它是用来监听Adapter.notifyDataSetChanged()的。
因为我们实际上绑定BannerView的是Wrapper之后的适配器adapter,而使用者手里调用的是原adapter的notifyDataSetChanged(),所以需要进行一个传递过程!

/**
     * 数据刷新 传递刷新信号-----------------------------------------------------
     */
    private class BannerPagerObserver extends DataSetObserver {

        @Override
        public void onChanged() {
            super.onChanged();
            dataSetChanged();
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();
            dataSetChanged();
        }
    }

    /**
     * 刷新数据方法
     */
    private void dataSetChanged() {
        if (bannerAdapterWrapper != null && pagerAdapter.getCount() > 0) {
            bannerAdapterWrapper.notifyDataSetChanged();
            bannerIndicatorView.setCount(pagerAdapter.getCount());
            setCurrentItem(0);
        }
    }

同理,我们在调用setCurrentItem()方法的时候position也是不一样的。


    @Override
    public void setCurrentItem(int item, boolean smoothScroll) {
        super.setCurrentItem(bannerAdapterWrapper.toWrapperPosition(item), smoothScroll);
    }

    @Override
    public void setCurrentItem(int item) {
        super.setCurrentItem(bannerAdapterWrapper.toWrapperPosition(item));
    }

    @Override
    public int getCurrentItem() {
        return bannerAdapterWrapper.bannerToAdapterPosition(super.getCurrentItem());
    }

3.翻页监听

    /**
     * 监听翻页----------------------------------------------------------------
     */
   private class BannerPageChangeListener implements OnPageChangeListener {

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

        }

        @Override
        public void onPageSelected(int position) {
            // 在这里同步指示器
            if (bannerIndicatorView != null) {
                bannerIndicatorView.setSelect(bannerAdapterWrapper.bannerToAdapterPosition(position));
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            int position = BannerView.super.getCurrentItem();
            // 无限轮播的跳转
            if (state == ViewPager.SCROLL_STATE_IDLE &&
                    (position == 0 || position == bannerAdapterWrapper.getCount() - 1)) {
                setCurrentItem(bannerAdapterWrapper.bannerToAdapterPosition(position), false);
            }
            // 手指拖动翻页的时候暂停自动轮播
            if (state == ViewPager.SCROLL_STATE_IDLE) {
                if (timer == null) {
                    timer = new Timer();
                    timer.schedule(new TimerTask() {
                        @Override
                        public void run() {
                            looperHandler.sendEmptyMessage(0);
                        }
                    }, intervalTime + scrollTime, intervalTime + scrollTime);
                }
            } else if (state == ViewPager.SCROLL_STATE_DRAGGING) {
                if (timer != null) {
                    timer.cancel();
                    timer = null;
                }
            }
        }
    }

里面的同步指示器和暂停自动轮播代码暂且不表。
主要就是无限轮播的跳转那一段代码 完成“无限”的实现。

4.自动轮播

这里我们使用了Timer+Handler的组合来完成定时滑动的操作:

    /**
     * 设置间隔时间 并开始Timer任务
     */
    public void setIntervalTime(int intervalTime) {
        this.intervalTime = intervalTime;
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                looperHandler.sendEmptyMessage(0);
            }
        }, intervalTime + scrollTime, intervalTime + scrollTime);
    }

    /**
     * 处理定时任务-------------------------------------------------------------------
     */
    private static class LooperHandler extends Handler {
        private WeakReference<BannerView> weakReference;

        public LooperHandler(BannerView bannerView) {
            this.weakReference = new WeakReference<>(bannerView);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            weakReference.get().setCurrentItem(weakReference.get().getCurrentItem() + 1);
        }
    }

另外还有设置滚动的时间,这里需要使用一下反射去修改mScroller这个对象。

    /**
     * 设置滚动时间  利用反射
     */
    public void setScrollTime(int scrollTime) {
        try {
            Field field = ViewPager.class.getDeclaredField("mScroller");
            field.setAccessible(true);
            FixedSpeedScroller scroller = new FixedSpeedScroller(getContext(),
                    new AccelerateInterpolator());
            field.set(this, scroller);
            scroller.setScrollDuration(scrollTime);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

/**
     * 修改ViewPager的滑动动画时间-----------------------------------------------------------
     */
    private class FixedSpeedScroller extends Scroller {
        private int duration = 300;

        public FixedSpeedScroller(Context context, Interpolator interpolator) {
            super(context, interpolator);
        }

        @Override
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
            super.startScroll(startX, startY, dx, dy, this.duration);
        }

        @Override
        public void startScroll(int startX, int startY, int dx, int dy) {
            super.startScroll(startX, startY, dx, dy, this.duration);
        }

        public void setScrollDuration(int duration) {
            this.duration = duration;
        }
    }

5. 指示器

先上代码

public class BannerIndicatorView extends View {
    private int count;
    private int select;

    private Paint pointPaint;
    private Paint selectPaint;
    private String selectColor = "#FFFFFF";
    private String normalColor = "#80FFFFFF";

    private int radius = 10;
    private int interval = 10;

    public BannerIndicatorView(Context context) {
        this(context, null);
    }

    public BannerIndicatorView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BannerIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        pointPaint = new Paint();
        pointPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        pointPaint.setColor(Color.parseColor(normalColor));
        pointPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        selectPaint = new Paint();
        selectPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        selectPaint.setColor(Color.parseColor(selectColor));
        selectPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 画出各个点的位置
        for (int i = 0; i < count; i++) {
            if (i == select) {
                canvas.drawCircle(radius + i * (radius * 2 + interval), getHeight() / 2, radius, selectPaint);
            } else {
                canvas.drawCircle(radius + i * (radius * 2 + interval), getHeight() / 2, radius, pointPaint);
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = count * radius * 2 + (count - 1) * interval;
        int height = radius * 2;
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    /**
     * 设置第几个点选中,然后刷新
     */
    public void setSelect(int select) {
        this.select = select;
        invalidate();
    }

    /**
     * 设置个数
     */
    public void setCount(int c) {
        count = c;
    }

    public void setSelectColor(String selectColor) {
        this.selectColor = selectColor;
    }

    public void setNormalColor(String normalColor) {
        this.normalColor = normalColor;
    }
}

这部分还是比较简单的,就是绘制了几个白色小圆点,然后提供setSelect的方法来变化选中点。

然后在BannerView里面写上setIndicator()的方法

    /**
     * 设置指示器,需要在setAdapter之后
     */
    public void setIndicator(BannerIndicatorView bannerIndicatorView) {
        this.bannerIndicatorView = bannerIndicatorView;
        if (pagerAdapter != null) {
            bannerIndicatorView.setCount(pagerAdapter.getCount());
        }
    }

三:示例与全部代码

在XML中的示例写法:

    <com.wzl.custom.BannerView
        android:id="@+id/bv_activity_banner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <com.wzl.custom.BannerIndicatorView
        android:id="@+id/biv_activity_banner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/bv_activity_banner"
        android:layout_marginBottom="7dp"
        android:layout_centerHorizontal="true"
        />

注意: android:layout_centerHorizonta = “true” 是为了让点居中。

class BannerActivity : AppCompatActivity() {
    var bannerView: BannerView? = null
    var indicatorView: BannerIndicatorView? = null
    var adapter: BannerAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_banner)
        bannerView = findViewById(R.id.bv_activity_banner) as BannerView
        indicatorView = findViewById(R.id.biv_activity_banner) as BannerIndicatorView
        adapter = BannerAdapter(this)
        // 设置adapter
        bannerView?.adapter = adapter
        // 绑定指示器
        bannerView?.setIndicator(indicatorView)
        // 滚动动画的时间
        bannerView?.setScrollTime(500)
        // 设置轮播间隔
        bannerView?.setIntervalTime(3000)
        val data:ArrayList<String> = ArrayList()
        data.add("1111")
        data.add("2222")
        data.add("1111")
        data.add("2222")
        adapter?.addData(data)
    }
}

这部分使用kotlin写的,不过调用就这几个方法,应该没什么看不懂的地方了。

查看评论

android BannerView实现自动轮播广告的用法(参考自github)

1、首先依赖这个包: compile 'xyz.eraise:bannerview:1.0.5' 2、最重要的是要添加网络权限: android:name="android.permission...
  • u012842688
  • u012842688
  • 2016-08-11 17:16:47
  • 1739

使用BannerView做无限轮播

之前在做无限轮播的时候一直在使用ViewPager可能是我比较守旧的缘故吧,一直到这几天又要实现此项功能了,听朋友说BannerView特别好用并且是真正意义上的无限轮播,再也不是之前的Integer...
  • zhourui_1021
  • zhourui_1021
  • 2017-02-10 16:14:51
  • 281

一款支持无限轮播、简单易用、扩展性强且超级稳定的轮播图库-Banner(BannerView)

按照惯例先上效果图: 写在前面GitHub上也是有比较详细的使用介绍的,如果你想直接看GitHub上的也可以直接点击后面的传送门去往GitHub。我是传送门本文的内容可能有点长,如果你想要直...
  • kelin410
  • kelin410
  • 2017-11-09 17:00:40
  • 320

Android动态加载轮播图BannerView

轮播图在每个app中扮演着一个点缀的角色,在独立做了三款app后都有这个需求,所以我决定把它单独抽出来。以后只需copy,然后再根据需求改一下即可。 /** * 加载网络轮播图 *@aut...
  • Jiang_Rong_Tao
  • Jiang_Rong_Tao
  • 2016-06-03 10:43:58
  • 3130

自定义BannerView,显示下个一个view部分界面

先上效果图:                       一.view的分析            首先看到view的轮播效果,一般人会想到用viewpager来实现这个功能,但是viewpager之...
  • qq_16782391
  • qq_16782391
  • 2017-11-22 16:04:28
  • 192

Android自定义控件 之 轮播图(ViewPager)重点及学习

记录学习一下轮播图的使用方法:首先如下图所示,要求达到轮播图的基本功能:1. 让图片滑动起来(ViewPager) 2. 让图片和文字,指示器对应起来 3. 让轮播器无限循环 4. 轮播器自动轮询从以...
  • ITermeng
  • ITermeng
  • 2016-08-10 15:31:41
  • 5543

Android自定义View(七)--很low的bannerView

最近公司的项目需要实现类似淘宝、京东首页的广告banner,作为一个专业的“拷贝型”程序猿,在github上搜了一下,还是挺多的,其中还有两个比较好的,扩展性和特效都不错,看得我很是羡慕啊,有兴趣的童...
  • ykb19891230
  • ykb19891230
  • 2016-03-02 13:31:37
  • 1827

自定义ViewGroup实现循环轮播ViewPager

自定义Viewgroup实现ViewPager轮播
  • qq_16009381
  • qq_16009381
  • 2017-06-12 01:51:16
  • 352

自定义View实现广告位轮播图barner组件

自定义View实现广告位轮播图barner组件
  • ligangying
  • ligangying
  • 2016-07-14 16:10:37
  • 2342

Android 第三方轮播图控件ConvenientBanner:通用的广告栏控件

首先我先声明一下这是一个三方控件,我在这里仅仅是介绍一下用法,当然你也许会说不就是一个轮播图吗?还用得着用第三方的,真是菜比,没错,我确实是菜比,不过有现成的好用的简单的 为何还一定要自己造轮子,自己...
  • Liang_WK
  • Liang_WK
  • 2017-09-13 09:12:36
  • 1423
    个人资料
    持之以恒
    等级:
    访问量: 5222
    积分: 334
    排名: 23万+
    最新评论