自定义轮播控件BannerView

一、介绍

在项目中使用的自动轮播控件一直是网上别人做的,在出现问题的时候去看代码细节扫雷就非常浪费时间。于是痛定思痛自己造个轮子。
这个控件在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 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中的示例写法:

    

    

注意: 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 = ArrayList()
        data.add("1111")
        data.add("2222")
        data.add("1111")
        data.add("2222")
        adapter?.addData(data)
    }
}

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

你可能感兴趣的:(自定义轮播控件BannerView)