实现一个使用简单,易于扩展的BannerView

1,013 阅读4分钟

背景

在项目的实践中,Banner的UI更改比较频繁。便想实现一个可以随意更换UI的BannerView

问题

  • 实现BannerView为了解决什么?
  • 如果将它作为一个公共依赖库应该具备什么?

文章的最后我们再来回答。

方案

在Android中我们最常见的UI应该是列表了,然后我们用RecyclerView去实现一个个样式迥异的列表。如果我们把Banner也理解成是一个列表的话,RecyclerView可自由实现item样式的特性也契合我们想随意更换Banner UI的需求。那么基于RecyclerView去实现BannerView应该是我们的首选方案了。

如果用RecyclerView去实现Banner。第一个问题是怎么解决每次拖动只切换一个item,类似于ViewPager的滑动切换效果。可喜的是在RecyclerView中提供了一个SnapHelper,我们使用它的一个扩展类PagerSnapHelper来辅助滑动便解决了我们的切换效果问题。

另一个问题是如何实现连续滑动切换,这里我们采用在RecyclerView的数据里首尾各添加一项数据。如图: 已上图为例子,当RecyclerView滑动到浅绿色3号页时就静态切换到深绿色3号页,当滑动到浅绿色1号页时就静态切换到深绿色1号页(两个3号页和两个1号页的UI和数据是一致的,所以用户感知不到程序触发了切换动作),这样就实现了连续滑动切换。其中的逻辑可以体会一下。

实践

整体的功能逻辑很简单,核心逻辑只用处理RecyclerView滑动到相应位置进行切换即可。我们用onScrollStateChanged来监听RecyclerView的滑动状态,当滑动停止时判断当前是第几页然后做相应切换。

//添加滑动监听
addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        super.onScrollStateChanged(recyclerView, newState)
        //判断滑动状态
        when (newState) {
            RecyclerView.SCROLL_STATE_DRAGGING -> {
                //当用户拖拽时...
                switch(recyclerView, pagerSnap)
            }
            RecyclerView.SCROLL_STATE_IDLE -> {
                //当滑动停止时...
                switch(recyclerView, pagerSnap)
            }
            else -> {
            }
        }
    }
})

核心的切换逻辑

private fun switch(recyclerView: RecyclerView, snap: PagerSnapHelper) {
    val layoutManager: LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
    val currentView: View = snap.findSnapView(layoutManager) ?: return
    //获取列表数量
    val bannerCount: Int = layoutManager.itemCount
    //获取当前滑动到哪一页
    _currentPosition = layoutManager.getPosition(currentView)
    when (_currentPosition) {
        0 -> {
            //如果当前页是列表数据中的第一项
            //按上图的示例应该切换到列表数据的倒数第二项
            _currentPosition = bannerCount - 2
            recyclerView.scrollToPosition(_currentPosition)
        }
        bannerCount - 1 -> {
            //如果当前页是列表数据中的最后一项
            //按上图的示例应该切换到列表数据的第二项
            _currentPosition = 1
            recyclerView.scrollToPosition(_currentPosition)
        }
        else -> {
        }
    }
}

这里还有个问题是如果只根据滑动结束后再切换,那么当我们快速滑动时,有可能页面切换的滑动还没结束,我们就要往下一页滑动了,这时因为滑动结束的监听没有被回调,导致我们的切换方法没有被执行。如果我们滑动到RecyclerView数据的最后一项或者第一项时,会因为后面没有数据导致我们无法继续滑动而出现停顿。所以我们在用户开始拖拽时也执行一下切换方法,这样就实现了近似无限滑动的效果。

自定义

如何自定义Banner的UI样式,因为我们是扩展自RecyclerView,所以理论上是继承了RecyclerView的所有特性。现在我们可以像使用RecyclerView一样来使用BannerView。自定义Banner的UI样式,只要自定义一个item布局即可。BannerView的数据类型同样可以根据业务数据来自定义。

//继承BannerAdapter         //根据业务数据自定义数据类型    //自定义item布局
class CustomAdapter : BannerAdapter<CustomModel>(R.layout.item_custom) {
    
    override fun onBindViewHolder(holder: BannerViewHolder, item: CustomModel, position: Int) {
        //通过holder.findViewById()获取自定义布局上的控件
    }

}

同时也提供了一个DefaultBannerAdapter方便使用,但是数据类型只支持String类型。如果没有自定义的需求直接使用即可。

val defaultAdapter: DefaultBannerAdapter = object : DefaultBannerAdapter() {
    override fun onBind(data: String, view: ImageView) {
        //可以处理一些工作
        //比如设置ImageView的属性、加载的图片等
    }
}

这样基本功能就实现了。一样的思路也实现了一个文本类型的Banner,TextBannerView。同样提供一个DefaultTextBannerAdapter方便使用。如果需要自定义,继承BannerAdapter实现自己的适配器即可。最终的效果大概是这样:

最后

现在我们来回答文章开始的问题:

  • 自定义样式现在可以很方便的去实现,解决了我们经常需要修改UI的问题。
  • BannerView没有再依赖任何第三方库,同时可以让我们简单快速的使用又兼具高可扩展性,作为一个公共依赖库应该是合格的。

更多使用方法和属性欢迎访问项目主页:github.com/mminng/Bann…

谢谢!