一個簡單易用的RatingBar

前幾天需要做一個評星的功能,於是先嚐試了一下系統自帶的RatingBar,但是使用的時候發現,在手機上很容易誤點!比如我想點4星,結果很容易點中5星。另外一個問題是星星之間的間距是不能設置的,於是到GitHub上找,發現有的效果做得很好看,但是還是無法解決容易誤點的問題,於是打算自定義一個。

先上一個效果圖

Alt

需求

先說一下我的需要:

  • 不易引起誤點操作
  • 支持修改顏色(選中和未選中的顏色)
  • 不需要像系統自帶的RatingBar那樣支持拖動
  • 支持設置星星總數和大小
  • 支持設置星星之間的間距

實現

有了需求之後就開始考慮實現的步驟:

  1. 創建自定義的屬性
  2. 實現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上了,可以點擊這裏跳轉

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章