Android鬼點子-自定義View就像PS

我是綠色大米呀發表於2017-12-18

分享一個最近實現的一個效果,主要是用來顯示分數。分數的範圍是0~100,沒有小數。

圖1
我一步一步的分解開,來說說我是怎麼實現的。首先來一張動態效果。
Android鬼點子-自定義View就像PS
設定分數後,分數會從0到目標分數增長,並伴隨圓環的動畫。

在你閱讀此文之前最好先了解自定義View的步驟,比如onMeasure,onLayout,onDraw等等。這類的文章有很多,我這裡不再一一贅述了。

準備階段

一.首先建好Activity,和Activity的佈局:

package com.greendami.gdm

import android.app.Activity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        bt_0.setOnClickListener { pp.setProgress("0",0f,true) }
        bt_100.setOnClickListener { pp.setProgress("100",100f,true) }
    }
}
複製程式碼

佈局檔案:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:background="@color/colorPrimary">

    <LinearLayout
        android:layout_marginBottom="100dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center">
        <Button
            android:id="@+id/bt_0"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="0" />
        <Button
            android:id="@+id/bt_100"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="100" />
    </LinearLayout>

    <com.greendami.gdm.PPCircleProgressView
        android:id="@+id/pp"
        android:layout_width="200dp"
        android:layout_height="200dp" />

</LinearLayout>
複製程式碼

然後是一個工具類,主要是用於dp轉px:

package com.greendami.gdm;

import android.content.Context;

/**
 * Created by hsy on 2016/4/8.
 */
public class DPUnitUtil {
    /**
     * 將px值轉換為dip或dp值,保證尺寸大小不變
     *
     * @param pxValue (DisplayMetrics類中屬性density)
     * @return
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

}

複製程式碼

二.建一個PPCircleProgressView類:

自定義的View就叫做PPCircleProgressView。 然後建一個類PPCircleProgressView,繼承View類。

package com.greendami.gdm;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;


/**
 * 圓形進度條
 * Created by GreendaMi on 2017/3/1.
 */

public class PPCircleProgressView extends View {

    private float progress = 0; //顯示的進度
    private String strprogress = "100"; //顯示的進度
    private int mLayoutSize = 100;//整個控制元件的尺寸(方形)
    public int mColor;//主要顏色
    public int mColorBackground;

    Context mContext;

    private float now = 0; //當前的進度

    public PPCircleProgressView(Context context) {
        super(context);
        mContext = context;
    }

    public PPCircleProgressView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        mColor = context.getResources().getColor(R.color.yellow);
        mColorBackground = context.getResources().getColor(R.color.colorPrimary);
    }

    public PPCircleProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
    }

}
複製程式碼

因為我只用在主xml中,所以要實現帶2個引數的構造方法,在這個方法中取了2個顏色。一般的做法是取style檔案,但是我偷懶一下,直接取的color檔案中的顏色。

到此準備工作結束。

實現階段

三.測量寬高

這是一個方形的View,我偷懶,就把方形定死了,直接在xml給定dp值,設定寬高。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        mLayoutSize = Math.min(widthSpecSize, heightSpecSize);
        if (mLayoutSize == 0) {
            mLayoutSize = Math.max(widthSpecSize, heightSpecSize);
        }
        setMeasuredDimension(mLayoutSize, mLayoutSize);
    }
複製程式碼

三.繪畫

在onDraw方法中繪畫。

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

        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(0x66d4b801);
        paint.setStyle(Paint.Style.FILL); //設定空心
    }

複製程式碼

首先是初始化畫筆,當然我知道這直接初始化不太好,一般都是在某處初始化一次,然後呼叫paint的reset()方法重置。

第一筆就是最外面的一個圓線,顏色是半透明的黃

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


        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(0x66d4b801);//透明的黃色
        paint.setStyle(Paint.Style.FILL); //設定填充

        //畫半透明黃線
        int centre = getWidth() / 2; //獲取圓心的x座標
        float radius = centre * 0.96f; //圓環的半徑
        canvas.drawCircle(centre, centre, radius, paint); //畫出圓環
        .
        .
        .
        .
    }

複製程式碼

效果是這樣的:

圖2

這個圓的半徑是寬的一半,但是由於那個小水滴的底部會在這個圓的外側,所以這個圓不可以佔滿整個View,所以 centre * 0.96f,縮小了這個半徑。

接著再畫一個紅色的圓,把半徑減小1,畫在上面那個圓的中心,這樣就是一個圓線了。

//接上面在onDraw中
paint.setColor(mColorBackground);
canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 1f), paint); //畫出圓環
複製程式碼

效果是這樣的:

圖3

接著畫中間一段一段的感覺的部分,這裡有一種被‘點亮’的感覺。背景‘未點亮’的是半透明的黃色,點亮就是正常的黃色。‘未點亮’是一個360度的扇形,‘點亮’的是角度會變化的扇形。先畫‘未點亮’的部分。

float gap = DPUnitUtil.dip2px(mContext, 14);
RectF rectF = new RectF(gap, gap, mLayoutSize - gap, mLayoutSize - gap);//找出扇形所在的矩形,距離View的邊框上下左右各縮14dp。

paint.setColor(0x44d4b801);
canvas.drawArc(rectF, 0, 360, true, paint);
複製程式碼

效果如下:

圖4
其實上面的效果可以畫圓而不是扇形。 下一步,畫點亮的部分。這裡的每一段是15°,所以有些數值需要四捨五入。

//15度一個格子,防止佔半個格子
int endR = (int) (360 * (now / 100) / 15) * 15;
paint.setColor(mColor);
canvas.drawArc(rectF, -90, endR, true, paint);
複製程式碼

endR是根據當前顯示的分數計算的扇形的結束角度,這裡會根據15°進行四捨五入。開始角度是-90°,扇形是從12點鐘方向開始。這裡的now就是當前的分數,因為是有動畫的,所以now的值會變化,具體是如何變化的後面說,這裡只是根據now值畫扇形。

圖5

接著用一個實心的紅色圓把這個扇形的內部‘蓋住’。這個圓的半徑再一次的縮小。這裡乘了一個0.83

//畫紅圓
paint.setColor(mColorBackground);        paint.setStyle(Paint.Style.FILL); //設定空心
radius = radius * 0.83f; //圓環的半徑
canvas.drawCircle(centre, centre, radius, paint); //畫出圓環
複製程式碼

效果是這樣的:

圖6

然後就是把這個比較寬的圓環切成一段一段的。形成‘斷開’的感覺。我用的就是比較寬的紅線,每旋轉15°畫一條。

        paint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 2));
        for (int r = 0; r < 360; r = r + 15) {
            canvas.drawLine(centre + (float) ((centre - gap) * Math.sin(Math.toRadians(r))),
                    centre - (float) ((centre - gap) * Math.cos(Math.toRadians(r))),
                    centre - (float) ((centre - gap) * Math.sin(Math.toRadians(r))),
                    centre + (float) ((centre - gap) * Math.cos(Math.toRadians(r))), paint);
        }
複製程式碼

為了方便看,我把線設定成的白色,是這樣的:

圖7

實際設成紅色,是這樣的:

圖8

然後畫內圈的一個淺淺的圓環,這裡的方法和外圈的畫法一樣:

        paint.setColor(0x44d4b801);
        canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 2f), paint); //畫出圓環
        paint.setColor(mColorBackground);
        canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 2.5f), paint); //畫出圓環
複製程式碼

效果如下,好像不太明顯:

圖9

接下來是畫上面的文字,如果文字是空白的,會畫兩條橫線:


        //到此,背景繪製完畢

        String per = (int) now + "";

        //寫百分比
        if ("".equals(strprogress)) {
            paint.setColor(mColor);
            paint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 2));
            canvas.drawLine(centre * 0.77f, centre, centre * 0.95f, centre, paint);
            canvas.drawLine(centre * 1.05f, centre, centre * 1.23f, centre, paint);
        } else {
            paint.setColor(mColor);
            paint.setTextSize(mLayoutSize / 4f);//控制文字大小
            Paint paint2 = new Paint();
            paint2.setAntiAlias(true);
            paint2.setTextSize(mLayoutSize / 12);//控制文字大小
            paint2.setColor(mColor);
            canvas.drawText(per,
                    centre - 0.5f * (paint.measureText(per)),
                    centre - 0.5f * (paint.ascent() + paint.descent()),
                    paint);
            canvas.drawText("分",
                    centre + 0.5f * (paint.measureText((int) now + "") + paint2.measureText("分")),
                    centre - 0.05f * (paint.ascent() + paint.descent()),
                    paint2);
        }
複製程式碼

文字的大小會根據控制元件的尺寸進行計算。然後就是隨便測量了一下文字的長度,計算文字的位置。上面這些數字後是目測隨便寫的。數字是根據now來變化的。

接下來畫最外面的小水滴,小水滴的位置是和扇形的endR一致。 先畫一個小球:

        centre = getWidth() / 2;
        canvas.drawCircle(centre + (float) ((centre * 0.95f) * Math.sin(Math.toRadians(endR))),
                centre - (float) ((centre * 0.95f) * Math.cos(Math.toRadians(endR))), centre * 0.04f, paint);
複製程式碼

小球的圓心是根據endR和最外面的圓環的半徑計算的,小球的半徑就是在外面圓環到View邊的距離(1-0.96)。

圖10
接下來時畫尖尖的角。我們需要一個Path。這個角的頂點在小球的圓心和View圓心的連線上,角度是endR。另外兩個點是角的頂點與小球的切點,這個就比較難了。因為這個水滴比較小,所以其實這兩個點不用十分精確,我把endR分別向左右移動2.5°,然後半徑從centre * 0.95f稍稍減小了一點到centre * 0.94f,差不多找到了‘切點’的位置。

        Path p = new Path();
        p.moveTo(centre + (float) ((centre * 0.86f) * Math.sin(Math.toRadians(endR))),
                centre - (float) ((centre * 0.86f) * Math.cos(Math.toRadians(endR))));//頂點

        p.lineTo(centre + (float) ((centre * 0.94f) * Math.sin(Math.toRadians(endR + 2.5))),
                centre - (float) ((centre * 0.94f) * Math.cos(Math.toRadians(endR + 2.5))));
        p.lineTo(centre + (float) ((centre * 0.94f) * Math.sin(Math.toRadians(endR - 2.5))),
                centre - (float) ((centre * 0.94f) * Math.cos(Math.toRadians(endR - 2.5))));
        p.close();
        canvas.drawPath(p, paint);
複製程式碼

效果如下:

圖11

最後動起來:

if (now < progress - 1) {
            now = now + 1;
            postInvalidate();
        } else if (now < progress) {
            now = (int) progress;
            postInvalidate();
        }
複製程式碼

now是當前動畫的顯示數值,progress是最終的顯示數值,如果now < progress - 1則呼叫postInvalidate()重繪。帶刺onDraw方法結束。

最後加上外部的呼叫設設值:

/**
     * 外部回撥
     *
     * @param strprogress 顯示調進度文字,如果是"",或者null了,則顯示兩條橫線
     * @param progress    進度條調進度
     * @param isAnim      進度條是否需要動畫
     */
    public void setProgress(String strprogress, float progress, boolean isAnim) {
        if (strprogress == null) {
            this.strprogress = "";
        } else {
            this.strprogress = strprogress;
        }
        this.now = 0;
        this.progress = progress;


        if (!isAnim) {
            now = progress;
        }
        postInvalidate();
    }
複製程式碼

完整程式碼

package com.greendami.gdm;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;


/**
 * 圓形進度條
 * Created by GreendaMi on 2017/3/1.
 */

public class PPCircleProgressView extends View {

    private float progress = 0; //顯示的進度
    private String strprogress = "100"; //顯示的進度
    private int mLayoutSize = 100;//整個控制元件的尺寸(方形)
    public int mColor;//主要顏色
    public int mColorBackground;

    Context mContext;

    private float now = 0; //當前的進度

    public PPCircleProgressView(Context context) {
        super(context);
        mContext = context;
    }

    public PPCircleProgressView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        mColor = context.getResources().getColor(R.color.yellow);
        mColorBackground = context.getResources().getColor(R.color.colorPrimary);
    }

    public PPCircleProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        mLayoutSize = Math.min(widthSpecSize, heightSpecSize);
        if (mLayoutSize == 0) {
            mLayoutSize = Math.max(widthSpecSize, heightSpecSize);
        }
        setMeasuredDimension(mLayoutSize, mLayoutSize);
    }

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


        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(0x66d4b801);
        paint.setStyle(Paint.Style.FILL); //設定空心

        //畫灰線
        int centre = getWidth() / 2; //獲取圓心的x座標
        float radius = centre * 0.96f; //圓環的半徑
        canvas.drawCircle(centre, centre, radius, paint); //畫出圓環

        paint.setColor(mColorBackground);
        canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 1f), paint); //畫出圓環


        float gap = DPUnitUtil.dip2px(mContext, 14);
        RectF rectF = new RectF(gap, gap, mLayoutSize - gap, mLayoutSize - gap);

        //15度一個格子,防止佔半個格子
        int endR = (int) (360 * (now / 100) / 15) * 15;
        paint.setColor(0x44d4b801);
        canvas.drawArc(rectF, 0, 360, true, paint);

        paint.setColor(mColor);
        canvas.drawArc(rectF, -90, endR, true, paint);

        //畫紅圓
        paint.setColor(mColorBackground);
        paint.setStyle(Paint.Style.FILL); //設定空心
        radius = radius * 0.83f; //圓環的半徑
        canvas.drawCircle(centre, centre, radius, paint); //畫出圓環

        //畫線,許多的線,15度畫一條,線的寬度是2dp
        paint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 2));

        for (int r = 0; r < 360; r = r + 15) {
            canvas.drawLine(centre + (float) ((centre - gap) * Math.sin(Math.toRadians(r))),
                    centre - (float) ((centre - gap) * Math.cos(Math.toRadians(r))),
                    centre - (float) ((centre - gap) * Math.sin(Math.toRadians(r))),
                    centre + (float) ((centre - gap) * Math.cos(Math.toRadians(r))), paint);
        }

        paint.setColor(0x44d4b801);
        canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 2f), paint); //畫出圓環
        paint.setColor(mColorBackground);
        canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 2.5f), paint); //畫出圓環

        //到此,背景繪製完畢

        String per = (int) now + "";

        //寫百分比
        if ("".equals(strprogress)) {
            paint.setColor(mColor);
            paint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 2));
            canvas.drawLine(centre * 0.77f, centre, centre * 0.95f, centre, paint);
            canvas.drawLine(centre * 1.05f, centre, centre * 1.23f, centre, paint);
        } else {
            paint.setColor(mColor);
            paint.setTextSize(mLayoutSize / 4f);//控制文字大小
            Paint paint2 = new Paint();
            paint2.setAntiAlias(true);
            paint2.setTextSize(mLayoutSize / 12);//控制文字大小
            paint2.setColor(mColor);
            canvas.drawText(per,
                    centre - 0.5f * (paint.measureText(per)),
                    centre - 0.5f * (paint.ascent() + paint.descent()),
                    paint);
            canvas.drawText("分",
                    centre + 0.5f * (paint.measureText((int) now + "") + paint2.measureText("分")),
                    centre - 0.05f * (paint.ascent() + paint.descent()),
                    paint2);
        }

        //外部小球
        centre = getWidth() / 2;
        canvas.drawCircle(centre + (float) ((centre * 0.95f) * Math.sin(Math.toRadians(endR))),
                centre - (float) ((centre * 0.95f) * Math.cos(Math.toRadians(endR))), centre * 0.04f, paint);

        Path p = new Path();
        p.moveTo(centre + (float) ((centre * 0.86f) * Math.sin(Math.toRadians(endR))),
                centre - (float) ((centre * 0.86f) * Math.cos(Math.toRadians(endR))));

        p.lineTo(centre + (float) ((centre * 0.94f) * Math.sin(Math.toRadians(endR + 2.5))),
                centre - (float) ((centre * 0.94f) * Math.cos(Math.toRadians(endR + 2.5))));
        p.lineTo(centre + (float) ((centre * 0.94f) * Math.sin(Math.toRadians(endR - 2.5))),
                centre - (float) ((centre * 0.94f) * Math.cos(Math.toRadians(endR - 2.5))));
        p.close();
        canvas.drawPath(p, paint);

        if (now < progress - 1) {
            now = now + 1;
            postInvalidate();
        } else if (now < progress) {
            now = (int) progress;
            postInvalidate();
        }
    }


    /**
     * 外部回撥
     *
     * @param strprogress 顯示調進度文字,如果是"",或者null了,則顯示兩條橫線
     * @param progress    進度條調進度
     * @param isAnim      進度條是否需要動畫
     */
    public void setProgress(String strprogress, float progress, boolean isAnim) {
        if (strprogress == null) {
            this.strprogress = "";
        } else {
            this.strprogress = strprogress;
        }
        this.now = 0;
        this.progress = progress;


        if (!isAnim) {
            now = progress;
        }
        postInvalidate();
    }


}
複製程式碼

相關文章