自定义轮播控件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<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写的,不过调用就这几个方法,应该没什么看不懂的地方了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容