前几天需要做一个评星的功能,于是先尝试了一下系统自带的RatingBar,但是使用的时候发现,在手机上很容易误点!比如我想点4星,结果很容易点中5星。另外一个问题是星星之间的间距是不能设置的,于是到GitHub上找,发现有的效果做得很好看,但是还是无法解决容易误点的问题,于是打算自定义一个。
先上一个效果图
需求
先说一下我的需要:
- 不易引起误点操作
- 支持修改颜色(选中和未选中的颜色)
- 不需要像系统自带的RatingBar那样支持拖动
- 支持设置星星总数和大小
- 支持设置星星之间的间距
实现
有了需求之后就开始考虑实现的步骤:
- 创建自定义的属性
- 实现View的子类,重写onMeasure、onDraw等方法
首先是属性定义:在res/value目录下创建一个attrs.xml文件,并添加以下的属性,
解释一下starType属性,考虑到有的场景可能会用到心形,所以定义这个属性来决定评星是星星的形状还是心形。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SimpleRatingView">
<attr name="starType" format="enum">
<enum name="star" value="1"/>
<enum name="heart" value="2"/>
</attr>
<attr name="starSize" format="dimension" />
<attr name="starSpacing" format="dimension" />
<attr name="numStars" format="integer" />
<attr name="rating" format="integer" />
<attr name="primaryColor" format="color" />
<attr name="secondaryColor" format="color" />
</declare-styleable>
</resources>
创建一个类继承View类,重写onMeasure、onDraw方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int width = (int) (paddingLeft + paddingRight + starSize * numStars + starSpacing * (numStars - 1));
int height = (int) (paddingTop + paddingBottom + starSize);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
// 以24为基准做缩放
float scale = starSize / 24;
// 校正参数
if (numStars <= 0) {
throw new IllegalArgumentException("numStars不能小于等于0");
}
if (rating < 0) {
rating = 0;
}
if (rating > numStars) {
rating = numStars;
}
// 先绘制primaryColor的星星
for (int i = 0; i < numStars; i++) {
if (i < rating) {
paint.setColor(primaryColor);
} else {
paint.setColor(secondaryColor);
}
canvas.save();
canvas.translate(paddingLeft + (starSize + starSpacing) * i, paddingTop);
path.reset();
if (starType == STAR) {
// 星星
path.moveTo(scale * 12f, scale * 17.27f);
path.lineTo(scale * 18.18f, scale * 21f);
path.rLineTo(scale * -1.64f, scale * -7.03f);
path.lineTo(scale * 22f, scale * 9.24f);
path.rLineTo(scale * -7.19f, scale * -0.61f);
path.lineTo(scale * 12f, scale * 2f);
path.lineTo(scale * 9.19f, scale * 8.63f);
path.lineTo(scale * 2f, scale * 9.24f);
path.rLineTo(scale * 5.46f, scale * 4.73f);
path.lineTo(scale * 5.82f, scale * 21f);
path.close();
} else if (starType == HEART) {
// 心形
path.moveTo(scale * 12f,scale * 21.35f);
path.rLineTo(scale * -1.45f,scale * -1.32f);
path.cubicTo(scale * 5.4f,scale * 15.36f,scale * 2f,scale * 12.28f,scale * 2f,scale * 8.5f);
path.cubicTo(scale * 2f,scale * 5.42f, scale * 4.42f,scale * 3f, scale * 7.5f,scale * 3f);
path.rCubicTo(scale * 1.74f,scale * 0f, scale * 3.41f,scale * 0.81f, scale * 4.5f,scale * 2.09f);
path.cubicTo(scale * 13.09f,scale * 3.81f, scale * 14.76f,scale * 3f, scale * 16.5f,scale * 3f);
path.cubicTo(scale * 19.58f,scale * 3f, scale * 22f,scale * 5.42f, scale * 22f,scale * 8.5f);
path.rCubicTo(scale * 0f,scale * 3.78f, scale * -3.4f,scale * 6.86f, scale *-8.55f,scale * 11.54f);
path.lineTo(scale * 12f,scale * 21.35f);
path.close();
} else {
}
canvas.drawPath(path, paint);
canvas.restore();
}
}
另外还需要重写一个重要的方法onTouchEvent,我们需要在这个方法中判断点击的位置,这样才能知道点中的是第几个星星,我们也可以在这个方法去设置点击的有效范围
@Override
public boolean onTouchEvent(MotionEvent event) {
float dx = event.getX();
float dy = event.getY();
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int oldRating = rating;
int newRating = rating;
float dxx;
boolean isClick = false;
// 计算点击事件产生在哪个星星上
if (dy > paddingTop && dy <= paddingTop + starSize) {
for (int i = 1; i <= numStars; i++) {
dxx = dx - paddingLeft - (starSize + starSpacing) * (i - 1);
if (dxx > 0 && dxx < starSize) {
newRating = i;
isClick = true;
break;
}
}
}
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (isClick) {
setRating(newRating);
if (onRatingChangeListener != null) {
onRatingChangeListener.onRatingChange(oldRating, newRating);
}
}
break;
case MotionEvent.ACTION_UP:
performClick();
break;
}
return true;
}
最后再加上一个回调事件接口:
public interface OnRatingChangeListener {
void onRatingChange(int oldRating, int newRating);
}
GitHub地址
该项目已经开源到GitHub上了,可以点击这里跳转