Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 儀表盤View

Android 儀表盤View

編輯:關於Android編程

導語

這裡展示的View估計項目中多半是用不到的,只是用來加深理解的。文章末尾會有全部的代碼,如果想研究可以復制過去直接運行,不需要額外的資源。

先看效果:
這裡寫圖片描述
這裡指針是通過手指來改變方向的,並不能通過數字參數來改變,如果需要,可以更改相應的代碼。

需要的數學知識

理論的涉及也非常簡單,如下所示:

在坐標系中,一個點與原點連線與X軸的正切值 tan = 點的縱坐標 ÷ 點的橫坐標 在每一個象限中,正切函數是單調函數;如圖所示:
這裡寫圖片描述

繪制流程

繪制由線段組成的圓弧

這裡寫圖片描述

利用線段的旋轉來繪圖

繪制上圖有多種方法,首先介紹一種簡單的方法:
將線段旋轉多個角度,這樣可以繪制出一個圓弧型:

private int width;
private int height;

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.translate(width / 2, height / 2);
    Paint mPaint = new Paint();
    mPaint.setStrokeWidth(5);
    for (int i = 0; i <= 360; i += 5) {               // 繪制圓形之間的連接線
        canvas.drawLine(0, 120, 0, 200, mPaint);
        canvas.rotate(10);
    }
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    width = w;
    height = h;
}

上述代碼的執行效果:
這裡寫圖片描述
雖然丑了點,但可以說明問題。不過這樣做,我們缺乏對弧形的控制。例如:如何實現圓弧呢?是不是要手動計算起始坐標,旋轉的角度,掃過的角度等各種各樣的問題?因為怕麻煩,這個方案就被我華麗的拋棄了。

利用兩個同心圓來繪制

這裡寫圖片描述

思路:

從圓心發射一條射線出來,與兩個圓相交於點A和點B,鏈接A與B,就可以劃出一條我們想要的線段。 均勻的發射多條射線,我們就可以得到一個由線段組成的圓弧。 如果兩個圓圈是圓弧的話,就可以達到我們所要的效果

所以,最終確定的步驟為:

畫一個大圓弧 畫一個縮小版的小圓弧 均勻地在兩個圓弧之間畫線段

用動態圖來展示下:

這裡寫圖片描述
相關代碼比較多,在文章末尾已經貼出來了(88-172行,代碼中有後續的細節處理,需要甄別下相關的代碼),這裡只是寫下思路,不再重復貼代碼了

畫個一個長度固定、原點確定,方向隨著手指變化的指針

這裡寫圖片描述
這步要實現的效果如上圖所示

假設,之前的指針為OZ,現在我們用手指觸摸了點A,這時我們希望指針變為OB,那麼,該如何實現呢?

獲取A點的坐標(通過onTouchEvent()可以獲取到) 畫取線段OA(O點為(0,0),所以可以畫取) 通過測量OA,可以利用PathMeasure.getPosTan()來獲取B點的坐標(指針的長度是固定的) 在Cavas中畫OB線段
這裡寫圖片描述

如果我們觸摸點為X,距離過短怎麼辦呢?

鏈接OX,並用MeasurePath來測量OX的長度,以及X的坐標(a,b) 計算OY與OX的比例 R = OY ÷ OX 計算Y點的坐標 x = a × R, y = b × R 在Cavas中畫OY線段
這裡寫圖片描述
相關代碼同樣比較多,在文章末尾已經貼出來了(179-242行,代碼中有後續的細節處理,需要甄別下相關的代碼),這裡只是寫下思路,不再重復貼代碼了

處理越界的情況

這裡寫圖片描述
上圖情況是我們不想看到。如果指針偏到最右邊,就不能再往下偏了;左邊同理。這個時候,就需要想到tan函數的性質:

在每一個象限中,正切函數是單調函數

這裡寫圖片描述

說明下:

在第二象限中,當前的tan值小於邊界OA的tan值a時,說明此時是在邊界外面;如果大於a,說明在邊界裡面 在第一象限中,當前的tan值大於OB的tan值b時,說明在邊界外面;如果小於b,說明在邊界裡面
這裡寫圖片描述

知道上述知識後,就非常好處理,具體過程如下:

在繪制弧形時,記錄下左側邊界的tan值和右側邊界的tan值(下面代碼129-151行) 在第一象限和第二象限時,記錄下當前位置的tan值,並與邊界的tan值進行比較來判斷是否在邊界中:(下面代碼214-233行)
在邊界中,不做處理 在邊界外,指針根據情況指向點A或者點B 繪制出相應的指針

再具體的細節我在後面的代碼中由詳細的注釋,各位可以看看

相關代碼

/**
 * Created by Kevin on 2016/8/31.
 *
 * 需要費腦的地方:
 * 1.繪制多條線段組成弧形
 * 2.指針跟隨著手指方向且長度確定
 * 3.指針的指向不能越過儀表盤
 */
public class LinearCircle extends View {

    private int width;
    private int height;

    private Paint outerCirclePaint;//外層圓的畫筆
    private Paint innerCirclePaint;//內層圓的畫筆
    private Paint linePaint;//線段畫筆
    private Paint arrowPaint;//指針畫筆

    private Path outerCirclePath;//外層圓的Path
    private Path innerCirclePath;//內層圓的Path
    private Path linePath;//線段的Path
    private Path arrowPath;//指針的Path
    private Path measureArrowPath;//arrowPath借助該Path來保持一定的長度

    private RectF outRectF;//用於繪制外層圓
    private RectF innerRectF;//用於繪制內層圓

    private int count = 80;//畫count根線
    private static int outerR = 200;//外部圓環的半徑
    private static int innerR = (int) (200 * 0.618f);//內部圓環的半徑
    private int shortageAngle = 140;//缺失的部分的角度
    private int startAngle;//開始的角度
    private int sweepAngle;//掃過的角度

    private float[] leftEndPoint;//左側邊界的坐標
    private float[] rightEndPoint;//右側邊界的坐標
    private float leftEndTan;//左側邊界的tan值
    private float rightEndTan;//右側邊界的tan值

    private float nowX = 0;//觸摸位置的橫坐標
    private float nowY = 0;//觸摸位置的縱坐標
    private static float percent = 0.9f;//指針與內層圓的比值
    private float arrowLength = innerR * percent;//指針的長度

    private PathMeasure arrowMeasure;//用於指針的測量


    public LinearCircle(Context context) {
        super(context);
        initPaint();
        initAngle();
    }

    public LinearCircle(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();
        initAngle();
    }

    public LinearCircle(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
        initAngle();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
        //讓指針一開始指向正上方
        nowX = 0;
        nowY = -1;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(width / 2, height / 2);

        drawOuterCircle();
        drawInnerCircle();
        drawLine(canvas);
        drawArrow(canvas);
    }

    /**
     * 外層圓圈
     */
    private void drawOuterCircle() {
        //一般繪制圓圈的方法,不做介紹了
        outerCirclePath = new Path();
        if (outRectF == null) {
            outRectF = new RectF(-outerR, -outerR, outerR, outerR);
        }
        outerCirclePath.addArc(outRectF, startAngle, sweepAngle);
    }

    /**
     * 內層圓圈
     */
    private void drawInnerCircle() {
        //一般繪制圓圈的方法,不做介紹了
        innerCirclePath = new Path();
        if (innerRectF == null) {
            innerRectF = new RectF(-innerR, -innerR, innerR, innerR);
        }
        innerCirclePath.addArc(innerRectF, startAngle, sweepAngle);
    }

    /**
     * 畫直線,組成一個類似於弧形的形狀
     *
     * @param canvas
     */
    private void drawLine(Canvas canvas) {
        linePath = new Path();
        //用於外層圓的測量
        PathMeasure outMeasure = new PathMeasure(outerCirclePath, false);
        float outlength = outMeasure.getLength();
        float[] outPos = new float[2];

        //用於內層圓的測量
        PathMeasure inMeasure = new PathMeasure(innerCirclePath, false);
        float inlength = inMeasure.getLength();
        float[] inPos = new float[2];

        //確定左側末尾的坐標以及tan值
        if (leftEndPoint == null) {
            leftEndPoint = new float[2];
            //通過getPosTan拿到內層圓的左側末尾坐標
            inMeasure.getPosTan(0, leftEndPoint, null);
            //因為指針要短一點;所以x,y都乘以percent才是指針真正的左側末尾坐標
            leftEndPoint[0] = leftEndPoint[0] * percent;
            leftEndPoint[1] = leftEndPoint[1] * percent;
            //確定指針在左側末尾時的tan值
            leftEndTan = leftEndPoint[1] / leftEndPoint[0];
        }

        //確定右側末尾的坐標以及tan值
        if (rightEndPoint == null) {
            rightEndPoint = new float[2];
            //通過getPosTan拿到內層圓的右側末尾坐標
            inMeasure.getPosTan(inlength, rightEndPoint, null);
            //因為指針要短一點;所以x,y都乘以percent才是指針真正的右側末尾坐標
            rightEndPoint[0] = rightEndPoint[0] * percent;
            rightEndPoint[1] = rightEndPoint[1] * percent;
            //確定指針在右側末尾時的tan值
            rightEndTan = rightEndPoint[1] / rightEndPoint[0];
        }

        //用來畫多條線段,組成弧形
        for (int i = 0; i <= count; i++) {
            //外層圓當前的弧長
            float outNowLength = outlength * i / (count * 1.0f);
            //當前弧長下對應的坐標outPos
            outMeasure.getPosTan(outNowLength, outPos, null);

            //內層圓當前的弧長
            float inNowLength = inlength * i / (count * 1.0f);
            //當前弧長下對應的坐標inPos
            inMeasure.getPosTan(inNowLength, inPos, null);

            //moveTo到內層圓弧上的點
            linePath.moveTo(outPos[0], outPos[1]);
            //lineTo到外層圓弧上的點
            linePath.lineTo(inPos[0], inPos[1]);

            canvas.drawPath(linePath, linePaint);
        }
    }

    /**
     * 繪制指針
     *
     * @param canvas
     */
    private void drawArrow(Canvas canvas) {
        //measureArrowPath只專門用來做計算的,不繪制(當然也可以不用多創建這個對象,直接用arrowPath來完成測量,繪制工作;
        //這裡是為了任務單一,做了區分)
        measureArrowPath = new Path();
        //指針最終是由arrowPath來繪制的
        arrowPath = new Path();
        arrowPath.reset();
        measureArrowPath.reset();

        //用來封裝指針的末尾坐標
        float[] endPoint = new float[2];

        //指針的起始坐標為原點,也就是(0,0)
        measureArrowPath.moveTo(0, 0);
        //指向手指目前的位置
        measureArrowPath.lineTo(nowX, nowY);
        //arrowMeasure用來測量原點到手指位置的線段
        arrowMeasure = new PathMeasure(measureArrowPath, false);
        //觸摸位置與原點的長度
        float nowLineLength = arrowMeasure.getLength();

        //距離原點過近(也就是長度不夠長)的處理
        if (nowLineLength < arrowLength) {
            //計算需要擴大的倍數(固定長度 ÷ 當前長度)
            float expand = arrowLength / (nowLineLength);
            //重置數據,並測量新數據
            measureArrowPath.reset();
            measureArrowPath.moveTo(0, 0);
            measureArrowPath.lineTo(nowX * expand, nowY * expand);
            arrowMeasure = new PathMeasure(measureArrowPath, false);
        }
        //測量指針末尾的坐標(指針在measureArrowPath這條線段上,且小於等於measureArrowPath線段的長度;
        // 通過getPosTan()來確定線段在長度為arrowLength時的坐標位置)
        arrowMeasure.getPosTan(arrowLength, endPoint, null);

        //第一象限的處理
        if (endPoint[0] > 0 && endPoint[1] > 0) {
            //右下角的情況處理
            double nowTan = endPoint[1] / endPoint[0];
            //當前觸摸位置的tan值大於邊界的tan值,表示手指目前在左側邊界的下方
            if (nowTan > rightEndTan) {
                endPoint[0] = rightEndPoint[0];
                endPoint[1] = rightEndPoint[1];
            }
        }
        //第二象限的處理
        if (endPoint[0] < 0 && endPoint[1] > 1) {
            //左下角的情況處理
            double nowTan = endPoint[1] / endPoint[0];
            //當前觸摸位置的tan值小於邊界的tan值,表示手指目前在右側邊界的下方
            if (nowTan < leftEndTan) {
                endPoint[0] = leftEndPoint[0];
                endPoint[1] = leftEndPoint[1];
            }
        }
        //這裡默認了第三、第四現象一般沒有限制;如果圓弧的缺口過大,需要處理下;方式與上面的相似



        //這時,指針的末尾位置最終確定了,可以繪制了
        arrowPath.moveTo(0, 0);
        arrowPath.lineTo(endPoint[0], endPoint[1]);
        canvas.drawPath(arrowPath, arrowPaint);
    }

    //通過觸摸等事件改變指針的指向
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
                nowX = event.getX();
                nowY = event.getY();
                break;
        }
        //nowX和nowY是以左上角為原點的坐標系,這裡進行了平移
        nowX = nowX - width / 2;
        nowY = nowY - height / 2;
        invalidate();
        return true;
    }

    /**
     * 初始化畫筆
     */
    private void initPaint() {
        if (outerCirclePaint == null) {
            outerCirclePaint = new Paint();
            outerCirclePaint.setStyle(Paint.Style.STROKE);
            outerCirclePaint.setColor(Color.BLACK);
        }
        if (innerCirclePaint == null) {
            innerCirclePaint = new Paint();
            innerCirclePaint.setStyle(Paint.Style.STROKE);
            outerCirclePaint.setColor(Color.BLACK);
        }
        if (linePaint == null) {
            linePaint = new Paint();
            linePaint.setStyle(Paint.Style.STROKE);
            linePaint.setStrokeWidth(4);
            linePaint.setColor(0xff1d8ffe);
        }
        if (arrowPaint == null) {
            arrowPaint = new Paint();
            arrowPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            arrowPaint.setColor(Color.RED);
            arrowPaint.setStrokeWidth(4);
        }
    }

    /**
     * 根據shortageAngle來調整圓弧的角度
     */
    private void initAngle() {
        sweepAngle = 360 - shortageAngle;
        startAngle = 90 + shortageAngle / 2;
    }
}

結語

曾經讓人煩透的數學還是有點用處的。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved