Android仿華為天氣繪制刻度盤
效果圖
可以看到這個自定義控件結(jié)合了顏色漸變、動態(tài)繪制刻度、動態(tài)水球效果。接下來我們就來看看這個效果是如何一步一步實現(xiàn)的。
開始自定義控件
和很多自定義控件方式一樣需要去基礎(chǔ)某種View或者某種ViewGroup
我這里選擇的是View,如下所示:
- public class HuaWeiView extends View {
- /**
- * 用來初始化畫筆等
- * @param context
- * @param attrs
- */
- public HuaWeiView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
- /**
- * 用來測量限制view為正方形
- * @param widthMeasureSpec
- * @param heightMeasureSpec
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- /**
- * 實現(xiàn)各種繪制功能
- * @param canvas
- */
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- }
- }
其中構(gòu)造方法用來布局中使用。
onMeasure()方法用來測量和限定view大小
onDraw()方法用來進(jìn)行具體的繪制功能
1.使用onMeasure()方法將View限制為一個正方形
只有確定了一個矩形才能夠去畫橢圓,如果這個矩形是正方形,橢圓也就隨之變成了圓形。
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int width=MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- //以最小值為正方形的長
- len=Math.min(width,height);
- //設(shè)置測量高度和寬度(必須要調(diào)用,不然無效果)
- setMeasuredDimension(len,len);
- }
分別通過MeasureSpec取得用戶設(shè)置的寬和高,然后取出最小值,設(shè)置給我們的view,這樣我們就做好了一個矩形
現(xiàn)在使用在布局中:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@color/colorPrimary"
- android:padding="20dp"
- tools:context="com.example.huaweiview.MainActivity">
- <com.example.huaweiview.HuaWeiView
- android:layout_gravity="center"
- android:background="@color/colorAccent"
- android:layout_width="200dp"
- android:layout_height="300dp"
- />
- </LinearLayout>
父布局背景為藍(lán)色背景,控件背景為粉色背景,而且設(shè)置的寬高不同,但是控件的顯示效果還是一個正方形,而且以小值為準(zhǔn)。我們的onMeasure()生效了
接下來就是如何在確定一個圓形區(qū)域了
2.onDraw()繪制圓形區(qū)域
繪制之前我們需要對Android中的坐標(biāo)系有個了解
我們都知道手機(jī)屏幕左上角為坐標(biāo)原點,往右為X正軸,往下為Y正軸。其實手機(jī)頁面就是activity的展示界面,也是一個View。那可不可以說所有的View在繪制圖形的時候都有自己的這么一個坐標(biāo)系呢(個人想法。。。)
也就是所每個View都有自己的一個坐標(biāo)系,比如現(xiàn)在的自定義View:
現(xiàn)在我們需要在我們自定義的view中繪制一個圓弧,那么這個圓弧的半徑就是我們自定義view的長度的一半,即:
radius=len/2;
那么圓心的坐標(biāo)剛好是(radius,radius)
接下來開始繪制
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //畫圓弧的方法
- canvas.drawArc(oval, startAngle, sweepAngle, useCenter,paint);
- }
介紹一下繪制圓弧的方法:
- 參數(shù)一oval是一個RectF對象為一個矩形
- 參數(shù)二startAngle為圓弧的起始角度
- 參數(shù)三sweepAngle為圓弧的經(jīng)過角度(掃過角度)
- 參數(shù)四useCenter為圓弧是一個boolean值,為true時畫的是圓弧,為false時畫的是割弧
- 參數(shù)五paint為一個畫筆對象
也就是說只要確定了一個矩形,在確定他起始和經(jīng)過的角度就能夠畫出一個圓弧(這點大家可以用畫板測試)
下來就是初始化這些參數(shù)
初始化矩形
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- //以最小值為正方形的長
- len = Math.min(width, height);
- //實例化矩形
- oval=new RectF(0,0,len,len);
- //設(shè)置測量高度和寬度(必須要調(diào)用,不然無效果)
- setMeasuredDimension(len, len);
- }
畫矩形需要確定左上角和右下角的坐標(biāo)(通過畫板可以測試),通過上面的分析坐標(biāo)原點就是我們view的左上角,右下角的坐標(biāo)當(dāng)然就是len了。
接下來就是初始化起始和經(jīng)過角度
- private float startAngle=120;
- private float sweepAngle=300;
需要搞清楚往下為Y軸正軸,剛好和上學(xué)時候?qū)W的相反,也就是說90度在下方,-90度在上方
初始化畫筆
- public HuaWeiView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- paint =new Paint();
- //設(shè)置畫筆顏色
- paint.setColor(Color.WHITE);
- //設(shè)置畫筆抗鋸齒
- paint.setAntiAlias(true);
- //讓畫出的圖形是空心的(不填充)
- paint.setStyle(Paint.Style.STROKE);
- }useCenter=false
到這里真不容易呀,然而發(fā)現(xiàn)只畫個圓弧沒用呀,我要的是刻度線呀,canvas里面又沒用給我們提供畫刻度線的方法,這個時候就需要我們自己去寫一個畫刻度線的方法了。
通過觀察圖片我們可以看出,所有的線都是從圓弧上的點為起點向某個方向畫一條直線,那么該如何確定這兩個點呢,需要我們做兩件事:
移動坐標(biāo)系
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //畫圓弧的方法
- canvas.drawArc(oval, startAngle, sweepAngle, useCenter,paint);
- //畫刻度線的方法
- drawViewLine(canvas);
- }
- private void drawViewLine(Canvas canvas) {
- //先保存之前canvas的內(nèi)容
- canvas.save();
- //移動canvas(X軸移動距離,Y軸移動距離)
- canvas.translate(radius,radius);
- //操作完成后恢復(fù)狀態(tài)
- canvas.restore();
- }
我們自己寫了一個繪制刻度線的方法并在onDraw()方法中調(diào)用。移動坐標(biāo)系之前需要保存之前的canvas狀態(tài),然后X和Y軸分別移動圓弧半徑的距離,如下圖:
canvas.translate(radius,radius);方法移動的是坐標(biāo)系(通過實際效果和查資料所得)
canvas.save()和canvas.restore()要成對出現(xiàn),就好像流用完要關(guān)閉一樣。
***件事情完成后,開始第二件事情,旋轉(zhuǎn)坐標(biāo)系
只通過移動坐標(biāo)系,仍然很難確定圓弧點上的坐標(biāo),和另外一點的坐標(biāo),如果這兩個點都在坐標(biāo)軸上該多好呀,下面實現(xiàn):
- private void drawViewLine(Canvas canvas) {
- //先保存之前canvas的內(nèi)容
- canvas.save();
- //移動canvas(X軸移動距離,Y軸移動距離)
- canvas.translate(radius,radius);
- //旋轉(zhuǎn)坐標(biāo)系
- canvas.rotate(30);
- //操作完成后恢復(fù)狀態(tài)
- canvas.restore();
畫刻度線的方法了增加了一個旋轉(zhuǎn)30度的代碼,旋轉(zhuǎn)后的坐標(biāo)系應(yīng)該怎么樣呢;
因為起始點和90度相差30,旋轉(zhuǎn)之后,起始點剛好落在了Y軸上,那么這個點的坐標(biāo)就很好確定了吧,沒錯就是(0,radius);如果我們在Y軸上在找一點不就可以畫出一條刻度線了嗎,那么它的坐標(biāo)是多少呢?對,應(yīng)該是(0,radius-y),因為我們要往內(nèi)部化刻度線,因此是減去一個值,趕快去試試吧,代碼如下:
- private void drawViewLine(Canvas canvas) {
- //先保存之前canvas的內(nèi)容
- canvas.save();
- //移動canvas(X軸移動距離,Y軸移動距離)
- canvas.translate(radius,radius);
- //旋轉(zhuǎn)坐標(biāo)系
- canvas.rotate(30);
- Paint linePatin=new Paint();
- //設(shè)置畫筆顏色
- linePatin.setColor(Color.WHITE);
- //線寬
- linePatin.setStrokeWidth(2);
- //設(shè)置畫筆抗鋸齒
- linePatin.setAntiAlias(true);
- //畫一條刻度線
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- //操作完成后恢復(fù)狀態(tài)
- canvas.restore();
- }
根據(jù)得到的兩個點的坐標(biāo),畫出來一條白線,如圖:
當(dāng)然這些點都是移動后的坐標(biāo)系在旋轉(zhuǎn)30度得到的,這里畫好了一條線,如果畫多條呢,還是剛才的思路每次都讓它旋轉(zhuǎn)一個小角度然后畫條直線不就好了嗎,那么旋轉(zhuǎn)多少度呢,比如這里:總共掃過的角度sweepAngle=300;需要100條刻度,那么每次需要旋轉(zhuǎn)的角度rotateAngle=sweepAngle/100,具體代碼如下:
- private void drawViewLine(Canvas canvas) {
- //先保存之前canvas的內(nèi)容
- canvas.save();
- //移動canvas(X軸移動距離,Y軸移動距離)
- canvas.translate(radius,radius);
- //旋轉(zhuǎn)坐標(biāo)系
- canvas.rotate(30);
- Paint linePatin=new Paint();
- //設(shè)置畫筆顏色
- linePatin.setColor(Color.WHITE);
- //線寬
- linePatin.setStrokeWidth(2);
- //設(shè)置畫筆抗鋸齒
- linePatin.setAntiAlias(true);
- //確定每次旋轉(zhuǎn)的角度
- float rotateAngle=sweepAngle/99;
- for(int i=0;i<100;i++){
- //畫一條刻度線
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- canvas.rotate(rotateAngle);
- }
- //操作完成后恢復(fù)狀態(tài)
- canvas.restore();
- }
100個刻度,需要101次循環(huán)畫線(請看你的手表),畫完線就旋轉(zhuǎn)。依次循環(huán),如圖
經(jīng)過這么久的時間總于完成了刻度盤了,接下來就是去確定不同角度顯示什么樣的顏色,***我們需要確定要繪制的范圍targetAngle:
繪制有色部分
- private void drawViewLine(Canvas canvas) {
- //先保存之前canvas的內(nèi)容
- canvas.save();
- //移動canvas(X軸移動距離,Y軸移動距離)
- canvas.translate(radius,radius);
- //旋轉(zhuǎn)坐標(biāo)系
- canvas.rotate(30);
- Paint linePatin=new Paint();
- //設(shè)置畫筆顏色
- linePatin.setColor(Color.WHITE);
- //線寬
- linePatin.setStrokeWidth(2);
- //設(shè)置畫筆抗鋸齒
- linePatin.setAntiAlias(true);
- //確定每次旋轉(zhuǎn)的角度
- float rotateAngle=sweepAngle/100;
- //繪制有色部分的畫筆
- Paint targetLinePatin=new Paint();
- targetLinePatin.setColor(Color.GREEN);
- targetLinePatin.setStrokeWidth(2);
- targetLinePatin.setAntiAlias(true);
- //記錄已經(jīng)繪制過的有色部分范圍
- float hasDraw=0;
- for(int i=0;i<=100;i++){
- if(hasDraw<=targetAngle&&targetAngle!=0){//需要繪制有色部分的時候
- //畫一條刻度線
- canvas.drawLine(0,radius,0,radius-40,targetLinePatin);
- }else {//不需要繪制有色部分
- //畫一條刻度線
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- }
- //累計繪制過的部分
- hasDraw+=rotateAngle;
- //旋轉(zhuǎn)
- canvas.rotate(rotateAngle);
- }
- //操作完成后恢復(fù)狀態(tài)
- canvas.restore();
- }
我們需要不斷的去記錄繪制過的有效部分,之外的部分畫白色。
根據(jù)角度的比例,顏色漸變
需要計算出已經(jīng)繪制過的角度占總角度(300)的比例
- for(int i=0;i<=100;i++){
- if(hasDraw<=targetAngle&&targetAngle!=0){//需要繪制有色部分的時候
- //計算已經(jīng)繪制的比例
- float percent=hasDraw/sweepAngle;
- int red= 255-(int) (255*percent);
- int green= (int) (255*percent);
- targetLinePatin.setARGB(255,red,green,0);
- //畫一條刻度線
- canvas.drawLine(0,radius,0,radius-40,targetLinePatin);
- }else {//不需要繪制有色部分
- //畫一條刻度線
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- }
- hasDraw+=rotateAngle;
- canvas.rotate(rotateAngle);
- }
只是在繪制有色部分的時候,利用三元素來實現(xiàn)漸變。所占比例越低紅色值越大,反正綠色值越大。
實現(xiàn)動態(tài)顯示
先想一下它的運動情況,分為前進(jìn)狀態(tài)和后退狀態(tài),如果正在運動(一次完整的后退和前進(jìn)沒用結(jié)束),就不能開始下次運動,需要兩個參數(shù),state和isRunning
- //判斷是否在動
- private boolean isRunning;
- //判斷是回退的狀態(tài)還是前進(jìn)狀態(tài)
- private int state = 1;
- public void changeAngle(final float trueAngle) {
- if (isRunning){//如果在動直接返回
- return;
- }
- final Timer timer = new Timer();
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- switch (state) {
- case 1://后退狀態(tài)
- isRunning=true;
- targetAngle -= 3;
- if (targetAngle <= 0) {//如果回退到0
- targetAngle = 0;
- //改為前進(jìn)狀態(tài)
- state = 2;
- }
- break;
- case 2://前進(jìn)狀態(tài)
- targetAngle += 3;
- if (targetAngle >= trueAngle) {//如果增加到指定角度
- targetAngle = trueAngle;
- //改為后退狀態(tài)
- state = 1;
- isRunning=false;
- //結(jié)束本次運動
- timer.cancel();
- }
- break;
- default:
- break;
- }
- //重新繪制(子線程中使用的方法)
- postInvalidate();
- }
- }, 500, 30);
- }
利用時間任務(wù),每個30毫秒去執(zhí)行一次run方法,每次都重新繪制圖片,然后在activity中調(diào)用此方法
- HuaWeiView hwv;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- hwv= (HuaWeiView) findViewById(R.id.hwv);
- hwv.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- //點擊事件中,調(diào)用動的方法
- hwv.changeAngle(200);
- }
- });
- }
看到這里了,相信你對坐標(biāo)系和角度動態(tài)變化,以及刻度盤的繪制有了個很好的認(rèn)識,多多驗證會有助于理解。
接下來要實現(xiàn)背景動態(tài)漸變
想想咱們的view中哪里用了漸變呢?對,在繪制有色部分的時候,如果我們能將顏色漸變的值不斷的傳到activity中該多好呀,下面就要用接口傳值實現(xiàn)這一功能了:
- ***在自定義View中聲明一個內(nèi)部接口:
- private OnAngleColorListener onAngleColorListener;
- public void setOnAngleColorListener(OnAngleColorListener onAngleColorListener) {
- this.onAngleColorListener = onAngleColorListener;
- }
- public interface OnAngleColorListener{
- void colorListener(int red,int green);
- }
我們在自定義View中聲明一個內(nèi)部接口,并聲明一個全局接口對象,提供一個set方法
接口內(nèi)有個方法用來獲取顏色值
接下來就是在合適的地方調(diào)用這個方法,那么哪里呢,就是我們繪制顏色刻度時調(diào)用:
- for (int i = 0; i <= 100; i++) {
- if (hasDraw <= targetAngle && targetAngle != 0) {//需要繪制有色部分的時候
- //計算已經(jīng)繪制的比例
- float percent = hasDraw / sweepAngle;
- int red = 255 - (int) (255 * percent);
- int green = (int) (255 * percent);
- //實現(xiàn)接口回調(diào),傳遞顏色值
- if (onAngleColorListener!=null){
- onAngleColorListener.colorListener(red,green);
- }
- targetLinePatin.setARGB(255, red, green, 0);
- //畫一條刻度線
- canvas.drawLine(0, radius, 0, radius - 40, targetLinePatin);
- } else {//不需要繪制有色部分
- //畫一條刻度線
- canvas.drawLine(0, radius, 0, radius - 40, linePatin);
- }
我們在繪制的時候?qū)崿F(xiàn)了接口回調(diào),接下來去activity中實現(xiàn)接口
- public class MainActivity extends AppCompatActivity {
- HuaWeiView hwv;
- LinearLayout ll_parent;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- hwv= (HuaWeiView) findViewById(R.id.hwv);
- //實例父布局
- ll_parent= (LinearLayout) findViewById(R.id.ll_parent);
- hwv.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- //點擊事件中,調(diào)用動的方法
- hwv.changeAngle(200);
- }
- });
- //設(shè)置角度顏色變化監(jiān)聽
- hwv.setOnAngleColorListener(new HuaWeiView.OnAngleColorListener() {
- @Override
- public void colorListener(int red, int green) {
- Color color=new Color();
- //通過Color對象將RGB值轉(zhuǎn)為int類型
- int backColor=color.argb(100,red,green,0);
- //父布局設(shè)置背景
- ll_parent.setBackgroundColor(backColor);
- }
- });
- }
- }
給父布局一個id,然后實例化。給我們的自定義控件設(shè)置一個角度顏色變化監(jiān)聽,從而拿到回調(diào)中傳過來的值,然后借助Color對象將RGB值轉(zhuǎn)為int值,再設(shè)置給父布局背景,這里背景稍稍透明一些。效果圖:

到了這里是不是感覺炫酷了不少呢,其實功能已經(jīng)實現(xiàn)的差不多了,接下來就是去繪制里面的內(nèi)容吧
繪制文字
當(dāng)然不去繪制文字也是可以的,你可以直接在布局中添加textview等。好話不多說,先分析一下繪制的過程吧,在刻度盤的內(nèi)部有一個小圓,然后這些文字就在小圓內(nèi),繪制小圓只需要讓它的半徑小點就OK了。
- /**
- * 繪制小圓和文本的方法,小圓顏色同樣漸變
- * @param canvas
- */
- private void drawScoreText(Canvas canvas) {
- //先繪制一個小圓
- Paint smallPaint = new Paint();
- smallPaint.setARGB(100,red,green,0);
- // 畫小圓指定圓心坐標(biāo),半徑,畫筆即可
- int smallRadius=radius-60;
- canvas.drawCircle(radius, radius, radius - 60, smallPaint);
- //繪制文本
- Paint textPaint=new Paint();
- //設(shè)置文本居中對齊
- textPaint.setTextAlign(Paint.Align.CENTER);
- textPaint.setColor(Color.WHITE);
- textPaint.setTextSize(smallRadius/2);
- //score需要通過計算得到
- canvas.drawText(""+score,radius,radius,textPaint);
- //繪制分,在分?jǐn)?shù)的右上方
- textPaint.setTextSize(smallRadius/6);
- canvas.drawText("分",radius+smallRadius/2,radius-smallRadius/4,textPaint);
- //繪制點擊優(yōu)化在分?jǐn)?shù)的下方
- textPaint.setTextSize(smallRadius/6);
- canvas.drawText("點擊優(yōu)化",radius,radius+smallRadius/2,textPaint);
- }
這里將之前漸變的red和green提為全局變量,先繪制一個小圓,畫筆顏色漸變。然后繪制文字分?jǐn)?shù)score需要通過計算的到
- //計算得到的分?jǐn)?shù)
- score=(int)(targetAngle/sweepAngle*100);
- //重新繪制(子線程中使用的方法)
- postInvalidate();
在時間任務(wù)中,每次繪制之前計算得到分?jǐn)?shù),然后在右上方畫一個固定值分,再在下方一個固定內(nèi)容點擊優(yōu)化(這個時候的坐標(biāo)已經(jīng)回到最初的模樣)
到此為止功能已經(jīng)寫的差不多了,還有一個水波加速球效果,下篇博客中寫吧。