首頁 > 軟體

Android自定義View實現星星評分效果

2022-11-27 14:01:37

前言

在前面的學習中,我們基本瞭解了一些 Canvas 的繪製,那麼這一章我們一起復習一下圖片的繪製幾種方式,和事件的簡單互動方式。

我們從易到難,作為基礎的進階控制元件,我們從最簡單的互動開始,那就自定義一個星星評分的控制元件吧。

一個 App 必不可少的評論系統打分的控制元件,可以展示評分,可以點選評分,可以滑動評分。它的實現總體上可以分為以下的步驟:

  • 強制測量大小為我們指定的大小
  • 先繪製Drawable未評分的圖片
  • 在繪製Bitmap已評分的圖片
  • 在onTouch中點選和移動的事件中動態計算當前的評分,進而重新整理佈局
  • 回撥的處理與屬性的抽取

思路我們已經有了,下面一步一步的來實現吧。

話不多說,Let's go

1、測量與圖片的繪製

我們需要繪製幾個星星,那麼我們必須要設定的幾個屬性:

當前的評分值,總共有幾個星星,每一個星星的間距和大小,選中和未選中的Drawable圖片:

    private int mStarDistance = 0;
    private int mStarCount = 5;
    private int mStarSize = 20;    //每一個星星的寬度和高度是一致的
    private float mScoreNum = 0.0F;  //當前的評分值
    private Drawable mStarScoredDrawable;  //已經評分的星星圖片
    private Drawable mStarUnscoredDrawable;  //還未評分的星星圖片

    private void init(Context context, AttributeSet attrs) {

        mScoreNum = 2.1f;
        mStarSize = context.getResources().getDimensionPixelSize(R.dimen.d_20dp);
        mStarDistance = context.getResources().getDimensionPixelSize(R.dimen.d_5dp);
        mStarScoredDrawable = context.getResources().getDrawable(R.drawable.iv_normal_star_yellow);
        mStarUnscoredDrawable = context.getResources().getDrawable(R.drawable.iv_normal_star_gray);
    }

測量佈局的時候,我們就不能根據xml設定的 match_parent 或 wrap_content 來設定寬高,我們需要根據星星的大小與間距來動態的計算,所以不管xml中如何設定,我們都強制性的使用我們自己的測量。

星星的數量 * 星星的寬度再加上中間的間距 * 數量-1,就是我們的控制元件寬度,控制元件高度則是星星的高度。

具體的確定測量我們再上一篇已經詳細的複習過了,這裡直接貼程式碼:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(mStarSize * mStarCount + mStarDistance * (mStarCount - 1), mStarSize);
    }

這樣就可以得到對應的測量寬高 (加一個背景方便看效果):

如何繪製星星?直接繪製Drawable即可,預設的Drawable的繪製為:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        for (int i = 0; i < mStarCount; i++) {
            mStarUnscoredDrawable.setBounds((mStarDistance + mStarSize) * i, 0, (mStarDistance + mStarSize) * i + mStarSize, mStarSize);
            mStarUnscoredDrawable.draw(canvas);
        }

    }

如果有5個星星圖片,那麼就為每一個星星定好位置:

那麼已經選中的圖片也需要使用這種方法繪製嗎?

計算當前的評分,然後計算計算需要繪製多少星星,那麼就是這樣做:

    int score = (int) Math.ceil(mScoreNum);
    for (int i = 0; i < score; i++) {
        mStarScoredDrawable.setBounds((mStarDistance + mStarSize) * i, 0, (mStarDistance + mStarSize) * i + mStarSize, mStarSize);
        mStarScoredDrawable.draw(canvas);
    }

可是這麼做不符合我們的要求啊 ,我們是需要是可以顯示評分為2.5之類值,那麼我們怎麼能繪製半顆星呢?Drawable.draw(canvas) 的方式滿足不了,那我們可以使用 BitmapShader 的方式來繪製。

初始化一個 BitmapShader 設定給 Paint 畫筆,通過畫筆就可以畫出對應的形狀。

比如此時的場景,我們如果想只畫0.5個星星,那麼我們就可以

     paint = new Paint();
    paint.setAntiAlias(true);
    paint.setShader(new BitmapShader(drawableToBitmap(mStarScoredDrawable), BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));

    @Override
    protected void onDraw(Canvas canvas) {
        for (int i = 0; i < mStarCount; i++) {
            mStarUnscoredDrawable.setBounds((mStarDistance + mStarSize) * i, 0, (mStarDistance + mStarSize) * i + mStarSize, mStarSize);
            mStarUnscoredDrawable.draw(canvas);
        }

         canvas.drawRect(0, 0, mStarSize * mScoreNum, mStarSize, paint);
    }

那麼如果是大於一個星星之後的小數點就可以用公式計算

    if (mScoreNum > 1) {
        canvas.drawRect(0, 0, mStarSize, mStarSize, paint);

        if (mScoreNum - (int) (mScoreNum) == 0) {
            //如果評分是3.0之類的整數,那麼直接按正常的rect繪製
            for (int i = 1; i < mScoreNum; i++) {
                canvas.translate(mStarDistance + mStarSize, 0);
                canvas.drawRect(0, 0, mStarSize, mStarSize, paint);
            }
        } else {
            //如果是小數例如3.5,先繪製之前的3個,再繪製後面的0.5
            for (int i = 1; i < mScoreNum - 1; i++) {
                canvas.translate(mStarDistance + mStarSize, 0);
                canvas.drawRect(0, 0, mStarSize, mStarSize, paint);
            }
            canvas.translate(mStarDistance + mStarSize, 0);
            canvas.drawRect(0, 0, mStarSize * (Math.round((mScoreNum - (int) (mScoreNum)) * 10) * 1.0f / 10), mStarSize, paint);
        }

    } else {
        canvas.drawRect(0, 0, mStarSize * mScoreNum, mStarSize, paint);
    }

效果:

關於 BitmapShader 的其他用法,可以翻看我之前的自定義圓角圓形View,和自定義圓角容器的文章,裡面都有用到過,主要是方便一些圖片的裁剪和縮放等。

2、事件的互動與計算

這裡並沒有涉及到什麼事件巢狀,攔截之類的複雜處理,只需要處理自身的 onTouch 即可。而我們需要處理的就是按下的時候和移動的時候評分值的變化。

在onDraw方法中,我們使用 mScoreNum 變數來繪製的已評分的 Bitmap 繪製。所以這裡我們只需要在 onTouch 中計算出對應的 mScoreNum 值,讓其重繪即可。

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        //x軸的寬度做一下最大最小的限制
        int x = (int) event.getX();
        if (x < 0) {
            x = 0;
        }
        if (x > mMeasuredWidth) {
            x = mMeasuredWidth;
        }

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE: {
                mScoreNum = x * 1.0f / (mMeasuredWidth * 1.0f / mStarCount);
                invalidate();
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
        }

        return super.onTouchEvent(event);

    }

計算出一顆星的長度,然後計算當前x軸的長度,就可以計算出當前有幾顆星,我們預設處理的是 float 型別。就可以根據計算出的 mScoreNum 值來得到對應的動畫效果:

3. 回撥處理與自定義屬性抽取

到此效果的實現算是結束了,但是我們還有一些收尾工作沒做,如何監聽進度的回撥,如何控制整數與浮點數的顯示,是否支援觸控等等。然後對其做一些自定義屬性的抽取,就可以在應用中比較廣泛的使用了。

自定義屬性:

    private int mStarDistance = 5;
    private int mStarCount = 5;
    private int mStarSize = 20;    //每一個星星的寬度和高度是一致的
    private float mScoreNum = 0.0F;  //當前的評分值
    private Drawable mStarScoredDrawable;  //已經評分的星星圖片
    private Drawable mStarUnscoredDrawable;  //還未評分的星星圖片
    private boolean isOnlyIntegerScore = false;  //預設顯示小數型別
    private boolean isCanTouch = true; //預設支援控制元件的點選
    private OnStarChangeListener onStarChangeListener;

自定義屬性的賦值與初始化操作:

    private void init(Context context, AttributeSet attrs) {
        setClickable(true);
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.StarScoreView);
        this.mStarDistance = mTypedArray.getDimensionPixelSize(R.styleable.StarScoreView_starDistance, 0);
        this.mStarSize = mTypedArray.getDimensionPixelSize(R.styleable.StarScoreView_starSize, 20);
        this.mStarCount = mTypedArray.getInteger(R.styleable.StarScoreView_starCount, 5);
        this.mStarUnscoredDrawable = mTypedArray.getDrawable(R.styleable.StarScoreView_starUnscoredDrawable);
        this.mStarScoredDrawable = mTypedArray.getDrawable(R.styleable.StarScoreView_starScoredDrawable);
        this.isOnlyIntegerScore = mTypedArray.getBoolean(R.styleable.StarScoreView_starIsTouchEnable, true);
        this.isOnlyIntegerScore = mTypedArray.getBoolean(R.styleable.StarScoreView_starIsOnlyIntegerScore, false);
        mTypedArray.recycle();

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setShader(new BitmapShader(drawableToBitmap(mStarScoredDrawable), BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
    }

自定義屬性的定義xml檔案:

    <!--  評分星星控制元件  -->
    <declare-styleable name="StarScoreView">
        <!--星星間距-->
        <attr name="starDistance" format="dimension" />
        <!--星星大小-->
        <attr name="starSize" format="dimension" />
        <!--星星個數-->
        <attr name="starCount" format="integer" />
        <!--星星已評分圖片-->
        <attr name="starScoredDrawable" format="reference" />
        <!--星星未評分圖片-->
        <attr name="starUnscoredDrawable" format="reference" />
        <!--是否可以點選-->
        <attr name="starIsTouchEnable" format="boolean" />
        <!--是否顯示整數-->
        <attr name="starIsOnlyIntegerScore" format="boolean" />
    </declare-styleable>

在OnTouch的時候就可以判斷是否能觸控

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (isCanTouch) {

            //x軸的寬度做一下最大最小的限制
            int x = (int) event.getX();
            if (x < 0) {
                x = 0;
            }
            if (x > mMeasuredWidth) {
                x = mMeasuredWidth;
            }

            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_MOVE: {
                    setStarMark(x * 1.0f / (getMeasuredWidth() * 1.0f / mStarCount));
                    break;
                }
                case MotionEvent.ACTION_UP: {
                    break;
                }
            }
            
            return super.onTouchEvent(event);

        } else {
            //如果設定不能點選,直接不觸發事件
            return false;
        }

    }

而 setStarMark 則是設定入口的方法,內部判斷是否支援小數點和設定對於的監聽,並呼叫重繪。

   public void setStarMark(float mark) {
        if (isOnlyIntegerScore) {
            mScoreNum = (int) Math.ceil(mark);
        } else {
            mScoreNum = Math.round(mark * 10) * 1.0f / 10;
        }
        if (this.onStarChangeListener != null) {
            this.onStarChangeListener.onStarChange(mScoreNum);  //呼叫監聽介面
        }
        invalidate();
    }

一個簡單的圖片繪製和事件觸控的控制元件就完成啦,使用起來也是超級方便。

    <com.guadou.kt_demo.demo.demo18_customview.star.StarScoreView
        android:id="@+id/star_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="@dimen/d_40dp"
        android:background="#f1f1f1"
        app:starCount="5"
        app:starDistance="@dimen/d_5dp"
        app:starIsOnlyIntegerScore="false"
        app:starIsTouchEnable="true"
        app:starScoredDrawable="@drawable/iv_normal_star_yellow"
        app:starSize="@dimen/d_35dp"
        app:starUnscoredDrawable="@drawable/iv_normal_star_gray" />

Activity中可以設定評分和設定監聽:

    override fun init() {

        val starView = findViewById<StarScoreView>(R.id.star_view)

        starView.setOnStarChangeListener {
            YYLogUtils.w("當前選中的Star:$it")
        }

        findViewById<View>(R.id.set_progress).click {
            starView.setStarMark(3.5f)
        }
    }

效果:

後記

整個流程走下來是不是很簡單呢,此控制元件不止用於星星型別的評分,任何圖片資源都可以使用,現在我們思路開啟擴充套件一下,相似的場景和效果我們可以實現一些圖片進度,觸控進度條,圓環的SeekBar,等等類似的控制都是相似的思路。

到此這篇關於Android自定義View實現星星評分效果的文章就介紹到這了,更多相關Android自定義View星星評分內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com