注册

自定义可点击可滑动的通用RatingBar

介绍

一个可以设置间距,设置选中未选中图标及数量,选中图标的类型(整,半,任意),可点击,可滑动选择的类似原生RatingBar的自定义View。

效果图预览

untitled.gif

实现

自定义属性

<declare-styleable name="CommonRatingBar">
<attr name="starCount" format="integer" />
<attr name="starPadding" format="dimension" />
<!-- 默认选中时的图标,可不设置,使用纯色starColor -->
<attr name="starDrawable" format="reference" />
<!-- 默认未选中时的图标 -->
<attr name="starBgDrawable" format="reference" />
<!-- 纯色样式 -->
<attr name="starColor" format="color" />
<attr name="starClickable" format="boolean" />
<attr name="starScrollable" format="boolean" />
<attr name="starType" format="enum">
<enum name="normal" value="0" />
<enum name="half" value="1" />
<enum name="whole" value="2" />
</attr>
</declare-styleable>

测量View

将控件的高度设置为测量高度,测量宽度为星星的数量+每个星星之间的padding

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
starSize = measuredHeight
setMeasuredDimension(starCount * starSize + (starCount - 1) * starPadding.toInt(), starSize)
}

绘制ratingbar

  1. 绘制未选中的背景
/**
* 未选中Bitmap
*/

private val starBgBitmap: Bitmap by lazy {
val bitmap = Bitmap.createBitmap(starSize, starSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val starDrawable = ContextCompat.getDrawable(context, starBgDrawable)
starDrawable?.setBounds(0, 0, starSize, starSize)
starDrawable?.draw(canvas)
bitmap
}

/**
* 绘制星星默认未选中背景
*/

private fun drawStar(canvas: Canvas) {
for (i in 0 until starCount) {
val starLeft = i * (starSize + starPadding)
canvas.drawBitmap(starBgBitmap, starLeft, 0f, starBgPaint)
}
}
  1. 绘制选中图标

这里bitmap宽度使用starSize + starPadding,配合BitmapShader的repeat模式,可以方便绘制出高亮的图标

/**
* 选中icon的Bitmap
*/

private val starBitmap: Bitmap by lazy {
val bitmap = Bitmap.createBitmap(starSize + starPadding.toInt(), starSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val starDrawable = ContextCompat.getDrawable(context, starDrawable)
starDrawable?.setBounds(0, 0, starSize, starSize)
starDrawable?.draw(canvas)
bitmap
}

/**
* 绘制高亮图标
*/

private fun drawStarDrawable(canvas: Canvas) {
starDrawablePaint.shader = BitmapShader(starBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
canvas.drawRect(0f, 0f, getStarProgressWidth(), height.toFloat(), starDrawablePaint)
}
  1. 绘制纯色的选中效果

使用离屏缓冲,纯色矩形与未选中背景相交的地方进行显示。具体使用可以参考扔物线大佬的文章

/**
* 星星纯色画笔
*/

private val starPaint = Paint().apply {
isAntiAlias = true
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
}

canvas?.let {
// xfermode需要使用离屏缓存
val saved = it.saveLayer(null, null)
drawStar(it)
if (starDrawable == -1) {
drawStarBgColor(it)
} else {
drawStarDrawable(it)
}
it.restoreToCount(saved)
}

/**
* 绘制高亮纯颜色
*/

private fun drawStarBgColor(canvas: Canvas) {
canvas.drawRect(0f, 0f, getStarProgressWidth(), height.toFloat(), starPaint)
}
  1. 绘制进度

根据type更正显示效果,是取半,取整还是任意取进度。open方法,可以方便修改

/**
* 获取星星绘制宽度
*/

private fun getStarProgressWidth(): Float {
val percent = progress / 100f
val starDrawCount = percent * starCount
return when (starType) {
StarType.HALF.ordinal -> {
ceilHalf(starDrawCount) * starSize + starDrawCount.toInt() * starPadding
}
StarType.WHOLE.ordinal -> {
ceilWhole(starDrawCount) * starSize + starDrawCount.toInt() * starPadding
}
else -> {
starDrawCount * starSize + starDrawCount.toInt() * starPadding
}
}
}

/**
* 取整规则
*/

private fun ceilWhole(x: Float): Float {
return ceil(x)
}

/**
* 取半规则
*/

private fun ceilHalf(x: Float): Float {
// 四舍五入 1.3->1+0.5->1.5 1.7->2
val round = round(x)
return when {
round < x -> round + 0.5f
round > x -> round
else -> x
}
}
  1. 点击+滑动

点击+滑动就是重写onTouchEvent事件:

  • 判断点击位置是否在范围内
/**
* 点击的point是否在view范围内
*/

private fun pointInView(x: Float, y: Float): Boolean {
return Rect(0, 0, width, height).contains(x.toInt(), y.toInt())
}
  • 记录按下位置,抬起位置。
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
}
MotionEvent.ACTION_UP -> {
if (starClickable && abs(event.y - downY) <= touchSlop && abs(event.x - downX) <= touchSlop && pointInView(event.x, event.y)) {
parent.requestDisallowInterceptTouchEvent(true)
val progress = (event.x / width * 100).toInt()
setProgress(progress)
listener?.onClickProgress(progress)
} else {
parent.requestDisallowInterceptTouchEvent(false)
}
}
  • 滑动记录手指move
MotionEvent.ACTION_MOVE -> {
if (starScrollable && abs(event.x - downX) - abs(event.y - downY) >= touchSlop && pointInView(event.x, event.y)) {
parent.requestDisallowInterceptTouchEvent(true)
val progress = (event.x / width * 100).toInt()
setProgress(progress)
listener?.onScrollProgress(progress)
} else {
parent.requestDisallowInterceptTouchEvent(false)
}
}
  1. 添加监听

添加OnCommonRatingBarListener,监听点击事件以及滑动事件,返回进度

click.gif

完整实现代码

class CommonRatingBar(context: Context, attrs: AttributeSet?) : View(context, attrs) {

/**
* 星星数量
*/

private var starCount = 5

/**
* 星星间隔
*/

private var starPadding = 0f

/**
* 星星大小
*/

private var starSize = 30

/**
* 星星选中背景图
*/

private var starDrawable: Int = -1

/**
* 星星未选中背景图
*/

private var starBgDrawable: Int = -1

/**
* 星星选择类型
*/

private var starType = StarType.NORMAL.ordinal

/**
* 星星颜色
*/

private var starColor: Int = Color.parseColor("#F7B500")

/**
* 星星可点击
*/

private var starClickable = false

/**
* 星星可滑动选择
*/

private var starScrollable = false

/**
* 星星未选中画笔
*/

private val starBgPaint = Paint().apply {
isAntiAlias = true
}

/**
* 星星选中画笔
*/

private val starDrawablePaint = Paint().apply {
isAntiAlias = true
}

/**
* 星星纯色画笔
*/

private val starPaint = Paint().apply {
isAntiAlias = true
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
}

private var progress = 0

/**
* 选中icon的Bitmap
*/

private val starBitmap: Bitmap by lazy {
val bitmap = Bitmap.createBitmap(starSize + starPadding.toInt(), starSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val starDrawable = ContextCompat.getDrawable(context, starDrawable)
starDrawable?.setBounds(0, 0, starSize, starSize)
starDrawable?.draw(canvas)
bitmap
}

/**
* 未选中Bitmap
*/

private val starBgBitmap: Bitmap by lazy {
val bitmap = Bitmap.createBitmap(starSize, starSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val starDrawable = ContextCompat.getDrawable(context, starBgDrawable)
starDrawable?.setBounds(0, 0, starSize, starSize)
starDrawable?.draw(canvas)
bitmap
}

init {
initView(context, attrs)
starPaint.color = starColor
}

private fun initView(context: Context, attrs: AttributeSet?) {
val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.CommonRatingBar)
starCount = obtainStyledAttributes.getInt(R.styleable.CommonRatingBar_starCount, 5)
starPadding = obtainStyledAttributes.getDimension(R.styleable.CommonRatingBar_starPadding, 10f)
starDrawable = obtainStyledAttributes.getResourceId(R.styleable.CommonRatingBar_starDrawable, -1)
starBgDrawable = obtainStyledAttributes.getResourceId(R.styleable.CommonRatingBar_starBgDrawable, -1)
starType = obtainStyledAttributes.getInt(R.styleable.CommonRatingBar_starType, StarType.NORMAL.ordinal)
starColor = obtainStyledAttributes.getColor(R.styleable.CommonRatingBar_starColor, Color.parseColor("#F7B500"))
starClickable = obtainStyledAttributes.getBoolean(R.styleable.CommonRatingBar_starClickable, false)
starScrollable = obtainStyledAttributes.getBoolean(R.styleable.CommonRatingBar_starScrollable, false)
obtainStyledAttributes.recycle()
}

override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
// super.dispatchTouchEvent(event) -> 当前view的onTouchEvent
// false -> viewGroup的onTouchEvent
return if (starClickable || starScrollable) super.dispatchTouchEvent(event)
else false
}

/**
* 最小触摸范围
*/

private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
private var downX = 0f
private var downY = 0f

@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
}
MotionEvent.ACTION_MOVE -> {
if (starScrollable && abs(event.x - downX) - abs(event.y - downY) >= touchSlop && pointInView(event.x, event.y)) {
parent.requestDisallowInterceptTouchEvent(true)
val progress = (event.x / width * 100).toInt()
setProgress(progress)
listener?.onScrollProgress(progress)
} else {
parent.requestDisallowInterceptTouchEvent(false)
}
}
MotionEvent.ACTION_UP -> {
if (starClickable && abs(event.y - downY) <= touchSlop && abs(event.x - downX) <= touchSlop && pointInView(event.x, event.y)) {
parent.requestDisallowInterceptTouchEvent(true)
val progress = (event.x / width * 100).toInt()
setProgress(progress)
listener?.onClickProgress(progress)
} else {
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
return true
}

/**
* 点击的point是否在view范围内
*/

private fun pointInView(x: Float, y: Float): Boolean {
return Rect(0, 0, width, height).contains(x.toInt(), y.toInt())
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
starSize = measuredHeight
setMeasuredDimension(starCount * starSize + (starCount - 1) * starPadding.toInt(), starSize)
}

override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (starBgDrawable == -1) {
return
}
canvas?.let {
// xfermode需要使用离屏缓存
val saved = it.saveLayer(null, null)
drawStar(it)
if (starDrawable == -1) {
drawStarBgColor(it)
} else {
drawStarDrawable(it)
}
it.restoreToCount(saved)
}
}

/**
* 绘制星星默认未选中背景
*/

private fun drawStar(canvas: Canvas) {
for (i in 0 until starCount) {
val starLeft = i * (starSize + starPadding)
canvas.drawBitmap(starBgBitmap, starLeft, 0f, starBgPaint)
}
}

/**
* 绘制高亮纯颜色
*/

private fun drawStarBgColor(canvas: Canvas) {
canvas.drawRect(0f, 0f, getStarProgressWidth(), height.toFloat(), starPaint)
}

/**
* 绘制高亮图标
*/

private fun drawStarDrawable(canvas: Canvas) {
starDrawablePaint.shader = BitmapShader(starBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
canvas.drawRect(0f, 0f, getStarProgressWidth(), height.toFloat(), starDrawablePaint)
}

/**
* 获取星星绘制宽度
*/

private fun getStarProgressWidth(): Float {
val percent = progress / 100f
val starDrawCount = percent * starCount
return when (starType) {
StarType.HALF.ordinal -> {
ceilHalf(starDrawCount) * starSize + starDrawCount.toInt() * starPadding
}
StarType.WHOLE.ordinal -> {
ceilWhole(starDrawCount) * starSize + starDrawCount.toInt() * starPadding
}
else -> {
starDrawCount * starSize + starDrawCount.toInt() * starPadding
}
}
}

private fun ceilWhole(x: Float): Float {
return ceil(x)
}

private fun ceilHalf(x: Float): Float {
// 四舍五入 1.3->1+0.5->1.5 1.7->2
val round = round(x)
return when {
round < x -> round + 0.5f
round > x -> round
else -> x
}
}

/**
* 星星的绘制进度
*/

fun setProgress(progress: Int) {
var p = progress
if (p < 0) p = 0
if (p > 100) p = 100
this.progress = p
postInvalidate()
}

fun setProgress(currentValue: Float, totalValue: Float) {
setProgress((currentValue * 100 / totalValue).toInt())
}

fun setOnCommonRatingBarListener(listener: OnCommonRatingBarListener) {
this.listener = listener
}

private var listener: OnCommonRatingBarListener? = null

interface OnCommonRatingBarListener {
fun onClickProgress(progress: Int)
fun onScrollProgress(progress: Int)
}

enum class StarType {
NORMAL, HALF, WHOLE
}

}

image.png

拓展

  • 修改纯色方法配合LinearGradient,可以有渐变的选中效果

0 个评论

要回复文章请先登录注册