1. 程式人生 > >Android自定義View實戰(會波動的View)

Android自定義View實戰(會波動的View)

今天寫這篇部落格的目的,主要是幫助自己總結一下今天學習到的自定義View的相關知識,如果順便能夠幫助大家一點點,那我也感覺很開心。
首先,自定義的一般步驟是:
1.建立自定義View,繼承系統自帶的View,並重寫其相關構造方法;
2.在Values下面新建attrs檔案,寫好相關的屬性名稱和格式;
3.在自定義View中獲取相關的屬性,重寫View的onDraw方法, 來完成檢視的繪製;
4.在XML檔案中使用自定義的View。
按照以上步驟,我來給大家分享一下我今天寫的這個自定義View。先給大家看一下效果圖:

效果圖

主要就是裡面一個實心的圓,外面一個實心的圓,然後還有兩個空心的圓在不斷地改變半徑,產生波動的效果,由於是圖片是靜態的,所以看不出來波動的效果,如果需要了解的,可以下載demo來看一下,demo的下載地址在文章的末尾。
首先,要建立一個自定義View,來繼承系統自帶的View,程式碼如下:

public class WaveView extends View {
    public WaveView(Context context) {
        this(context, null);
    }

    public WaveView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WaveView(Context context, AttributeSet attrs, 
        int defStyleAttr) {
        super
(context, attrs, defStyleAttr); }

然後要在values目錄下新建一個attrs檔案,來把自定義View相關的屬性提取出來。這裡主要提取的屬性有:
1.內圓的顏色;
2.外圓的顏色;
3.內圓的半徑;
4.文字的內容;
5.文字的顏色;
6.文字的大小;
7.波動的頻率。
看attrs檔案的內容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="firstColor" format="color"/>
    <attr
name="secondColor" format="color"/>
<attr name="wavingInterval" format="integer"/> <attr name="circleRadius" format="dimension"/> <attr name="centerText" format="string"/> <attr name="centerTextColor" format="color"/> <attr name="centerTextSize" format="dimension"/> <declare-styleable name="WaveView"> <attr name="firstColor"/> <attr name="secondColor"/> <attr name="circleRadius"/> <attr name="centerText"/> <attr name="centerTextColor"/> <attr name="centerTextSize"/> <attr name="wavingInterval"/> </declare-styleable> </resources>

然後要在自定義View的構造方法當中獲取自定義的屬性,程式碼如下:

public WaveView(Context context, AttributeSet attrs, 
    int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs, 
                R.styleable.WaveView,
                defStyleAttr, 
                0);
        int n = a.getIndexCount();
        for(int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.WaveView_firstColor:
                    firstColor = a.getColor(
                    attr, Color.parseColor("#1992f7"));
                    break;
                case R.styleable.WaveView_secondColor:
                    secondColor = a.getColor(
                    attr, Color.parseColor("#661992f7"));
                    break;
                case R.styleable.WaveView_wavingInterval:
                    wavingInterval = a.getInteger(attr, 50);
                    break;
                case R.styleable.WaveView_circleRadius:
                    circleRadius = a.getDimensionPixelSize(
                        attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 
                            50,
                            getResources().getDisplayMetrics()));
                    break;
                case R.styleable.WaveView_centerText:
                    centerText = (String) a.getText(attr);
                    break;
                case R.styleable.WaveView_centerTextColor:
                    centerTextColor = a.getColor(
                    attr, Color.WHITE);
                    break;
                case R.styleable.WaveView_centerTextSize:
                    centerTextSize = a.getDimensionPixelSize(
                    attr, (int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_SP,16,
                        getResources().getDisplayMetrics()));
                    break;
                default:
                    break;
            }
        }
        a.recycle();
    }

並且要在自定義View的onDraw方法中繪製相關的檢視,程式碼如下:

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

    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setAlpha(255);
    mPaint.setColor(firstColor);
    canvas.drawCircle(
        width / 2, height / 2, 
        circleRadius , mPaint
    );
    mPaint.setColor(secondColor);
    canvas.drawCircle(
        width / 2, height / 2,
        circleRadius + dip2px(15), 
        mPaint
     );
    mPaint.setColor(centerTextColor);
    canvas.drawText(
        centerText, 
        width / 2 - centerTextBound.width() / 2, 
        height / 2 + centerTextBound.height() / 2, 
        mPaint
    );
    mPaint.setColor(secondColor);

    if(isDrawFirst) {
        mPaint.setAlpha(alpha1);
        mPaint.setStrokeWidth(2);
        mPaint.setStyle(Paint.Style.STROKE);
        RectF rectF = new RectF(
            width / 2 - radius1, 
            height / 2 - radius1,
            width / 2 + radius1,
            height / 2 + radius1
         );
        canvas.drawOval(rectF, mPaint);
    }

    if(isDrawSecond) {
        mPaint.setAlpha(alpha2);
        mPaint.setStrokeWidth(2);
        mPaint.setStyle(Paint.Style.STROKE);
        RectF rectF = new RectF(
            width / 2 - radius2, 
            height / 2 - radius2, 
            width / 2 + radius2, 
            height / 2 + radius2
         );
        canvas.drawOval(rectF, mPaint);
        }
    }

完成了以上的相關工作之後,就可以在XML檔案中使用這個自定義的View。使用如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.yulin.customviewset.customview.WaveView
        android:id="@+id/id_wave_view"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_centerInParent="true"
        app:firstColor="#1992f7"
        app:secondColor="#661992f7"
        app:circleRadius="75dp"
        app:wavingInterval="80"
        app:centerText="Waving"
        app:centerTextColor="#ffffff"
        app:centerTextSize="20sp"
        />
</RelativeLayout>

大致介紹以後,我們在來分析一下這個自定義View的一些關鍵點。
首先是自定義屬性的獲取。自定義屬性的獲取的關鍵部分有一下幾個:
1.在attrs檔案中正確宣告相關的屬性名和屬性對應的資料格式;
2.在attrs檔案中還需要宣告declare-styleable,並把已經宣告的相關屬性加入進來;
3.在自定義View的構造方法中以正確的方式獲取相關的屬性。比如上面的自定義View要獲取圓的半徑這個屬性,方式如下:

circleRadius = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics()));

由於半徑這個屬性的格式是dimension,所以用TypedArray的getDimensionPixelSize來獲取,其中的TypedVale可以把dp或者sp轉換成px,50是預設值。
然後是onDraw方法中檢視的繪製,主要包括這麼幾個部分:
1.內部實心圓;
2.外部實心圓;
3.中間的文字;
4.兩個不斷變動的圓環。
對於兩個靜態的圓,繪製方法很簡單,核心程式碼如下:

mPaint.setStyle(Paint.Style.FILL);
mPaint.setAlpha(255);
mPaint.setColor(firstColor);
canvas.drawCircle(
    width / 2, 
    height / 2, 
    circleRadius, 
    mPaint
);
mPaint.setColor(secondColor);
canvas.drawCircle(
    width / 2, 
    height / 2, 
    circleRadius + dip2px(15), 
    mPaint
);

Paint的setStyle是設定畫筆的型別,FILL表示填充,setAlpha表示設定畫筆的透明度,值是0到255,setColor是設定畫筆的顏色,drawCircle表示繪製一個圓,需要指定圓心和半徑以及畫筆。
還需要繪製顯示在內部實心圓中心的文字,繪製一段文字很簡單,關鍵的難點在於如何把這個文字繪製在圓的中心,關鍵步驟如下:

1.要根據文字的長度和文字的大小來得到文字的邊界範圍;

centerTextBound = new Rect();
mPaint.setTextSize(centerTextSize);
mPaint.getTextBounds(
    centerText, 
    0, 
    centerText.length(), 
    centerTextBound
);

2.計算出繪製文字的起始位置,繪製文字。

 mPaint.setColor(centerTextColor);
 canvas.drawText(
     centerText, 
     width / 2 - centerTextBound.width() / 2, 
     height / 2 +  centerTextBound.height() / 2, 
     mPaint
);

剩下的內容就是如何改變兩個圓環的半徑和透明度來讓實現波動的效果了。首先要把兩個圓環繪製出來,如下:

if(isDrawFirst) {
    mPaint.setAlpha(alpha1);
    mPaint.setStrokeWidth(2);
    mPaint.setStyle(Paint.Style.STROKE);
    RectF rectF = new RectF(
        width / 2 - radius1, 
        height / 2 - radius1, 
        width / 2 + radius1, 
        height / 2 + radius1
     );
    canvas.drawOval(rectF, mPaint);
}

if(isDrawSecond) {
    mPaint.setAlpha(alpha2);
    mPaint.setStrokeWidth(2);
    mPaint.setStyle(Paint.Style.STROKE);
    RectF rectF = new RectF(
        width / 2 - radius2, 
        height / 2 - radius2, 
        width / 2 + radius2, 
        height / 2 + radius2
     );
    canvas.drawOval(rectF, mPaint);
 }

在onDraw方法中繪製好著兩個圓環之後,我們就可以通過改變isDrawFirst和isDrawSecond這兩個變數的值來控制繪製哪一個圓環,以及通過改變radius1和radius2的值來控制圓環1和圓環2的半徑,來產生動態波動的效果,因此我們可以開啟一個執行緒來迴圈改變這些引數,以實現不停地波動的效果。這裡我們使用執行緒池來改變這些變數的值,程式碼如下:

 @Override 
 public void startWaving() {
     isWaving = true;
     if(wavingService == null) {
         wavingService =  Executors
          .newSingleThreadScheduledExecutor();
     }
     wavingService.scheduleAtFixedRate(new Runnable() {
         @Override
         public void run() {
             if(isWaving) {
                 isDrawFirst = true;
                 if(radius1 < maxRadius) radius1 += 2;

                 if(radius1 >= maxRadius) {
                     radius1 = circleRadius;
                     alpha1 = 255;
                  }

                 if(radius2 < maxRadius) radius2 += 2;

                 if(radius2 >= maxRadius) {
                     radius2 = circleRadius;
                     alpha2 = 255;
                 }

                 if((radius1 - height / 4) > 
                     (maxRadius - height/4) / 2 &&
                      !isDrawSecond) {
                     isDrawSecond = true;
                     alpha2 = 255;
                     radius2 = circleRadius;
                 }

                 if(alpha1 == 0) alpha1 = 255;
                 if(alpha2 == 0) alpha2 = 255;
                 alpha1 -= 2;
                 alpha2 -= 2;
                 postInvalidate();
                }
            }
        },0, wavingInterval, TimeUnit.MILLISECONDS);
    }

以上程式碼表示,每隔wavingInterval毫秒來進行一次檢視的重繪操作,postInvalidate用於在非UI執行緒中通知View呼叫onDraw方法進行檢視的重繪。
需要說明的是,由於執行緒池顯示的呼叫shutdown或shutdownNow方法,再次進行操作的時候會出現java.util.concurrent.RejectedExecutionException異常,因此這裡通過改變isWaving的值來決定是否需要進行檢視的重繪,以節省資源。
在Activity中的具體使用方法如下:

package com.yulin.customviewset;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

import com.yulin.customviewset.customview.WaveView;

public class MainActivity extends AppCompatActivity {

    private WaveView mWaveView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWaveView = (WaveView) findViewById(R.id.id_wave_view);
    }


    @Override
    protected void onResume() {
        super.onResume();
        mWaveView.startWaving();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mWaveView.pauseWaving();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mWaveView.stopWaving();
    }

    public void startWaving(View v) {
        if(!mWaveView.isWaving()) mWaveView.startWaving();
    }

    public void pauseWaving(View v) {
        if(mWaveView.isWaving()) mWaveView.pauseWaving();
    }
}

這裡為了節省資源,做了以下處理,在onResume中開始波動,在onPause方法中暫停波動,在onDestroy中停止波動,釋放相關的資源。最後附上Demo下載地址: