自定義view之無所不能的path

最近專案中需要完成以下這個需求
這裡寫圖片描述

UI給我了五張圖片,我感覺太浪費了,自定義view完全可以做而且適配起來更加的方便

最終實現效果

  • 專案效果
    這裡寫圖片描述

  • 擴充套件
    擴充套件1

    擴充套件2

需要知道技術點

在實現這個過程之前,我們需要了解path的一系列的原理(如果你瞭解path的用法直接跳過)

PathMeasure(是一個用來測量Path的類,主要有以下方法)

這裡寫圖片描述

  • setPath、 isClosed 和 getLength

這三個方法都如字面意思一樣,非常簡單,這裡就簡單是敘述一下,不再過多講解。
setPath 是 PathMeasure 與 Path 關聯的重要方法,效果和 建構函式 中兩個引數的作用是一樣的。
isClosed 用於判斷 Path 是否閉合,但是如果你在關聯 Path 的時候設定 forceClosed 為 true 的話,這個方法的返回值則一定為true。
getLength 用於獲取 Path 的總長度

  • getSegment
//返回值(boolean)    判斷擷取是否成功    true 表示擷取成功,結果存入dst中,false 擷取失敗,不會改變dst中內容
//startD    開始擷取位置距離 Path 起點的長度 取值範圍: 0 <= startD < stopD <= Path總長度
//stopD 結束擷取位置距離 Path 起點的長度 取值範圍: 0 <= startD < stopD <= Path總長度
//dst   擷取的 Path 將會新增到 dst 中    注意: 是新增,而不是替換
//startWithMoveTo   起始點是否使用 moveTo  用於保證擷取的 Path 第一個點位置不變
//如果 startD、stopD 的數值不在取值範圍 [0, getLength] 內,或者 startD == stopD 則返回值為 false,不會改變 dst 內容。
//如果在安卓4.4或者之前的版本,在預設開啟硬體加速的情況下,更改 dst 的內容後可能繪製會出現問題,請關閉硬體加速或者給 dst 新增一個單個操作,例如: dst.rLineTo(0, 0)

boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)
  • getPosTan
/*這個方法是用於得到路徑上某一長度的位置以及該位置的正切值:
引數    作用    備註
返回值(boolean)    判斷獲取是否成功    true表示成功,資料會存入 pos 和 tan 中,
false 表示失敗,pos 和 tan 不會改變
distance    距離 Path 起點的長度   取值範圍: 0 <= distance <= getLength
pos 該點的座標值  座標值: (x==[0], y==[1])
tan 該點的正切值  正切值: (x==[0], y==[1])
*/
boolean getPosTan (float distance, float[] pos, float[] tan)
  • getMatrix

這個方法是用於得到路徑上某一長度的位置以及該位置的正切值的矩陣:

/*
返回值(boolean)    判斷獲取是否成功    true表示成功,資料會存入matrix中,false 失敗,matrix內容不會改變
distance    距離 Path 起點的長度   取值範圍: 0 <= distance <= getLength
matrix  根據 falgs 封裝好的matrix 會根據 flags 的設定而存入不同的內容
flags   規定哪些內容會存入到matrix中   可選擇
POSITION_MATRIX_FLAG(位置)
ANGENT_MATRIX_FLAG(正切)

*/
boolean getMatrix (float distance, Matrix matrix, int flags)

實現

可以明顯的看出這個view的5個園的圓心都在一個大的圓上
這裡寫圖片描述

通過path得到一個園,然後將圓分割5份

Path pathCircle = new Path();
pathCircle.addCircle(with / 2, hight / 2, hight / 2 - pading - radius, Path.Direction.CW);

通過PathMeasure的getPosTan方法得到等分點在圓上的座標,然後判斷當前的狀態,給選中的狀態圓不同的顏色值

 float[] position = new float[2];
        for (int index = 0; index < 5; index++) {
            if (currentPosition == index) {
                paint.setColor(Color.RED);
            } else {
                paint.setColor(Color.BLUE);
            }
            float allLength = pathMeasure.getLength();
            distance = (allLength / 5) * (index + 1);
            pathMeasure.getPosTan(distance, position, tan);
            canvas.drawCircle(position[0], position[1], radius, paint);
   }

這裡寫圖片描述

實現完以後我們發現問題,圓的位置每個圓環的位置和效果圖不是一樣的,那是為什麼呢?

其實在path新增大圓的時候我們只能控制path路徑的軌跡方向,並不能指定其開始位置,而且現在我們寫死了很多變數:顏色,圓環數等*
解決辦法:那我們用arc(圓弧)去畫指定其實位置;通過指定要屬性實現動態新增屬性;

優化

畫出圓弧,指定開始位置為正上方及時-90°

Path pathCircle = new Path();
RectF rectF = new RectF(pading + radius, pading + radius, with - pading - radius, hight - pading - radius);
pathCircle.arcTo(rectF, -90, 359);

通過自定義屬性動態指定引數

    //    寬
    private int with;
    //    高
    private int hight;
    //    間距
    private int pading;
    //    小圓環半徑
    private int radius;
    //    圓環寬度
    private int paintWith;
    //    圓環數
    private int pie;
    //    當前選中圓環
    private int currentPosition;
    //    正常顏色
    private int normalColor;
    //    選中顏色
    private int clickColor;
    //    畫筆
    private Paint paint;


    public ProgressCircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressCircleOldView);
        pading = a.getDimensionPixelOffset(R.styleable.ProgressCircleOldView_pading, 0);
        radius = a.getDimensionPixelOffset(R.styleable.ProgressCircleOldView_radius, 10);
        paintWith = a.getDimensionPixelOffset(R.styleable.ProgressCircleOldView_paintWith, 4);
        pie = a.getInt(R.styleable.ProgressCircleOldView_pie, 5);
        currentPosition = a.getInt(R.styleable.ProgressCircleOldView_currentPosition, 0);
        normalColor = a.getColor(R.styleable.ProgressCircleOldView_normalColor, Color.BLUE);
        clickColor = a.getColor(R.styleable.ProgressCircleOldView_clickColor, Color.RED);
        a.recycle();
        initPaint();
    }

對應的xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ProgressCircleOldView">
        <!--間距-->
        <attr name="pading" format="dimension"/>
        <!--小圓環半徑-->
        <attr name="radius" format="dimension"/>
        <!--圓環寬度-->
        <attr name="paintWith" format="dimension"/>
        <!--圓環數-->
        <attr name="pie" format="integer"/>
        <!--當前選中圓環-->
        <attr name="currentPosition" format="integer"/>
        <!--正常顏色-->
        <attr name="normalColor" format="color"/>
        <!-- 選中顏色-->
        <attr name="clickColor" format="color"/>
    </declare-styleable>
</resources>

得到座標點,畫出圓

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float[] position = new float[2];
        float[] tan = new float[2];
        float distance;
        Path pathCircle = new Path();
        RectF rectF = new RectF(pading + radius, pading + radius, with - pading - radius, hight - pading - radius);
        pathCircle.arcTo(rectF, -90, 359);
        PathMeasure pathMeasure = new PathMeasure(pathCircle, false);
        for (int index = 0; index < pie; index++) {
            if (currentPosition == index) {
                paint.setColor(clickColor);
            } else {
                paint.setColor(normalColor);
            }
            float allLength = pathMeasure.getLength();
            distance = (allLength / pie) * (index);
            pathMeasure.getPosTan(distance, position, tan);
            canvas.drawCircle(position[0], position[1], radius, paint);
        }
    }

到這裡我們基本已經完成了這個需求了但是估計大家還是沒有講PathMeasure沒有很好的理解,所以就有了下面的擴充套件

擴充套件

這裡寫圖片描述

上面的效果在很多場景中我們都能用到,不如載入、經度顯示等;其實通過動畫我們也可以實現,但是自定義view也是可以的,而且它的效率更高,
靈活性更加好,功能也可以做的更加強大,主要是你實現起來還很簡單哦!

其實上面的矩形和圓軌跡都是走的同樣的邏輯,不過是path添加了不同的圖形,所以你可以自由發揮哦,所以就拿上面的圓形進度為例子來講解了

path給定一個圖形

  Path path = new Path();
  path.addCircle(600, 400, 100, Path.Direction.CCW);

通過比getPosTan得到位置和偏移量

//        按照比例獲取
        progress = progress < 1 ? progress + 0.0005 : 0;
        Matrix matrix = new Matrix();
        paint.setColor(Color.YELLOW);
        measure.getPosTan((int) (measure.getLength() * progress), position, tan);

通過得到的點座標畫出箭頭

        Path path1 = new Path();
        path1.moveTo(position[0] - 20, position[1] + 20);
        path1.lineTo(position[0], position[1]);
        path1.lineTo(position[0] + 20, position[1] + 20);
//        是否閉合,閉合就是三角形了
        path1.close();

通過tan得到箭頭的偏移量

  Path path2 = new Path();
        float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
        matrix.setRotate(degrees + 90, position[0], position[1]);
        path2.addPath(path1, matrix);

通過getSegment得到進度上擷取的弧線,連結箭頭

  //        進度線
        measure.getSegment(-1000, (int) (measure.getLength() * progress), path2, true);
        paint.setColor(Color.BLUE);
        canvas.drawPath(path2, paint);

最後不斷的重新整理介面重畫

    /**
     * 繪製panth上每一個點的位置
     * 帶箭頭的進度框
     *
     * @param canvas
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void PaintMatr(Canvas canvas) {
        paint.setStrokeWidth(10);
        paint.setStyle(Paint.Style.STROKE);
        Path path = new Path();
        path.addCircle(600, 400, 100, Path.Direction.CCW);
        PathMeasure measure = new PathMeasure(path, false);
//        按照比例獲取
        progress = progress < 1 ? progress + 0.0005 : 0;
        Matrix matrix = new Matrix();
        paint.setColor(Color.YELLOW);
        measure.getPosTan((int) (measure.getLength() * progress), position, tan);
        canvas.drawPath(path, paint);

//        箭頭
        paint.setColor(Color.RED);
        Path path1 = new Path();
        path1.moveTo(position[0] - 20, position[1] + 20);
        path1.lineTo(position[0], position[1]);
        path1.lineTo(position[0] + 20, position[1] + 20);
//        是否閉合,閉合就是三角形了
        path1.close();
        Path path2 = new Path();
        float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
        matrix.setRotate(degrees + 90, position[0], position[1]);
        path2.addPath(path1, matrix);
        //        進度線
        measure.getSegment(-1000, (int) (measure.getLength() * progress), path2, true);
        paint.setColor(Color.BLUE);
        canvas.drawPath(path2, paint);
        invalidate();
    }

到這裡你也是path就完事了 no no no其實path還能結合SVG( 是一種向量圖,內部用的是 xml 格式化儲存方式儲存這操作和資料,你完全可以將 SVG 看作是 Path 的各項操作簡化書寫後的儲存格式)

svg和path的結合

SVG 是一種向量圖,內部用的是 xml 格式化儲存方式儲存這操作和資料,你完全可以將 SVG 看作是 Path 的各項操作簡化書寫後的儲存格式
他們結合能創找出很多意想不到的東西,有興趣的同學可以自己去研究一下

demo

原始碼

建議

.