项目中需要添加类似京东 淘宝首页商品滑动 下面的指示器跟随随便位移对应进度。具体效果如下:
具体实现demo效果:
1、分析
其实这个很简单,主要就是有以下几点
- 1.绘制一个圆角矩形做背景;
- 2.绘制一个圆角矩形做背景;
- 3 .绘制一个圆角矩形做指示器;
- 4.确定指示器的长度和指示器的位置;
- 5.根据RecyclerView滑动的距离动态改变指示器的位置。
2、绘制指示器
绘制背景的圆角矩形的时候,不考虑padding信息,就很简单
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
mBgRect.set(0f, 0f, w * 1f, h * 1f)
mRadius = h / 2f
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//绘制背景
canvas?.drawRoundRect(mBgRect, mRadius, mRadius, mBgPaint)
}
绘制指示器
ratio指的是指示器长度,即如果滚动内容有两屏,则指示器应该为1/2长度,以此类推 (当然上面所示app不一定实现了这个,可能会为了美观设置一个固定比例)
progress指的是滑动距离和指示器对应关系,这个实际上就是滑动进度条的意思
//计算指示器的长度和位置
val leftOffset = viewWidth * (1f - ratio) * progress
val left = mBgRect.left + leftOffset
val right = left + viewWidth * ratio
mRect.set(left, mBgRect.top, right, mBgRect.bottom)
//绘制指示器
canvas?.drawRoundRect(mRect, mRadius, mRadius, mPaint)
3、和RecyclerView联动
获取RecyclerView滚动的位置可根据以下几个方法获取
- computeVerticalScrollExtent()/computeHorizontalScrollExtent//当前RcyclerView显示区域的高度。水平列表屏幕从左侧到右侧显示范围
- computeVerticalScrollOffset()/computeHorizontalScrollOffset //已经向下滚动的距离,为0时表示已处于顶部。
- computeVerticalScrollRange()/computeHorizontalScrollRange //整体的高度,注意是整体,包括在显示区域之外的
监听滑动,配合上诉方法就可以拿到滑动位置的比例
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val offsetX = recyclerView.computeHorizontalScrollOffset()
val range = recyclerView.computeHorizontalScrollRange()
val extend = recyclerView.computeHorizontalScrollExtent()
val progress: Float = offsetX * 1.0f / (range - extend) //因为指示器有长度,所以这里需要减去首屏长度
this@HIndicator.progress = progress //设置滚动距离所占比例
}
})
具体实现逻辑:
class TreeIndicator @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val mBgPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val mBgRect: RectF = RectF()
private var mRadius: Float = 0f
private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var mRect: RectF = RectF()
private var viewWidth: Int = 0
private var mBgColor = Color.parseColor("#e5e5e5")
private var mIndicatorColor = Color.parseColor("#ff4646")
var ratio = 0.5f //长度比例
set(value) {
field = value
invalidate()
}
var progress: Float = 0f //滑动进度比例
set(value) {
field = value
invalidate()
}
init {
val typedArray: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.HIndicator)
mBgColor = typedArray.getColor(R.styleable.HIndicator_hi_bgColor, mBgColor)
mIndicatorColor =
typedArray.getColor(R.styleable.HIndicator_hi_indicatorColor, mIndicatorColor)
typedArray.recycle()
mBgPaint.color = mBgColor
mBgPaint.style = Paint.Style.FILL
mPaint.color = mIndicatorColor
mPaint.style = Paint.Style.FILL
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
mBgRect.set(0f, 0f, w * 1f, h * 1f)
mRadius = h / 2f
}
/**
* 设置指示器背景进度条的颜色
* @param color 背景色
*/
fun setBgColor(@ColorInt color: Int) {
mBgPaint.color = color
invalidate()
}
/**
* 设置指示器的颜色
* @param color 指示器颜色
*/
fun setIndicatorColor(@ColorInt color: Int) {
mPaint.color = color
invalidate()
}
/**
* 绑定recyclerView
*/
fun bindRecyclerView(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val offsetX = recyclerView.computeHorizontalScrollOffset() //已经向下滚动的距离,为0时表示已处于顶部。
val range = recyclerView.computeHorizontalScrollRange() //整体的高度,注意是整体,包括在显示区域之外的
val extend = recyclerView.computeHorizontalScrollExtent() //当前RcyclerView显示区域的高度。水平列表屏幕从左侧到右侧显示范围
val progress: Float = offsetX * 1.0f / (range - extend)
this@HIndicator.progress = progress //设置滚动距离所占比例
Log.d("==distance==offsetX",offsetX.toString())
Log.d("==distance==range",range.toString())
Log.d("==distance==extend",extend.toString())
Log.d("==distance==progress",progress.toString())
}
})
recyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
val range = recyclerView.computeHorizontalScrollRange()
val extend = recyclerView.computeHorizontalScrollExtent()
val ratio = extend * 1f / range
this@HIndicator.ratio = ratio //设置指示器所占的长度比例
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//绘制背景
canvas?.drawRoundRect(mBgRect, mRadius, mRadius, mBgPaint)
//计算指示器的长度和位置
val leftOffset = viewWidth * (1f - ratio) * progress
val left = mBgRect.left + leftOffset
val right = left + viewWidth * ratio
mRect.set(left, mBgRect.top, right, mBgRect.bottom)
//绘制指示器
canvas?.drawRoundRect(mRect, mRadius, mRadius, mPaint)
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView.adapter = MAdapter()
hIndicator.bindRecyclerView(recyclerView)
btn1.setOnClickListener {
hIndicator.setBgColor(Color.parseColor("#333333"))
}
btn2.setOnClickListener {
hIndicator.setIndicatorColor(Color.parseColor("#ffffff"))
}
}
private inner class MAdapter : RecyclerView.Adapter<MViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MViewHolder =
MViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_test,
parent,
false
)
)
override fun getItemCount(): Int = 15
override fun onBindViewHolder(holder: MViewHolder, position: Int) {
}
}
private inner class MViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"/>