其他
自定义可点击、可滑动的通用RatingBar
作者:Loren
,
链接:https://juejin.cn/post/7007322024062222343
介绍
一个可以设置间距,设置选中未选中图标及数量,选中图标的类型(整,半,任意),可点击,可滑动选择的类似原生RatingBar
的自定义View
。
效果图预览
实现
自定义属性
<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
绘制未选中的背景
/**
* 未选中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)
}
}
绘制选中图标
这里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)
}
绘制纯色的选中效果
使用离屏缓冲,纯色矩形与未选中背景相交的地方进行显示。具体使用可以参考扔物线大佬的文章
/**
* 星星纯色画笔
*/
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)
}
绘制进度
根据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
}
}
点击+滑动
点击+滑动就是重写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)
}
}
添加监听
添加OnCommonRatingBarListener
,监听点击事件以及滑动事件,返回进度
完整实现代码
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
}
}
拓展
修改纯色方法配合LinearGradient,可以有渐变的选中效果
---END---
更文不易,点个“在看”支持一下👇