Android 自定義View 繪制五角星

之前寫過的App里有評分的功能,而顯示評分一般使用系統(tǒng)的RatingBar再加自定義,一切都很完美,但是產(chǎn)品提了一個需求,例如4.6、4.7、5.8分,不要顯示為4個星星加一個半星(4.5分),而是能顯示出區(qū)別。(系統(tǒng)的RatingBar必須滿正整數(shù)才可以滿星,如果沒滿,還是顯示一半的效果)

這時系統(tǒng)的RatingBar就不滿足需求了,需要我們自定義控件,當時需求太趕了,需求先放下來了,既然要繪制星星,我們就從繪制單個星星開始吧,后續(xù)只要對星星做一層PorterDuffXfermode處理即可。

Android 自定義View 繪制五角星.png

原理

我們以星星的中心為圓心,對五角星的5個端點畫一個外接圓。以五角星的內(nèi)部5個小角畫一個內(nèi)接圓,所以有2個圓。

原理圖解.png
  1. 五角星的5個頂點,將360的圓平分5份,平均角度為72度。
  2. 取一個90度直角為參考,90度直角將右上角的部分分為2個角,分別是大的角和小的角,大的角為72度,所以小角的角度為90度減去72度,為18度。
  3. 我們再計算出一半的平均角度,72除以2,為36度。
  4. 而內(nèi)角,就是凹進去的那個小角的角度就可以計算出來,36度加18度,為54度。
  5. 知道2個角的角度,以及外接圓和內(nèi)接圓的半徑,就可以用三角函數(shù)計算出坐標點。

文字描述有點不清楚,具體原理可以觀看視頻教程,慕課網(wǎng)Web前端 Canvas畫星星教程

我也是聽了一遍講解后,用Android的Canvas畫一次,Web端的Canvas雖然API有點不一樣,但也是類似的,有些地方要稍微處理一下,例如Web端的Canvas的beginPath(x,y)是直接在點坐標x,y開始,而不經(jīng)過中心,Android端的Canvas會經(jīng)過0,0點,所以第一個點我們要先將Path調(diào)用moveTo(x,y),移動到第一個點,再繼續(xù)lineTo(x,y)下一個點。最后調(diào)用close()閉合Path。

完整代碼

  • 自定義屬性
<declare-styleable name="StarsView">
    <!-- 星星的顏色 -->
    <attr name="stv_color" format="color" />
    <!-- 星星的邊數(shù) -->
    <attr name="stv_num" format="integer|dimension|reference" />
    <!-- 邊的線寬 -->
    <attr name="stv_edge_line_width" format="float|dimension|reference" />
    <!-- 填充風格 -->
    <attr name="stv_style" format="enum">
        <!-- 填滿 -->
        <enum name="fill" value="1" />
        <!-- 描邊 -->
        <enum name="stroke" value="2" />
    </attr>
</declare-styleable>
  • Java代碼
public class StarsView extends View {
    /**
     * View默認最小寬度
     */
    private static final int DEFAULT_MIN_WIDTH = 100;
    /**
     * 風格,填滿
     */
    private static final int STYLE_FILL = 1;
    /**
     * 風格,描邊
     */
    private static int STYLE_STROKE = 2;

    /**
     * 控件寬
     */
    private int mViewWidth;
    /**
     * 控件高
     */
    private int mViewHeight;
    /**
     * 外邊大圓的半徑
     */
    private float mOutCircleRadius;
    /**
     * 里面小圓的的半徑
     */
    private float mInnerCircleRadius;
    /**
     * 畫筆
     */
    private Paint mPaint;
    /**
     * 多少個角的五角星
     */
    private int mAngleNum;
    /**
     * 星星的路徑
     */
    private Path mPath;
    /**
     * 星星的顏色
     */
    private int mColor;
    /**
     * 邊的線寬
     */
    private float mEdgeLineWidth;
    /**
     * 填充風格
     */
    private int mStyle;

    public StarsView(Context context) {
        this(context, null);
    }

    public StarsView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StarsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        initAttr(context, attrs, defStyleAttr);
        //取消硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        //畫筆
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        if (mStyle == STYLE_FILL) {
            mPaint.setStyle(Paint.Style.FILL);
        } else if (mStyle == STYLE_STROKE) {
            mPaint.setStyle(Paint.Style.STROKE);
        }
        mPaint.setColor(mColor);
        mPaint.setStrokeWidth(mEdgeLineWidth);
    }

    private void initAttr(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        int defaultColor = Color.argb(255, 0, 0, 0);
        int defaultNum = 5;
        int mineNum = 2;
        float defaultEdgeLineWidth = dip2px(context, 1f);
        int defaultStyle = STYLE_STROKE;
        if (attrs != null) {
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.StarsView, defStyleAttr, 0);
            mColor = array.getColor(R.styleable.StarsView_stv_color, defaultColor);
            int num = array.getInt(R.styleable.StarsView_stv_num, defaultNum);
            mAngleNum = num <= mineNum ? mineNum : num;
            mEdgeLineWidth = array.getDimension(R.styleable.StarsView_stv_edge_line_width, defaultEdgeLineWidth);
            mStyle = array.getInt(R.styleable.StarsView_stv_style, defaultStyle);
            array.recycle();
        } else {
            mColor = defaultColor;
            mAngleNum = defaultNum;
            mEdgeLineWidth = defaultEdgeLineWidth;
            mStyle = defaultStyle;
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
        //計算外邊大圓的半徑
        mOutCircleRadius = (Math.min(mViewWidth, mViewHeight) / 2f) * 0.95f;
        //計算里面小圓的的半徑
        mInnerCircleRadius = (Math.min(mViewWidth, mViewHeight) / 2f) * 0.5f;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //將畫布中心移動到中心點
        canvas.translate(mViewWidth / 2, mViewHeight / 2);
        //畫星星
        drawStars(canvas);
    }

    /**
     * 畫星星
     */
    private void drawStars(Canvas canvas) {
        //計算平均角度,例如360度分5份,每一份角都為72度
        float averageAngle = 360f / mAngleNum;
        //計算大圓的外角的角度,從右上角為例計算,90度的角減去一份角,得出剩余的小角的角度,例如90 - 72 = 18 度
        float outCircleAngle = 90 - averageAngle;
        //一份平均角度的一半,例如72 / 2 = 36度
        float halfAverageAngle = averageAngle / 2f;
        //計算出小圓內(nèi)角的角度,36 + 18 = 54 度
        float internalAngle = halfAverageAngle + outCircleAngle;
        //創(chuàng)建2個點
        Point outCirclePoint = new Point();
        Point innerCirclePoint = new Point();
        if (mPath == null) {
            mPath = new Path();
        }
        mPath.reset();
        for (int i = 0; i < mAngleNum; i++) {
            //計算大圓上的點坐標
            //x = Math.cos((18 + 72 * i) / 180f * Math.PI) * 大圓半徑
            //y = -Math.sin((18 + 72 * i)/ 180f * Math.PI) * 大圓半徑
            outCirclePoint.x = (int) (Math.cos(angleToRadian(outCircleAngle + i * averageAngle)) * mOutCircleRadius);
            outCirclePoint.y = (int) -(Math.sin(angleToRadian(outCircleAngle + i * averageAngle)) * mOutCircleRadius);
            //計算小圓上的點坐標
            //x = Math.cos((54 + 72 * i) / 180f * Math.PI ) * 小圓半徑
            //y = -Math.sin((54 + 72 * i) / 180 * Math.PI ) * 小圓半徑
            innerCirclePoint.x = (int) (Math.cos(angleToRadian(internalAngle + i * averageAngle)) * mInnerCircleRadius);
            innerCirclePoint.y = (int) -(Math.sin(angleToRadian(internalAngle + i * averageAngle)) * mInnerCircleRadius);
            //第一次,先移動到第一個大圓上的點
            if (i == 0) {
                mPath.moveTo(outCirclePoint.x, outCirclePoint.y);
            }
            //坐標連接,先大圓角上的點,再到小圓角上的點
            mPath.lineTo(outCirclePoint.x, outCirclePoint.y);
            mPath.lineTo(innerCirclePoint.x, innerCirclePoint.y);
        }
        mPath.close();
        canvas.drawPath(mPath, mPaint);
    }

    /**
     * 角度轉(zhuǎn)弧度,由于Math的三角函數(shù)需要傳入弧度制,而不是角度值,所以要角度換算為弧度,角度 / 180 * π
     *
     * @param angle 角度
     * @return 弧度
     */
    private double angleToRadian(float angle) {
        return angle / 180f * Math.PI;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(handleMeasure(widthMeasureSpec), handleMeasure(heightMeasureSpec));
    }

    /**
     * 處理MeasureSpec
     */
    private int handleMeasure(int measureSpec) {
        int result = DEFAULT_MIN_WIDTH;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            //處理wrap_content的情況
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    public static int dip2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }
}
  • 簡單使用
<com.zh.cavas.sample.widget.StarsView
    android:id="@+id/stars"
    android:layout_width="30dp"
    android:layout_height="30dp"
    android:layout_margin="10dp"
    app:stv_color="#0000FF"
    app:stv_edge_line_width="1dp"
    app:stv_num="5"
    app:stv_style="stroke" />

<com.zh.cavas.sample.widget.StarsView
    android:id="@+id/stars2"
    android:layout_width="30dp"
    android:layout_height="30dp"
    android:layout_margin="10dp"
    app:stv_color="#0000FF"
    app:stv_edge_line_width="1dp"
    app:stv_num="5"
    app:stv_style="fill" />
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容