今天要來講述個我曾經因為這項功能而懷疑人生的項目(´д`)

Canvas,直譯為畫布

在Android上就是提供一系列的函式來讓你可以自由地在畫面上畫出任何你想畫的東西

並且搭配介面刷新的方法(postInvalidate)方法就可以完成各種動態行為

也就是說~善用這個Canvas的話,理論上你能夠做出各種超帥的功能Σ(・口・)

包含如果你家的UI設計師又畫出那些超級漂亮你會很想掐死他的元件的話

某些程度上你也能夠(不)輕鬆地駕馭它(`・ω・´)+

那今天要來聊的是"在Android上寫出一個弧形儀表板"

今天的程式不需要載入第三方資源,也不需要權限;直接用程式碼去完成

先上功能吧,功能有兩個部分

一個是使用seekBar去控制表版的進度,另一個是自己設置上限及下限,然後自己輸入數字顯示

首先是SeekBar控制

儀表板範例_seekBar

然後是自定義上下限後顯示比例

儀表板範例_輸入

GitHub:

https://github.com/thumbb13555/DashboardExample


0.準備工作

在寫這種功能之前,通常我都會建議先拿起紙筆畫出今天想完成的圖案,再整理思緒

同時也針對今天要完成的東西做一個簡單地說明

20200228_204122

針對繪圖方法,以下幾個解釋

  1. 畫出進度條的方法是畫一個空心扇形後再加粗邊框寬度
  2. 畫進度條時,會有一個底圖跟進度圖,兩者重疊用不一樣的顏色就能呈現我們要的效果
  3. 控制進度條的參數為角度,所以進度條必須要經過百分比計算後刻度才會準

此外我們要創造的View,因為是能夠被xml畫面偵測並使用,因此長寬的限制上需特別注意

一般來說如果我們是要畫這種圓形的元件,就必須決定他的原點,才能設置一個圓

而如果想要製造一個弧形的軌跡運動,就必須去了解相關計算公式來實踐

我這邊的話,如果只畫弧形進度條是不需要,但是指針就需要了

基礎公式則是:cos(弧度)*圓半徑 或者 sin(弧度)*圓半徑

如此這般地去求援上每一個點座標

這個部分的話,會在下面畫指標時會用到

好拉總之廢話就到這,開始來畫圖吧(´д`)

1.設置控制進度的UI

先來處理畫面問題,如果要做得跟我的一樣

畫面中要有seekBar用來控制表版、三個EditText與一個Button以及一個View

首先我們要先來創造一個View

請新增一個類別檔(我的Git應該會有DashBoardView跟DashBoard2,前者是我之前試做的,不用理他)

截圖 2020-02-29 上午12.13.12

再com.的資料夾上點擊右鍵->New->Java class

截圖 2020-02-28 下午9.44.01

然後讓他繼承View,並新增建構子(可四個都加入)

截圖 2020-02-28 下午9.45.35

 

截圖 2020-02-28 下午9.46.19

回到介面設計

這時候在介面設計的xml內就可以找到你剛剛所寫的類別View囉(´・з・)

截圖 2020-02-28 下午9.47.53

整體的畫面XML在這

 

activity_main.xml

https://github.com/thumbb13555/DashboardExample/blob/master/app/src/main/res/layout/activity_main.xml

樹狀圖

截圖 2020-02-28 下午9.39.59

 

最後是連結元件功能,我們來到MainActivity.java

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SeekBar mSeekBar = (SeekBar) findViewById(R.id.seekBar);
        EditText mEdit = findViewById(R.id.editText);
        Button btText = findViewById(R.id.button);
        DashBoard2 mBoard = (DashBoard2) findViewById(R.id.myDashBoard); 
        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                 //稍後寫控制View的地方
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });//onSeekbar
        btText.setOnClickListener((view -> {
            EditText edMax,edMin;
            edMax = findViewById(R.id.edMax);
            edMin = findViewById(R.id.edMin);
            String s = mEdit.getText().toString();
            String mS= edMax.getText().toString();
            String nS= edMin.getText().toString();

            if (s.length()!=0 &&mS.length()!=0&&nS.length()!=0){
                 //稍後寫控制View的地方
            }
        }));

    }
}

這時候可以先執行看看,基本的控制項就會出來囉(・ωー)~☆

截圖 2020-02-28 下午9.58.10

2.畫進度條與指針

2-1.取得畫布寬度及高度

再來我們要來畫半圓型進度條了(`・ω・´)+

首先我設幾個全域變數以及新增兩個複寫

請按下⌘+o(windows:ctrl+o)新增

分別是onMeasure以及onDraw

 

onMeasure是用來取得View的高度寬度用的

於是我在onMeasure內新增以下紅字

以及這是目前程式碼樣貌

講個笑話...最後這個參數壓根沒用到XD

因為取得高度寬度的話,在onDraw中用getWeightgetHeight就好了...

public class DashBoard2 extends View {
    private int totleHeight, totleWeight;//總高度以及總寬度
    private float percentage;//進度條趴數
    private String originValue;//輸入值
    public DashBoard2(Context context) {
        super(context);
    }

    public DashBoard2(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

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

    public DashBoard2(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        totleHeight = MeasureSpec.getSize(heightMeasureSpec);
        totleWeight = MeasureSpec.getSize(widthMeasureSpec);

        setMeasuredDimension(totleWeight, totleHeight);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
    
}

2-2.繪製進度條的底圖(灰色部分)

在繪製底圖之前,先新增一個把dp轉換為px(像素)的副程式

程式在這邊直接複製貼到與onDraw同一層就好(´・з・)

private float dp2px(float dp) {
        DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
        return dp * metrics.density;
    }

接著我們先來繪製半圓型的底圖(也就是進度條的灰色部分)

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setAntiAlias(true);//消除鋸齒
        int bottomColor = Color.parseColor("#C9C9C9");//設置顏色
        paint.setColor(bottomColor);//設置底部顏色
        paint.setStrokeJoin(Paint.Join.ROUND);//設置畫筆畫出的形狀
        paint.setStrokeCap(Paint.Cap.ROUND);//使線的尾端具有圓角
        paint.setStyle(Paint.Style.STROKE);//使圓為空心
        paint.setStrokeWidth(dp2px(40));//設置外圍線的粗度
        RectF mRecF = new RectF(10 + dp2px(20), 10 + dp2px(20)
                , getWidth() - 10 - dp2px(20), getHeight() - 10);//畫一個扇形
        canvas.drawArc(mRecF, 180, 180, false, paint);//將扇形畫至畫布上
}

這時候畫面上就有一個漂亮的半圓囉~(•ө•)♡

截圖 2020-02-28 下午11.46.28

2-3.繪製進度條(藍色部分)

至於進度條部分,簡單來說就是"在底圖上再畫一個一樣的東西,只是不同顏色"

於是只要接著剛剛的程式往下加入第二張圖就好

我寫在藍色部分

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setAntiAlias(true);//消除鋸齒
        int bottomColor = Color.parseColor("#C9C9C9");//設置顏色
        paint.setColor(bottomColor);//設置底部顏色
        paint.setStrokeJoin(Paint.Join.ROUND);//設置畫筆畫出的形狀
        paint.setStrokeCap(Paint.Cap.ROUND);//使線的尾端具有圓角
        paint.setStyle(Paint.Style.STROKE);//使圓為空心
        paint.setStrokeWidth(dp2px(40));//設置外圍線的粗度
        RectF mRecF = new RectF(10 + dp2px(20), 10 + dp2px(20)
                , getWidth() - 10 - dp2px(20), getHeight() - 10);//畫一個扇形
        canvas.drawArc(mRecF, 180, 180, false, paint);//將扇形畫至畫布上

        paint.setColor(Color.parseColor("#1C98EB"));//把顏色換成藍色
        canvas.drawArc(mRecF, 180, 90, false, paint);//畫進度
    }

這時候畫面會呈現這樣

截圖 2020-02-28 下午11.59.46

那要如何改變進度呢?請回頭看剛剛的程式,90的位置有底線加粗Σ(・口・)

這個部分就是關鍵;可以去試著手動調整,進度條就會在不一樣的位置(・ωー)~☆

了解關鍵點後,我們把它改成先前宣告過的變數percentage

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setAntiAlias(true);//消除鋸齒
        int bottomColor = Color.parseColor("#C9C9C9");//設置顏色
        paint.setColor(bottomColor);//設置底部顏色
        paint.setStrokeJoin(Paint.Join.ROUND);//設置畫筆畫出的形狀
        paint.setStrokeCap(Paint.Cap.ROUND);//使線的尾端具有圓角
        paint.setStyle(Paint.Style.STROKE);//使圓為空心
        paint.setStrokeWidth(dp2px(40));//設置外圍線的粗度
        RectF mRecF = new RectF(10 + dp2px(20), 10 + dp2px(20)
                , getWidth() - 10 - dp2px(20), getHeight() - 10);//畫一個扇形
        canvas.drawArc(mRecF, 180, 180, false, paint);//將扇形畫至畫布上

        paint.setColor(Color.parseColor("#1C98EB"));//把顏色換成藍色
        canvas.drawArc(mRecF, 180, percentage, false, paint);//畫進度
    }

2-4.先設置一個接口

由於需要測試之緣故,我先做一個讓他用seekBar去控制的接口

首先先增一個副程式,名字隨意;位置與onDraw同一層

public void setPercentage(float value){
        percentage=value;
        postInvalidate();//刷新Canvas畫面
    }

再來到MainActivity.java中取用接口

 

MainActivity.java

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SeekBar mSeekBar = (SeekBar) findViewById(R.id.seekBar);
        EditText mEdit = findViewById(R.id.editText);
        Button btText = findViewById(R.id.button);
        DashBoard2 mBoard = (DashBoard2) findViewById(R.id.myDashBoard);
        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                mBoard.setPercentage(i);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });//onSeekbar
        btText.setOnClickListener((view -> {
            EditText edMax,edMin;
            edMax = findViewById(R.id.edMax);
            edMin = findViewById(R.id.edMin);
            String s = mEdit.getText().toString();
            String mS= edMax.getText().toString();
            String nS= edMin.getText().toString();

            if (s.length()!=0 &&mS.length()!=0&&nS.length()!=0){
                 //稍後設置
            }
        }));

這時候按下執行~(*゚ー゚)ゞ

Gif_20200229001910644_by_gifguru

嗯嗯~進度有出來了,但是刻度好像差強人意...(´д`)

那關於這個問題,我先挖個坑,後面補上 (´・д・`)

2-5.畫指針

這篇文章的第一句話不曉得看到這邊的你記不記得?

"今天要來講述個我曾經因為這項功能而懷疑人生的項目(´д`)"

主要就是這個指針啦( ´_ゝ`)

因本人是學店生,可想而知我的基礎數學大概就是個笑話吧

((沒錯就是個笑話,而且我的人生也是(凸)

不過後來我找了好多資料後,還是成功完成了啦...

我先直接PO程式碼,再來分析吧

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setAntiAlias(true);//消除鋸齒
        int bottomColor = Color.parseColor("#C9C9C9");//設置顏色
        paint.setColor(bottomColor);//設置底部顏色
        paint.setStrokeJoin(Paint.Join.ROUND);//設置畫筆畫出的形狀
        paint.setStrokeCap(Paint.Cap.ROUND);//使線的尾端具有圓角
        paint.setStyle(Paint.Style.STROKE);//使圓為空心
        paint.setStrokeWidth(dp2px(40));//設置外圍線的粗度
        RectF mRecF = new RectF(10 + dp2px(20), 10 + dp2px(20)
                , getWidth() - 10 - dp2px(20), getHeight() - 10);//畫一個扇形
        canvas.drawArc(mRecF, 180, 180, false, paint);//將扇形畫至畫布上

        paint.setColor(Color.parseColor("#1C98EB"));//把顏色換成藍色
        canvas.drawArc(mRecF, 180, percentage, false, paint);//畫進度

        Paint point = new Paint();
        point.setAntiAlias(true);
        point.setColor(Color.parseColor("#096148"));
        double x, y, Rx1, Ry1, Rx2, Ry2;
        float ovalbottom = getHeight() + 4 * 16;//指針下移點數(可以改為*100看看效果,你就會懂了)
        int xPos = (getWidth() / 2);//畫布X軸中心點
        int yPos = (getHeight() / 2);//畫布Y軸中心點
        float px = 50;//指針寬度
        x = xPos + Math.cos(
                Math.toRadians((percentage - 180))) * (yPos * 2 / 3);//針頭x座標,指針長度=(yPos * 2 / 3)
        y = ovalbottom - yPos + Math.sin(
                Math.toRadians((percentage - 180))) * (yPos * 2 / 3);//針頭y座標,指針長度=(yPos * 2 / 3)
        Rx1 = xPos + Math.cos(Math.toRadians(((percentage - 180) + 90))) * px;//指針底部第一點X座標
        Ry1 = ovalbottom - yPos + Math.sin(Math.toRadians(((percentage - 180) + 90))) * px;//指針底部第一點Y座標
        Rx2 = xPos + Math.cos(Math.toRadians(((percentage - 180) - 90))) * px;//指針底部第二點X座標
        Ry2 = ovalbottom - yPos + Math.sin(Math.toRadians(((percentage - 180) - 90))) * px;//指針底部第二點X座標
        Path path = new Path();//畫一個三角形
        path.moveTo((int) x, (int) y);
        path.lineTo((int) Rx1, (int) Ry1);
        path.lineTo((int) Rx2, (int) Ry2);
        path.close();
        canvas.drawPath(path, point);//畫進去畫布內
    }

我上面也有講,算圓的點座標的基本算式是長這樣:

cos(弧度)*圓半徑 或者 sin(弧度)*圓半徑

而使用sine跟使用cosine的差異性在於一個算正弦一個算餘弦((廢話(╯=▃=)╯︵┻━┻

而toRadians()這個函式本身就是在幫把角度算為弧度的一個方法

其公式在這->角度/180*π

參考->http://tw.gitbook.net/java/lang/math_toradians.html

參考2->https://www.learnmode.net/upload/flip/book/b6/b6e666231461f809/07248f93878b.pdf

吐槽:****高中數學3....應該是高二上吧(゚A゚;)

好啦有興趣研究的再去問你們數學老師,如果我有講錯的部分再多多指教

其他的我註解也有詳盡的介紹,我就不再多說

這時候執行起來應該會長這樣

截圖 2020-02-29 下午2.19.11

 

恩..怪怪的,畫一個圓遮一下好了

請在畫三角形的程式上面加入這個

canvas.drawCircle(getWidth() / 2, getHeight() / 2 + 64+0.8f, px - 0.5f, point);//畫一個圓遮醜

這樣就有模有樣囉(・∀・)

截圖 2020-02-29 下午2.25.33

3.設置接口與進度條修正

3-1.設置接口

接口的設置,其實剛剛上面也講過了

但是因應要使用上下限值,我設置了兩個接口

截圖 2020-02-29 下午2.33.35

3-2.設置百分比

百分比計算的話~哎呀這不是國小數學嗎XD

瞬間就可愛多了(•ө•)♡

非常EZ,直接這樣搞定

public void setPercentage(float value){
        if (value >= 0) {
            value = value * 180 / 100;
            percentage = value;
            postInvalidate();
        }
    }

這時候你就可以看到你的進度能確實執行囉!

截圖 2020-02-29 下午2.41.34

3-3.設置動畫

也許你現在會覺得你的程式有點生硬

我指針轉到哪就在哪(´・з・)

那現在來加個動畫吧~

動畫的相關方法是這個ValueAnimator

->https://developer.android.com/reference/android/animation/ValueAnimator

我們要用的方法是這個

->https://www.programcreek.com/java-api-examples/?class=android.animation.ValueAnimator&method=addUpdateListener

那我的程式修改為這樣

public void setPercentage(float value){
        if (value >= 0) {
            DecimalFormat df = new DecimalFormat("#####0.#");
            originValue = df.format(value);
            value = value * 180 / 100;
            ValueAnimator valueAnimator = ValueAnimator.ofFloat(percentage, value);
            percentage = value;
            valueAnimator.addUpdateListener((vA -> {
                percentage = (float) vA.getAnimatedValue();
                postInvalidate();
            }));
            valueAnimator.setDuration(500);//設置動畫時間
            valueAnimator.start();

        }
    }

執行~

(痞客邦無法執行高畫質GIF,本功能若不使用高畫質GIF則無法展現,請見諒無圖(゚д゚;))

3-4.顯示輸入值

輸入值的部分,首先在onDraw內加入以下就好

paint.reset();//重置畫筆
paint.setColor(Color.BLACK);//設置為黑色
paint.setTextSize(80);//設置字體大小
if (originValue == null){//若還尚未輸入,則顯示0
       originValue = "0";
   }
canvas.drawText("值:"+originValue,getHeight()/2-50,getWidth()/2+200,paint);//畫上去

並且在setPercentage的部分的藍色字,就是他的接口

截圖 2020-02-29 下午3.08.40

3-4.收尾

基本上所有程式都寫好了,唯獨剩下上限下限的那部分沒寫

其實不難,程式大概也是一看就懂,我直接PO吧

 

DashBoard2.java

public void setPercentage(float value, float max, float min) {
        if (max > min) {
            DecimalFormat df = new DecimalFormat("#####0.#");
            float f1;
            if (value <= min) {
                f1 = (min - min) / (max - min);
                originValue = df.format(min);
            } else if (value >= max) {
                f1 = (max - min) / (max - min);
                originValue = df.format(max);
            } else {
                f1 = (value - min) / (max - min);
                originValue = df.format(value);
            }
            value = f1 * 180;

            ValueAnimator vA = ValueAnimator.ofFloat(percentage, value);
            percentage = value;
            vA.addUpdateListener((v -> {
                percentage = (float) vA.getAnimatedValue();
                postInvalidate();
            }));
            vA.setDuration(500);
            vA.start();
        }
    }

 

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SeekBar mSeekBar = (SeekBar) findViewById(R.id.seekBar);
        EditText mEdit = findViewById(R.id.editText);
        Button btText = findViewById(R.id.button);
        DashBoard2 mBoard = (DashBoard2) findViewById(R.id.myDashBoard);
        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                mBoard.setPercentage(i);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });//onSeekbar
        btText.setOnClickListener((view -> {
            EditText edMax,edMin;
            edMax = findViewById(R.id.edMax);
            edMin = findViewById(R.id.edMin);
            String s = mEdit.getText().toString();
            String mS= edMax.getText().toString();
            String nS= edMin.getText().toString();

            if (s.length()!=0 &&mS.length()!=0&&nS.length()!=0){

                mBoard.setPercentage(Float.parseFloat(s)
                        ,Float.parseFloat(mS)
                        ,Float.parseFloat(nS));

            }
        }));
    }
}

 

執行~~(`・ω・´)+

截圖 2020-02-29 下午3.17.59

 


心得

當時接到這個功能時,也是覺得"好難啊~~~~~"(´・_・`)

但是其實只要克服了好像就沒這麼誇張

但是一想到這種東西還有多采多姿地變化型就覺得頭痛...

當時在畫指針的時候,一度痛恨過去的自己為什麼沒把數學學好...

但是有時候想一想,哎..就算讓我重回過去去學,我大概還是學不會吧(´υ`)

因為即使擺到現在我一樣看不懂...((雖然有懂一點點了

我覺得這就是智商的問題吧,我就爛啦怎樣(・ω・)b

79956749_10157901015184641_555730345324969984_o

呱吉版的比較好笑XDDD

但是呱吉一點都不爛,至少人家考得上建中台大還當哭已丸

我學店生QQ

好啦今天的文就到這,希望可以普渡芸芸眾生(?)

喜歡我的網誌請加入書籤~一希望我的網誌對你有幫助

下載

 

arrow
arrow

    碼農日常 發表在 痞客邦 留言(0) 人氣()