Android 自定義View 繪制五角星

之前寫(xiě)過(guò)的App里有評(píng)分的功能,而顯示評(píng)分一般使用系統(tǒng)的RatingBar再加自定義备图,一切都很完美灿巧,但是產(chǎn)品提了一個(gè)需求赶袄,例如4.6张咳、4.7能真、5.8分,不要顯示為4個(gè)星星加一個(gè)半星(4.5分),而是能顯示出區(qū)別扛芽。(系統(tǒng)的RatingBar必須滿(mǎn)正整數(shù)才可以滿(mǎn)星,如果沒(méi)滿(mǎn)牛哺,還是顯示一半的效果)

這時(shí)系統(tǒng)的RatingBar就不滿(mǎn)足需求了蟆豫,需要我們自定義控件,當(dāng)時(shí)需求太趕了溉跃,需求先放下來(lái)了村刨,既然要繪制星星,我們就從繪制單個(gè)星星開(kāi)始吧撰茎,后續(xù)只要對(duì)星星做一層PorterDuffXfermode處理即可嵌牺。

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

原理

我們以星星的中心為圓心,對(duì)五角星的5個(gè)端點(diǎn)畫(huà)一個(gè)外接圓。以五角星的內(nèi)部5個(gè)小角畫(huà)一個(gè)內(nèi)接圓逆粹,所以有2個(gè)圓募疮。

原理圖解.png
  1. 五角星的5個(gè)頂點(diǎn),將360的圓平分5份僻弹,平均角度為72度阿浓。
  2. 取一個(gè)90度直角為參考,90度直角將右上角的部分分為2個(gè)角蹋绽,分別是大的角和小的角芭毙,大的角為72度,所以小角的角度為90度減去72度卸耘,為18度稿蹲。
  3. 我們?cè)儆?jì)算出一半的平均角度,72除以2鹊奖,為36度苛聘。
  4. 而內(nèi)角,就是凹進(jìn)去的那個(gè)小角的角度就可以計(jì)算出來(lái)忠聚,36度加18度设哗,為54度。
  5. 知道2個(gè)角的角度两蟀,以及外接圓和內(nèi)接圓的半徑网梢,就可以用三角函數(shù)計(jì)算出坐標(biāo)點(diǎn)。

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

我也是聽(tīng)了一遍講解后党涕,用Android的Canvas畫(huà)一次烦感,Web端的Canvas雖然API有點(diǎn)不一樣,但也是類(lèi)似的膛堤,有些地方要稍微處理一下手趣,例如Web端的Canvas的beginPath(x,y)是直接在點(diǎn)坐標(biāo)x,y開(kāi)始,而不經(jīng)過(guò)中心肥荔,Android端的Canvas會(huì)經(jīng)過(guò)0,0點(diǎn)绿渣,所以第一個(gè)點(diǎn)我們要先將Path調(diào)用moveTo(x,y),移動(dòng)到第一個(gè)點(diǎn)燕耿,再繼續(xù)lineTo(x,y)下一個(gè)點(diǎn)中符。最后調(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" />
    <!-- 填充風(fēng)格 -->
    <attr name="stv_style" format="enum">
        <!-- 填滿(mǎn) -->
        <enum name="fill" value="1" />
        <!-- 描邊 -->
        <enum name="stroke" value="2" />
    </attr>
</declare-styleable>
  • Java代碼
public class StarsView extends View {
    /**
     * View默認(rèn)最小寬度
     */
    private static final int DEFAULT_MIN_WIDTH = 100;
    /**
     * 風(fēng)格誉帅,填滿(mǎn)
     */
    private static final int STYLE_FILL = 1;
    /**
     * 風(fēng)格淀散,描邊
     */
    private static int STYLE_STROKE = 2;

    /**
     * 控件寬
     */
    private int mViewWidth;
    /**
     * 控件高
     */
    private int mViewHeight;
    /**
     * 外邊大圓的半徑
     */
    private float mOutCircleRadius;
    /**
     * 里面小圓的的半徑
     */
    private float mInnerCircleRadius;
    /**
     * 畫(huà)筆
     */
    private Paint mPaint;
    /**
     * 多少個(gè)角的五角星
     */
    private int mAngleNum;
    /**
     * 星星的路徑
     */
    private Path mPath;
    /**
     * 星星的顏色
     */
    private int mColor;
    /**
     * 邊的線寬
     */
    private float mEdgeLineWidth;
    /**
     * 填充風(fēng)格
     */
    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);
        //畫(huà)筆
        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;
        //計(jì)算外邊大圓的半徑
        mOutCircleRadius = (Math.min(mViewWidth, mViewHeight) / 2f) * 0.95f;
        //計(jì)算里面小圓的的半徑
        mInnerCircleRadius = (Math.min(mViewWidth, mViewHeight) / 2f) * 0.5f;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //將畫(huà)布中心移動(dòng)到中心點(diǎn)
        canvas.translate(mViewWidth / 2, mViewHeight / 2);
        //畫(huà)星星
        drawStars(canvas);
    }

    /**
     * 畫(huà)星星
     */
    private void drawStars(Canvas canvas) {
        //計(jì)算平均角度谭期,例如360度分5份,每一份角都為72度
        float averageAngle = 360f / mAngleNum;
        //計(jì)算大圓的外角的角度吧凉,從右上角為例計(jì)算隧出,90度的角減去一份角,得出剩余的小角的角度阀捅,例如90 - 72 = 18 度
        float outCircleAngle = 90 - averageAngle;
        //一份平均角度的一半胀瞪,例如72 / 2 = 36度
        float halfAverageAngle = averageAngle / 2f;
        //計(jì)算出小圓內(nèi)角的角度,36 + 18 = 54 度
        float internalAngle = halfAverageAngle + outCircleAngle;
        //創(chuàng)建2個(gè)點(diǎn)
        Point outCirclePoint = new Point();
        Point innerCirclePoint = new Point();
        if (mPath == null) {
            mPath = new Path();
        }
        mPath.reset();
        for (int i = 0; i < mAngleNum; i++) {
            //計(jì)算大圓上的點(diǎn)坐標(biāo)
            //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);
            //計(jì)算小圓上的點(diǎn)坐標(biāo)
            //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);
            //第一次饲鄙,先移動(dòng)到第一個(gè)大圓上的點(diǎn)
            if (i == 0) {
                mPath.moveTo(outCirclePoint.x, outCirclePoint.y);
            }
            //坐標(biāo)連接凄诞,先大圓角上的點(diǎn),再到小圓角上的點(diǎn)
            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);
    }
}
  • 簡(jiǎn)單使用
<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" />
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末汛蝙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子朴肺,更是在濱河造成了極大的恐慌窖剑,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件戈稿,死亡現(xiàn)場(chǎng)離奇詭異西土,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)鞍盗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)需了,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人般甲,你說(shuō)我怎么就攤上這事肋乍。” “怎么了欣除?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵住拭,是天一觀的道長(zhǎng)挪略。 經(jīng)常有香客問(wèn)我历帚,道長(zhǎng),這世上最難降的妖魔是什么杠娱? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任挽牢,我火速辦了婚禮,結(jié)果婚禮上摊求,老公的妹妹穿的比我還像新娘禽拔。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布睹栖。 她就那樣靜靜地躺著硫惕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪野来。 梳的紋絲不亂的頭發(fā)上恼除,一...
    開(kāi)封第一講書(shū)人閱讀 51,727評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音曼氛,去河邊找鬼豁辉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛舀患,可吹牛的內(nèi)容都是我干的徽级。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼聊浅,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼餐抢!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起低匙,我...
    開(kāi)封第一講書(shū)人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤弹澎,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后努咐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體苦蒿,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年渗稍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了佩迟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡竿屹,死狀恐怖报强,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拱燃,我是刑警寧澤秉溉,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站碗誉,受9級(jí)特大地震影響召嘶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜哮缺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一弄跌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尝苇,春花似錦铛只、人聲如沸埠胖。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)直撤。三九已至,卻和暖如春蜕着,著一層夾襖步出監(jiān)牢的瞬間谊惭,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工侮东, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留圈盔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓悄雅,卻偏偏與公主長(zhǎng)得像驱敲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宽闲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355