前幾天需要做一個評星的功能,於是先嚐試了一下系統自帶的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上了,可以點擊這裏跳轉