老规矩:先上最终效果图
做前端的同学在平常的工作中,很多时候都会接触到各种指示器,这次我就接到一个等级指示器的需求:
RecyclerView横向滚动,item之间有分割线,中间的item会被放大,边上的item会有一定的透明度进行淡化,滚动时要将等级变更回调给外界进行处理,停止滚动后被选中的item需要居中。
效果图如下:

实现流程
- 创建一个LevelRecyclerView继承RecyclerView,在内部init方法设置它的layoutManager,在外部提供数据源与adapter,然后最简单的RecyclerView就展示出来了
- 给每个item添加分割线
- 这时候RecyclerView可以随意滚动,给它添加一个PagerSnapHelper,让RecyclerView停下来时,item可以自动居中
- 这时又发现首尾的item,由于滚不到RecyclerView的中间,无法被选中,于是调整首尾item的分割线长度,使它们可以滚动到RecyclerView的中间
- 给RecyclerView注册滚动监听,在滚动过程中动态修改item的缩放与透明度,并将当前选中等级回调出去
- 重写smoothScrollToPosition,方法原实现是将某个item可见,但现在的需求是居中选中,所以要重写
初始化基本的RecyclerView
在LevelRecyclerView初始化时设置一个横向的LinearLayoutManager
init {
layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
}
设置基本的数据源与adapter
private fun initRecycler() {
val list = mutableListOf<Int>()
list.add(R.drawable.icon_vip_level_0)
list.add(R.drawable.icon_vip_level_1)
list.add(R.drawable.icon_vip_level_2)
list.add(R.drawable.icon_vip_level_3)
list.add(R.drawable.icon_vip_level_4)
list.add(R.drawable.icon_vip_level_5)
list.add(R.drawable.icon_vip_level_6)
list.add(R.drawable.icon_vip_level_7)
list.add(R.drawable.icon_vip_level_8)
list.add(R.drawable.icon_vip_level_9)
list.add(R.drawable.icon_vip_level_10)
rv_level.adapter = object : CommonAdapter<Int>(this, R.layout.level_item, list) {
override fun convert(holder: ViewHolder, t: Int, position: Int) {
holder.setImageResource(R.id.iv_image, t)
holder.setOnClickListener(R.id.iv_image) {
rv_level.smoothScrollToPosition(position)
}
}
}
}
效果图:

给每个item添加分割线
创建一个LevelDividerItemDecoration类继承ItemDecoration,构造参数需要传入分割线的水平长度与高度,分割线的颜色为可选参数,重写getItemOffsets与onDraw方法,熟悉ItemDecoration的同学可能会觉得onDraw方法有点眼熟,因为我这个onDraw是在DividerItemDecoration上修改的
class LevelDividerItemDecoration @JvmOverloads constructor(
private val itemDividerHorizontalMargin : Int,
private val dividerHeight : Int,
dividerColor : Int = Color.parseColor("#3A3A3C")
) : ItemDecoration() {
//分割线Drawable
private val mDivider = ColorDrawable(dividerColor)
//分割线绘制区域
private val mBounds = Rect()
/**
* 计算item的分割线需要的尺寸,就是一个偏移量,可简单看成外边距
*/
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
//上下不需要分割线设置为0,左右则是将构造时传入的itemDividerHorizontalMargin设置进去
outRect.set(itemDividerHorizontalMargin, 0, itemDividerHorizontalMargin, 0)
}
/**
* 绘制分割线
*/
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(canvas, parent, state)
canvas.save()
val top = (parent.height - dividerHeight) / 2
val bottom = top + dividerHeight
if (parent.clipToPadding) {
canvas.clipRect(parent.paddingLeft, top, parent.width - parent.paddingRight, bottom)
}
val childCount = parent.childCount
for (i in 0 until childCount) {
val item = parent.getChildAt(i)
//获取item的Rect,包含它的外边距(包含上面设置进去的偏移量)
parent.layoutManager!!.getDecoratedBoundsWithMargins(item, mBounds)
//左边分割线
mDivider.setBounds(mBounds.left, top, mBounds.left + itemDividerHorizontalMargin, bottom)
mDivider.draw(canvas)
//右边分割线
mDivider.setBounds(mBounds.right - itemDividerHorizontalMargin, top, mBounds.right, bottom)
mDivider.draw(canvas)
}
canvas.restore()
}
}
在init中添加到LevelRecyclerView
addItemDecoration(LevelDividerItemDecoration(
UIUtil.dip2px(context, 16.0),
UIUtil.dip2px(context, 4.0)))
效果图:

这时候RecyclerView可以随意滚动,给它添加一个PagerSnapHelper,让RecyclerView停下来时,item可以自动居中
添加一个PagerSnapHelper
在内部添加一个PagerSnapHelper,并在init时设置依附于LevelRecyclerView
private val mSnapHelper = PagerSnapHelper()
init {
mSnapHelper.attachToRecyclerView(this)
layoutManager = mLayoutManager
}
效果图:

这时又发现首尾的item,由于滚不到RecyclerView的中间,无法被选中,于是优化LevelDividerItemDecoration的计算与绘制,调整首尾item的分割线长度,使它们可以滚动到RecyclerView的中间
优化LevelDividerItemDecoration的计算与绘制
class LevelDividerItemDecoration @JvmOverloads constructor(
private val itemDividerHorizontalMargin : Int,
private val dividerHeight : Int,
dividerColor : Int = Color.parseColor("#3A3A3C")
) : ItemDecoration() {
//分割线Drawable
private val mDivider = ColorDrawable(dividerColor)
//分割线绘制区域
private val mBounds = Rect()
/**
* 计算item的分割线需要的尺寸,就是一个偏移量,可简单看成外边距
*/
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val parentWidth = parent.measuredWidth
val itemWidth = view.layoutParams.width
val lastPosition = parent.adapter?.itemCount?.minus(1) ?: 0
//针对首尾两个item计算它们的左右边距,用parentWidth - itemWidth再除2,可以使item刚好到达RecyclerView的中间
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.set(((parentWidth - itemWidth) * 0.5).toInt(), 0, itemDividerHorizontalMargin, 0)
}
lastPosition -> {
outRect.set(itemDividerHorizontalMargin, 0, ((parentWidth - itemWidth) * 0.5).toInt(), 0)
}
else -> outRect.set(itemDividerHorizontalMargin, 0, itemDividerHorizontalMargin, 0)
}
}
/**
* 绘制分割线
*/
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(canvas, parent, state)
canvas.save()
val top = (parent.height - dividerHeight) / 2
val bottom = top + dividerHeight
if (parent.clipToPadding) {
canvas.clipRect(parent.paddingLeft, top, parent.width - parent.paddingRight, bottom)
}
//RecyclerView宽度
val parentWidth = parent.measuredWidth
val childCount = parent.childCount
for (i in 0 until childCount) {
val item = parent.getChildAt(i)
//item宽度
val itemWidth = item.measuredWidth
//获取item的Rect,包含它的外边距(包含上面设置进去的偏移量)
parent.layoutManager!!.getDecoratedBoundsWithMargins(item, mBounds)
//左边分割线
if (i == 0 && mBounds.width() > itemWidth + itemDividerHorizontalMargin * 2) {
mDivider.setBounds(0, top, mBounds.right - itemWidth - itemDividerHorizontalMargin, bottom)
} else {
mDivider.setBounds(mBounds.left, top, mBounds.left + itemDividerHorizontalMargin, bottom)
}
mDivider.draw(canvas)
//右边分割线
if (i == childCount - 1 && mBounds.width() > itemWidth + itemDividerHorizontalMargin * 2) {
mDivider.setBounds(mBounds.left + itemWidth + itemDividerHorizontalMargin, top, parentWidth, bottom)
} else {
mDivider.setBounds(mBounds.right - itemDividerHorizontalMargin, top, mBounds.right, bottom)
}
mDivider.draw(canvas)
}
canvas.restore()
}
}
效果图:

给RecyclerView注册滚动监听,在滚动过程中动态修改item的缩放与透明度,并将当前选中等级回调出去
定义一个等级回调接口
interface OnLevelChangeListener {
fun onLevelChange(position : Int)
}
添加OnScrollListener,在滚动过程中做了一些计算,每个方法都写了注释,具体看下面代码↓
addOnScrollListener(object : OnScrollListener() {
//系数最大值
private val maxFactor = .45F
/**
* RecyclerView滚动
*/
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val first = mLayoutManager.findFirstVisibleItemPosition()
val last = mLayoutManager.findLastVisibleItemPosition()
val parentCenter = recyclerView.width / 2F
for (i in first..last) {
setItemTransform(i, parentCenter)
}
changeSnapView()
}
/**
* RecyclerView滚动状态改变
*/
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == SCROLL_STATE_IDLE) {
changeSnapView()
}
}
/**
* 对item进行各种变换
* 目前是缩放与透明度变换
*/
private fun setItemTransform(position : Int, parentCenter : Float) {
mLayoutManager.findViewByPosition(position)?.run {
val factor = calculationViewFactor(left.toFloat(), width.toFloat(), parentCenter)
val scale = 1 + factor
scaleX = scale
scaleY = scale
alpha = 1 - maxFactor + factor
}
}
/**
* 计算当前item的缩放与透明度系数
* item的中心离recyclerView的中心越远,系数越小(负相关)
*/
private fun calculationViewFactor(left: Float, width : Float, parentCenter : Float) : Float {
val viewCenter = left + width / 2
val distance = abs(viewCenter - parentCenter) / width
return max(0F, (1F - distance) * maxFactor)
}
/**
* 修改当前居中的item,把当前等级回调给外界
*/
private fun changeSnapView() {
mSnapHelper.findSnapView(mLayoutManager)?.let {
mLayoutManager.getPosition(it).let { position ->
if (lastPosition != position) {
lastPosition = position
levelListener?.onLevelChange(position)
}
}
}
}
})
给LevelRecyclerView设置等级回调监听
rv_level.levelListener = object : LevelRecyclerView.OnLevelChangeListener {
override fun onLevelChange(position: Int) {
Log.e("levelListener","levelListener $position")
tv_level.text = "等级:$position"
}
}
效果图:

重写smoothScrollToPosition,方法原实现是将某个item可见,但现在的需求是居中选中,所以要重写
方法的原实现其实就是LinearLayoutManager内部创建了一个LinearSmoothScroller去进行滚动,现在我们创建一个CenterSmoothScroller类去继承LinearSmoothScroller,重写它的calculateDtToFit方法,calculateDtToFit用于计算滚动距离,而calculateSpeedPerPixel计算滚动速度
class CenterSmoothScroller(context: Context?) : LinearSmoothScroller(context) {
override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int,
boxEnd: Int, snapPreference: Int): Int {
return boxStart + (boxEnd - boxStart) / 2 - (viewStart + (viewEnd - viewStart) / 2)
}
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
return super.calculateSpeedPerPixel(displayMetrics) * 3F
}
}
override fun smoothScrollToPosition(position : Int) {
if (position == lastPosition) return
if (position < 0 || position >= (adapter?.itemCount ?: 0)) return
mLayoutManager.startSmoothScroll(
CenterSmoothScroller(context).apply {
targetPosition = position
}
)
}
到这里就完成了对整个LevelRecyclerView的开发了,实现了文章开头的动画效果

总结
10分钟过去了,这个简单的LevelRecyclerView你拿下没有?
觉得不错的话,就不要吝啬你的点赞!
需要整份代码的话,下面链接自提。
代码链接 : github.MyCustomView








































































































































































若不是最近看到这样一篇新闻。
当时机哥身边的亲朋好友,只要是有智能手机的。
原因倒是不复杂,在那个流量资费偏高的年代。
WiFi万能钥匙,总是能如它的名字般神奇,帮用户成功连上WiFi。
关键是,这软件还免费使用。
WiFi万能钥匙发布不到三年,就拥有超过5亿的激活用户。
什么叫风头无两啊,


再加上App内部,出现了各种离谱的广告。
不知道的,还以为下了个病毒软件呢...
但它这些年,积攒起来的崩坏口碑。
当然啦,如果只是广告讨人嫌。
但它能帮咱们连上各种场合的WiFi,原理并不是暴力破解。
听起来,似乎是个不错的模式对吧。
你帮我,我帮你,天下就没有难办的事儿了。
但理想很丰满,现实很骨感。

我凭什么无缘无故,给一个陌生人一块钱?
他表示,App可以直接从用户手机拿到WiFi密码。
可谓是明文存放,点开就送。
可很凑巧的是,早期的安卓手机获取权限非常简单。
紧接着,最关键的问题来了。
作为一个,只会输入“Hello World”的代码废柴。

嗯?难道说...
如果实在遇到一些,数据库里配对不上的WiFi。
机友们都懂的,其实很多家庭路由器,密码都设置得很简单。


“我们可是有9亿用户总量的,欢迎来合作啊喂。
不能说克制,只能说处处皆是广告位。
而WiFi万能钥匙,对于广告的内容筛选,更是让人汗流浃背。

讲道理,以它如此庞大的用户总量。
更何况,现在早就不是,流氓App能随意践踏手机的时代了。


结果发现,它现在往App塞了个短剧板块。
emmm...机哥也没啥好说的,祝它一切顺利吧。
























