自定义Banner控件

自定义轮播的Banner网上有很多资源,经过自己思考后决定想尝试下自己写一个更为通用的Banner。

Banner支持的功能

  1. 支持只有图片轮播
  2. 支持图片+圆点提示轮播(三点位置支持左,中,右)
  3. 支持图片+圆点提示+文字提示+提示背景色轮播
  4. 支持自定义扩展,继承BaseBannerIndicator
    提醒:详细的使用文档请查看github上的wiki
    Demo github地址

Banner类

Banner内部简介

  1. Banner是继承一个FrameLayout,继承的本质是为了能提供改变Banner的四个角的属性。
  2. Banner内部实例化一个ViewPager,将其添加在第一个层级上。
  3. Banner根据Attribute属性选择生成对应的Indicator(本质是View)实例,如果没选择,可以为空。如果不为空,添加到第二个层级上。所以Indicator是在ViewPager上方的,背景色会遮挡ViewPager部分内容,背景色属性与View的background属性相同。
  4. 轮播机制的实现是用Handler与sendMessageDelay来实现的。

Banner内部实现介绍

ViewPager的Adapter

  1. 继承getCount抽象方法,告知Adapter里含有的Pager数量
  2. 重写instantiateItem(container: ViewGroup, position: Int)方法,以便可以复用itemView
  3. 重写destroyItem(container: ViewGroup, position: Int, object: Any?)方法,itemView在onDestroy的时候从container里面移除。
BannerPagerAdapter关键代码
//原理上实现了无限右滑动
 override fun getCount(): Int {
        return Int.MAX_VALUE
 }
//获取外部接口IBannerItem返回的ItemView
override fun instantiateItem(container: ViewGroup, position: Int): Any {
        val itemBanner = getBannerItem(position)
        val itemView = itemBanner.getChildView(container)
        itemView.tag = itemBanner
        itemView.setOnClickListener(this)
        container.addView(itemView)
        return itemView
    }
//移除不需要itemView
 override fun destroyItem(container: ViewGroup, position: Int, `object`: Any?) {
        (`object` as? View)?.let {
            container.removeView(it)
        }
    }

BannerIndicator初始化

  1. BaseBannerIndicator类主要是保存ViewPager引用,在ViewPager状态发生变化的时候,能通知到对应子类的Indicator。
  2. 每次ViewPager切换的时候,代表着数据源会发生变化,所以对应要触发requestLayout(),重新走一次完整View的measure,Layout,onDraw方法。
  3. 子类的Indicator继承父类的getMeasureWidth(widthMeasureSpec: Int)和getMeasureWidth(widthMeasureSpec: Int)方法,在里面根据specMode判断Params的类型,以此来计算出子类在对应的Params类型下该返回多大的宽与高。
  4. 子类继承onDrawView方法后,在里面计算起始x,y坐标,绘制自己需要的字体,图像等。
以PointBannerIndicator代码为例
//方法1:BaseBannerIndicator保存了ViewPager的引用,实现了ViewPager.OnPageChangeListener,每当ViewPager切换时候都会接受到回调。
//方法2:每次回调都会触发requestLayout(),从而达到上诉方法2的效果。
 override fun onPageSelected(position: Int) {
        mCurrentPage = position
        requestLayout()
        if (mListener != null) {
            mListener!!.onPageSelected(position)
        }
    }

//方法3:子View对应要告知父布局,在对应Params模式下,自身的宽与高,这里就用宽度做例子讲明就可以了。
override fun getMeasureWidth(widthMeasureSpec: Int): Int {
        var result: Int
        val specMode = MeasureSpec.getMode(widthMeasureSpec)
        val specSize = MeasureSpec.getSize(widthMeasureSpec)

        if (specMode == MeasureSpec.EXACTLY || mViewPager == null) {
            //该specMode对应的是match_parent,所以自身的宽度应该与父布局的宽度一样。
           //android艺术探索:EXACTLY -> match_parent,;AT_MOST -> wrap_content;UNSPECIFIED(子View要多大,父就给多大,我也希望有这种金主爸爸) 
            result = specSize
        } else {
            //获取绘制的圆点个数 => 得出所需的宽度 => AT_MOST模式下取ParentWidth和CircleWidth的最小值。
            val count = if (mRealSize == 0) mViewPager!!.adapter!!.count else mRealSize
            result = (paddingLeft + paddingRight + count * 2 * mBaseIndicatorParams.circleRadius + (count - 1) * mBaseIndicatorParams.offset)
            //Respect AT_MOST value if that was what is called for by measureSpec
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize)
            }
        }
        return result
    }

//方法4:计算绘制圆点的(x,y)和确定圆点显示位置
override fun onDrawView(canvas: Canvas) {
        if (mViewPager == null) {
            return
        }
        val count = mViewPager!!.adapter!!.count
        if (count == 0 || mRealSize == 0) {
            return
        }
        if (mCurrentPage >= count) {
            setCurrentItem(count - 1)
            return
        }
        //计算圆点的y坐标,(x,y)为起始点,radius为半径绘制一个圆
        val yOffset = (paddingTop + mBaseIndicatorParams.circleRadius).toFloat()

        var dX: Float
        var dY: Float

        for (iLoop in 0 until mRealSize) {
            //确定圆点的显示位置:left,center,right
            dX = switchGravity(iLoop).toFloat()
            dY = yOffset
            // Only paint fill if not completely transparent
            if (mSelectPaint.alpha > 0) {
                canvas.drawCircle(dX, dY, mBaseIndicatorParams.circleRadius.toFloat(), mUnSelectPaint)
            }
        }

        val current = if (mRealSize == 0) mCurrentPage else mCurrentPage % mRealSize
        dX = switchGravity(current).toFloat()
        dY = yOffset
        canvas.drawCircle(dX, dY, mBaseIndicatorParams.circleRadius.toFloat(), mSelectPaint)
        return
    }

ItemView布局初始化

  1. 创建一个Banner基类,将抽象方法createItemView与getLayoutId结合,这样暴露外面的方法只需要getLayoutId和onViewCreate就可以了。
  2. 创建ImageTextBanner继承基类,实现IBannerDataCallback接口,返回数据包含的图片和文字。
基类代码
//方法1:创建布局上,对外只需继承getLayoutId抽象方法即可。
abstract class BaseImageBanner : Banner.BaseBannerItem() {
    override fun createItemView(viewGroup: ViewGroup): View {
        val view = LayoutInflater.from(viewGroup.context).inflate(getLayoutId(), viewGroup, false)
        onViewCreate(view)
        return view
    }
    abstract fun onViewCreate(itemView: View)
}
//方法2:继承基类,加载数据
class ImageTextBanner(val b: BannerBean.DataBean) : BaseImageBanner(), IBannerDataCallback {
    override fun getImageUrl(): String {
        return b.image
    }
    override fun getShowText(): String {
        return b.title
    }

    lateinit var mImageView: SimpleDraweeView
    //复用View的时候需要重新加载正确的图片,文字是onDrawView时候绘制的,所以无需外部重新设置。
    override fun useOldView(itemView: View) {
        mImageView = itemView.findViewById(R.id.draweeView) as SimpleDraweeView
        mImageView.setImageURI(b.image)
    }

    override fun getLayoutId(): Int {
        return R.layout.item_banner
    }

    override fun onViewCreate(itemView: View) {
        mImageView = itemView.findViewById(R.id.draweeView) as SimpleDraweeView
        mImageView.setImageURI(b.image)
    }
}

定时轮播

  1. 主要通过handler.sendMessageDelay()方法来实现,但是sendMesageDelay只能保证不被提前执行,无法保证准时执行,如果不明白的可以网上搜下资料看下。
  2. 当ViewPager处于滑动状态时候,本次message无效,然后重新发送一次新的delay message事件。
  3. 收到有效的message时间后,调用viewPager.setCurrentItem(position : Int)实现自动滑动效果。
Handler关键代码
override fun handleMessage(msg: Message?): Boolean {
        when (msg?.what) {
            MSG_FLIP -> {
                mHandler?.let {
                    it.removeMessages(MSG_FLIP)
                    if (mIndicator != null) {
                        if (mIndicator!!.isScrollIdle()) {
                            it.sendEmptyMessageDelayed(MSG_FLIP, mBannerParams.flipTime.toLong())
                            showNextItem()
                        } else {
                            it.sendEmptyMessageDelayed(MSG_FLIP, BANNER_START_DELAY.toLong())
                        }
                    } else {
                        it.sendEmptyMessageDelayed(MSG_FLIP, mBannerParams.flipTime.toLong())
                        showNextItem()
                    }
                }
            }
        }
        return true
    }

总结

  1. Banner其实就是利用Pager的滑动机制实现的。主要是如何处理好多次创建布局所带来的资源损耗,所以我这里想到可以复用已经被container remove的itemView,这样子就无需多次创建布局。
  2. 图片轮播上可以采用Android timer或者是Handler,我这里实现是用了Handler与message的特性去实现的,如果有兴趣也可以将起该成timer来实现。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容