自定义轮播控件BannerView

作者: AmStrong_ | 来源:发表于2018-04-16 14:17 被阅读199次

    一、介绍

    在项目中使用的自动轮播控件一直是网上别人做的,在出现问题的时候去看代码细节扫雷就非常浪费时间。于是痛定思痛自己造个轮子。
    这个控件在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写的,不过调用就这几个方法,应该没什么看不懂的地方了。

    相关文章

      网友评论

        本文标题:自定义轮播控件BannerView

        本文链接:https://www.haomeiwen.com/subject/cgockftx.html