自定义Banner用法明细
支持XML自定义属性:
bv_viewHeight:Banner视图区域的高度,小于等于0时为该布局的高度
bv_viewCornerRadius:视图区域圆角的半径
bv_itemViewWidthRatio:根据该布局宽度的百分比设置ItemView的宽度
bv_itemViewMargin:设置ItemView之间的间距
bv_intervalInMillis:Banner轮换时间(在SMOOTH模式下为Banner从右匀速到左的时间)
bv_pageHoldInMillis:手指滑动后,页面停留的时长(只在SMOOTH模式下生效)
bv_scrollMode:设置Banner滚动模式
- INTERVAL:间隔切换模式
- SMOOTH:匀速滚动模式
bv_itemViewAlign:ItemView与父WrapperView的对齐方式(决定了itemViewMargin的留白位置)
- CENTER_HORIZONTAL:水平居中
- ALIGN_PARENT_LEFT:居左对齐
- ALIGN_PARENT_RIGHT:居右对齐
暴露的API有:
- setBannerViewImpl(impl: IBannerView):设置Banner必须的实现类
- startAutoScroll():开始自动滚动(页面数量小于1时不会滚动)
- stopAutoScroll():停止自动滚动
1 | /** |
源码地址:
https://github.com/drawf/SourceSet/tree/master/app/src/main/java/me/erwa/sourceset/view/banner
思考分析
NOTE:这篇文章我们专注于BannerView的封装与实现,关于更底层的PagerSnapHelper的原理部分不在范围内,但在文末我拜读的文章中贴出了一份链接,大家可自行食用。
前路漫漫,我们先梳理下需求:
- 要支持两种滚动模式,间隔切换、平滑滚动
- 要支持设置视图区域圆角
- 要支持设置条目视图圆角(ItemView)(该需求本次未做实现,下文会自动忽略该需求)
- 要支持无限循环滚动
- 要支持根据BannerView的宽的比值设置ItemView的宽
- 要支持设置ItemView之间的间距
- 要支持设置滚动间隔,匀速模式要支持设置滚动一页的时间
- 要支持设置匀速模式下,手指滑动后,页面停留的时长
- 要支持设置ItemView与父WrapperView的对齐方式(决定了itemViewMargin的留白位置)
- 要支持设置默认是否开启滚动
- 要支持设置数据源为空时的默认View
- 要支持数据源只有1张banner时,禁止滚动
- 要暴露API控制Banner的自动滚动与暂停
- 要支持设置指示器(Indicator),且能灵活控制指示器位置,且与BannerView解耦
🤩这么多需求,不要怕,我们根据需求来理一遍核心技术点:
平滑滚动模式
可以使用RecyclerView+PagerSnapHelper实现,间隔滚动模式
可以继续使用ViewPager实现,也可以使用前者方式实现。(本文统一使用RecyclerView+PagerSnapHelper方式,不过代码中也留出了接口,可用ViewPager做实现)- 设置圆角还是采用
Xfermode
做裁剪合成即可。(该方式在之前的文章ShadowLayout中使用过,故本文不再赘述) - 需求[4]将adpter中getItemCount()返回Int.MAX_VALUE,再在绑定View时候,用当前的position与真实count求余数,作为真实的position去绑定数据,即可实现。
- 需求[4]到[13],都没有技术复杂度,但有业务复杂度,做常规实现即可。
- 需求[14]可定义Indicator涉及的接口做代码解耦,并将BannerView继承RelativeLayout,这样Indicator作为子View在xml中可灵活控制位置。
这样一来,实现我们想要的BannerView只是耐心+时间的问题了。以下,我会挑本次实现中重要的几点来做说明,如下:
- RecyclerView+PagerSnapHelper实现的PagerRecyclerView
- 生成PagerView实例的工厂PagerViewFactory
- Indicator的解耦实现
PagerRecyclerView
看名字便知这是一个用RecyclerView实现ViewPager功能的类,所以继承自RecyclerView。
它作为BannerView的核心功能实现类,为了与上层解耦(也就是方便切换为其它实现,比如用ViewPager做实现)所以定义接口IPagerViewInstance
。
1 | /** |
关于PagerSnapHelper
的使用极其简单,只需创建出实例,attachToRecyclerView一下,即可让RecyclerView摇身一变成为ViewPager一样。(这里实在让人惊叹!!我们都应该追求这种API的极致设计)
1 | /** |
关于间隔切换模式 匀速滚动模式
的实现主要是在startTimer()
方法中,两者的区别在于Timer的间隔时间不同、回调中执行的方法不同。其中匀速模式的Timer间隔时间需要使用外部设置的滚动一屏的时间、一屏的宽度、每次scrollBy的距离
计算而来。
1 | /** |
页面选中是根据PagerSnapHelper中提供的findSnapView方法,先找到Snap(就是当前的目标View),再找它的位置,当然还需用一个变量记录一下,防止多次触发回调。
1 | /** |
还有一个值得说道的点是初始化时需要矫正Snap的位置,因为PagerSnapHelper手指滑动的时候才工作让RecyclerView滑动出ViewPager的感觉,所以初始化时不矫正会发现选中的页面不居中显示,还是一个RecyclerView的样子。那如何矫正呢?这里去看了PagerSnapHelper实现,搬过来,稍加修改即可。
1 | /** |
以上是我认为PagerRecyclerView较为关键的点,其它部分均为业务逻辑的处理与实现,大家可打开源码自行食用。
PagerViewFactory
这里采用了工厂方法模式来创建Banner底层的核心实现。
首先定义了BannerView实例接口,它将作为工厂实例的构造方法参数,用于区分创建底层实现。
1 | interface IBannerViewBase { |
工厂有个getPagerView()的方法,来创建Banner核心实现
1 | /** |
这里就是创建了之前写好的PagerRecyclerView,其实就是创建配置使用一个RecyclerView的过程。
1 | /** |
Indicator的解耦实现
解耦的惯用套路就是抽象方法定义接口。所以我们定义了两个接口,一个是指示器实例需实现的接口,一个是指示器依赖的外部实现。所以使用这两个接口,可以自定义实现想要的样式。
1 | /** |
对于我们这次实现的CrossBarIndicator,它就是一个常规的自定义View,这里已没有什么好说的啦。重点要说的是需求中有一条且能灵活控制指示器位置,如何实现呢?需求分析时说了,我们的BannerView是一个RelativeLayout,Indicator作为其子View可以很方便的控制其位置。
然后,看下BannerView中的关键代码:
1 | override fun onFinishInflate() { |
文末
到这里整体要说的就完结了,整个BannerView的实现细节、逻辑还是很多的,不过复杂度倒没那么高,建议食用源码~ O(∩_∩)O哈哈~
个人能力有限,如有不正之处欢迎大家批评指出,我会虚心接受并第一时间修改,以不误导大家。
拜读的文章
SnapHelper硬核讲解: