1. 程式人生 > >閱讀徐宜生《Android群英傳》的筆記——第3章 Android控制元件架構與自定義控制元件詳解(3.6-3.8)

閱讀徐宜生《Android群英傳》的筆記——第3章 Android控制元件架構與自定義控制元件詳解(3.6-3.8)

3.6 自定義 View

在自定義 View 時,我們通常會去重寫 onDraw() 方法來繪製 View 的顯示內容。如果該 View 還需要使用 wrap_content 屬性,那麼還必須重寫 onMeasure() 方法。另外,通過自定義 attrs 屬性,還可以設定新的屬性配置值。

在 View 中通常有以下一些比較重要的回撥方法:

  • onFinishInflate():從 XML 載入元件後回撥。
  • onSizeChanged():元件大小改變時回撥。
  • onMeasure():回撥該方法來進行測量。
  • onLayout():回撥該方法來確定顯示的位置。
  • onTouchEvent():監聽到觸控事件時回撥。

當然,建立自定義 View 的時候,並不需要重寫所有的方法,只需要重寫特定條件的回撥方法即可。這也是 Android 控制元件架構靈活性的體現。
通常情況下,有以下三種方法來實現自定義的控制元件:

  1. 對現有控制元件進行擴充套件;
  2. 通過組合來實現新的控制元件;
  3. 重寫 View 來實現全新的控制元件。

3.6.1 對現有控制元件進行擴充套件

這是一個非常重要的自定義 View 方法,它可以在原生控制元件的基礎上進行擴充套件,增加新的功能、修改顯示的UI等。一般來說,我們可以在 onDraw() 方法中對原生控制元件進行擴充套件。

例子一,以一個 TextView 為例,來看看如何使用擴充套件原生控制元件的方法建立新的控制元件,比如想讓一個 TextView 的背景更加豐富,給其多繪製幾層背景。程式執行如下圖所示:

這裡寫圖片描述

佈局程式碼為:

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

    <com.example.test.CustomTextView
        android:layout_width="80dp"
        android:layout_height
="50dp" android:layout_centerInParent="true" android:gravity="center" android:text="測試資料" />
</RelativeLayout>

自定義 View 的程式碼(CustomTextView)為:

package com.example.test;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.widget.TextView;

/**
 * 自定義 TextView 控制元件
 * Created by HourGlassRemember on 2016/9/6.
 */
public class CustomTextView extends TextView {

    //畫筆
    private Paint mPaint1, mPaint2;

    public CustomTextView(Context context) {
        this(context, null);
    }

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

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

    /**
     * 初始化畫筆
     */
    private void initPaint() {
        //初始化畫筆1
        mPaint1 = new Paint();
        //設定畫筆1的顏色
        mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
        //設定畫筆1的風格,Paint.Style.FILL為實心
        mPaint1.setStyle(Paint.Style.FILL);

        //初始化畫筆2
        mPaint2 = new Paint();
        //設定畫筆2的顏色
        mPaint2.setColor(Color.YELLOW);
        //設定畫筆2的風格,Paint.Style.FILL為實心
        mPaint2.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        /* 在回撥父類方法前,實現自己的邏輯,對 TextView 來說即是在繪製文字內容前 **/
        //繪製外層矩形
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint1);
        //繪製內層矩形
        canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, mPaint2);
        //儲存畫布當前的狀態, canvas.save();和 canvas.restore();是相互匹配出現的,restore可以比save少,但不能多。如果restore呼叫次數比save多,會引發Error。
        //save之後可以呼叫Canvas的平移、放縮、旋轉、錯切、裁剪等操作
        canvas.save();
        //繪製文字前平移10畫素
        //translate(x,y):x代表在x軸上平移,正值代表向右平移,負值代表向左平移。y代表在Y軸上平移,正值代表向下平移,負值代表向上平移
        canvas.translate(10, 0);
        //父類完成的方法,即繪製文字
        super.onDraw(canvas);
        /* 在回撥父類方法後,實現自己的邏輯,對 TextView 來說即是在繪製文字內容後 **/
        //恢復畫布之前儲存過的狀態,防止save後對Canvas執行的操作對後續的繪製有影響。
        canvas.restore();
    }

}

例子二,再來看一個稍微複雜一點的 TextView,比如可以利用 LinearGradient 和 Matrix 來實現一個動態的文字閃動效果,程式執行如下圖所示:(這個栗子還不是很理解啊!)

這裡寫圖片描述

要想實現這一個效果,可以充分利用 Android 中 Paint 物件的 Shader 渲染器。通過設定一個不斷變化的 LinearGradient,並使用帶有該屬性的 Paint 物件來繪製要顯示的文字。

佈局程式碼為:

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

    <com.example.test.CustomTextView
        android:layout_width="200dp"
        android:layout_height="50dp"
        android:layout_centerInParent="true"
        android:gravity="center"
        android:text="測試資料測試資料測試資料" />

</RelativeLayout>

自定義 View 的程式碼(CustomTextView)為:

package com.example.test;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.widget.TextView;

/**
* 自定義 TextView 控制元件

* Created by HourGlassRemember on 2016/9/6.
*/
public class CustomTextView extends TextView {

    //畫筆
    private Paint mPaint;
    //線性漸變
    private LinearGradient mLinearGradient;
    //矩陣
    private Matrix mGradientMatrix;
    private int mViewWidth;
    private int mTranslate;

    public CustomTextView(Context context) {
        this(context, null);
    }

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

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //通過矩陣的方式來不斷平移漸變效果
        if (null != mGradientMatrix) {
            mTranslate += mViewWidth / 5;
            if (mTranslate > 2 * mViewWidth) {
                mTranslate = -mViewWidth;
            }
            mGradientMatrix.setTranslate(mTranslate, 0);
            mLinearGradient.setLocalMatrix(mGradientMatrix);
            //設定失效時間為100毫秒,即0.1秒
            postInvalidateDelayed(100);
        }
    }

    /**
    * 元件大小改變時回撥
    *
    * @param w
    * @param h
    * @param oldw
    * @param oldh
    */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                //獲取當前繪製 TextView 的 Paint 物件
                mPaint = getPaint();
                //設定 LinearGradient 屬性的值
                //設定 LinearGradient 屬性的值
                //引數1:漸變起始點座標X軸位置
                //引數2:漸變起始點座標Y軸位置
                //引數3:漸變終點座標X軸位置
                //引數4:漸變終點座標Y軸位置
                //引數5:參與漸變效果的顏色集合
                //引數6:定義每個顏色處於的漸變相對位置,這個引數可以為null,為null表示所有的顏色按順序均勻的分佈
                //引數7:平鋪方式
                mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{Color.BLUE, 0xffffffff, Color.BLUE}, null, Shader.TileMode.CLAMP);
                //給這個 Paint 物件設定 LinearGradient 屬性
                mPaint.setShader(mLinearGradient);
                mGradientMatrix = new Matrix();
            }
        }
    }

}

Ps:Invalidate 和 postInvalidate 的區別
關於 Invalidate 和 postInvalidate 是做什麼用的,老外是這樣回答的:
Each class which is derived from the View class has the invalidate and the postInvalidate method.
If invalidate gets called it tells the system that the current view has changed and it should be redrawn as soon as possible. As this method can only be called from your UIThread another method is needed for when you are not in the UIThread and still want to notify the system that your View has been changed. The postInvalidate method notifies the system from a non-UIThread and the View gets redrawn in the next eventloop on the UIThread as soon as possible. It is also briefly explained in the SDK documentation.
Just compare invalidate and postInvalidate.
大致的意思是:
只要是 View 的子類,都會從 View 中繼承 invalidate 和 postInvalidate 這兩個方法。
當 invalidate 方法被呼叫的時候,就是在告訴系統當前的 View 發生改變,應該儘可能快的來進行重繪。這個方法僅能在UI執行緒中被呼叫。
如果想要在工作執行緒中進行重新整理介面,那麼其他的方法將會被呼叫,這個方法就是 postInvalidate 方法。這個方法將會發送訊息到主執行緒,當主執行緒的訊息佇列輪詢到當前訊息的時候,這個方法會被呼叫。但是需要注意的是,重新整理介面並不能保證馬上重新整理,只是儘可能快的進行重新整理,尤其在 postInvalidate 方法中,這種情況會出現。
至於可能有人會問 postInvalidate 是怎麼保證執行緒安全的,那麼我們需要看一下 postInvalidate 的原始碼:

public void postInvalidate() {
        postInvalidateDelayed(0);
    }

public void postInvalidateDelayed(long delayMilliseconds) {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

3.6.2 建立複合控制元件

建立複合控制元件可以很好地創建出具有重用功能的控制元件集合,這種方式通常需要繼承一個合適的 ViewGroup,再給它新增指定功能的控制元件,從而組合成新的複合控制元件。通過這種方式建立的控制元件,我們一般會給它指定一些可配置的屬性,讓它具有更強的擴充套件性。
下面就以一個 TopBar為示例,講解如何建立複合控制元件。
我們知道為了應用程式風格的統一,很多應用程式都有一些共通的 UI 介面,如果下圖中所示的 TopBar 這樣的一個標題欄:

這裡寫圖片描述

通常情況下,這些介面都會被抽象出來,形成一個共通的 UI 元件。所有需要新增標題欄的介面都會引用這樣的一個 TopBar,而不是每個介面都在佈局檔案中寫這樣的一個 TopBar。同時,設計者還可以給 TopBar 增加相應的介面,讓呼叫者能夠更加靈活地控制 TopBar,這樣不僅可以提高介面的複用率,更能在需要修改 UI 時,做到快速修改,而不需要對每個頁面的標題欄都進行修改。
下面我們就來看看該如何建立一個這樣的 UI 模板。首先,模板應該具有通用性與可定製性,也就是說,我們需要給呼叫者以豐富的介面,讓他們可以更改模板中的文字、顏色、行為等資訊,而不是所有的模板都一樣,那樣就失去了模板的意義。

1、定義屬性

為一個 View 提供可自定義的屬性非常簡單,只需要在 res 資源目錄的 values 目錄下建立一個 attrs.xml 的屬性定義檔案,並在該檔案中通過如下程式碼定義相應的屬性即可。

<declare-styleable name="MyTopBar">
    <!--標題文字-->
    <attr name="mTitle" format="string" />
    <!-- 標題文字的大小-->
    <attr name="mTitleTextSize" format="dimension" />
    <!--標題文字的顏色-->
    <attr name="mTitleTextColor" format="color" />
    <!--左邊文字的顏色-->
    <attr name="mLeftTextColor" format="color" />
    <!--左邊文字的背景-->
    <attr name="mLeftBackground" format="reference|color" />
    <!--左邊文字-->
    <attr name="mLeftText" format="string" />
    <!--右邊文字的顏色-->
    <attr name="mRightTextColor" format="color" />
    <!--右邊文字的背景-->
    <attr name="mRightBackground" format="reference|color" />
    <!--右邊文字-->
    <attr name="mRightText" format="string" />
</declare-styleable>

我們在程式碼中通過 標籤聲明瞭自定義屬性,通過 標籤來宣告具體的自定義屬性,並通過 name 屬性來確定引用的名稱 ,通過 format 屬性來指定屬性的型別。
確定好屬性後,就可建立一個自定義控制元件——TopBar,並讓它繼承 ViewGroup,從而組合一些需要的控制元件。這裡為了簡單,我們繼承 RelativeLayout。在構造方法中,通過如下的程式碼來獲取在 XML 佈局檔案中自定義的那些屬性,即與我們使用系統提供的那些屬性一樣。

//通過這個方法將你在 attrs.xml 中定義的 declare-styleable 的所有屬性的值儲存到 TypeArray 中
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTopBar);
//從 TypeArray 中取出對應的值來為要設定的屬性賦值
mLeftText = typedArray.getString(R.styleable.MyTopBar_mLeftText);
mLeftTextColor = typedArray.getColor(R.styleable.MyTopBar_mLeftTextColor, 0);
mLeftBackground = typedArray.getDrawable(R.styleable.MyTopBar_mLeftBackground);

mRightText = typedArray.getString(R.styleable.MyTopBar_mRightText);
mRightTextColor = typedArray.getColor(R.styleable.MyTopBar_mRightTextColor, 0);
mRightBackground = typedArray.getDrawable(R.styleable.MyTopBar_mRightBackground);

mTitleText = typedArray.getString(R.styleable.MyTopBar_mTitle);
mTitleColor = typedArray.getColor(R.styleable.MyTopBar_mTitleTextColor, 0);
mTitleSize = typedArray.getDimension(R.styleable.MyTopBar_mTitleTextSize, 10);

2、組合控制元件

接下來我們就可以來組合控制元件了,具體實現程式碼如下所示:

mLeftButton = new Button(context);
mRightButton = new Button(context);
mTextView = new TextView(context);

//為建立的元件元素賦值,值就來源於我們在引用的 xml 檔案中給對應屬性的賦值
mLeftButton.setText(mLeftText);
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackgroundDrawable(mLeftBackground);

mRightButton.setText(mRightText);
mRightButton.setTextColor(mRightTextColor);
mRightButton.setBackgroundDrawable(mRightBackground);

mTextView.setText(mTitleText);
mTextView.setTextSize(mTitleSize);
mTextView.setTextColor(mTitleColor);
mTextView.setGravity(Gravity.CENTER);

//為元件元素設定相應的佈局元素
mLeftParams = new LayoutParams(60, LayoutParams.MATCH_PARENT);
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT | RelativeLayout.CENTER_VERTICAL, TRUE);
//新增到 ViewGroup中
addView(mLeftButton, mLeftParams);

mRightParams = new LayoutParams(60, LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
addView(mRightButton, mRightParams);

mTitleParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(mTextView, mTitleParams);

既然是 UI 模板,那麼每個呼叫者所需要這些按鈕能夠實現的功能都是不一樣的,因此,不能直接在 UI 模板中新增具體的實現邏輯,只能通過介面回撥的思想,將具體的實現邏輯交給呼叫者,實現過程如下所示:
首先是定義介面:

/**
* 介面物件,實現回撥機制,在回撥方法中通過對映的介面物件呼叫介面中的方法,

* 而不用去考慮如何實現,具體的實現由呼叫者去建立

*/
public interface topbarClickListener {
    void leftClick();

    void rightClick();
}

接著是暴露介面給呼叫者:

//介面物件
private topbarClickListener mListener;

/**
* 暴露一個方法給呼叫者來註冊介面回撥,通過介面來獲得回撥者對介面方法的實現

*
* @param mListener
*/
public void setOnTopBarClickListener(topbarClickListener mListener) {
    this.mListener = mListener;
}

//為左右按鈕新增點選事件,但不去實現具體的邏輯,而是呼叫介面中相應的點選方法
mLeftButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        if (null != mListener) {
            mListener.leftClick();
        }
    }
});
mRightButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        if (null != mListener) {
            mListener.rightClick();
        }
    }
});

然後是實現介面回撥:

//自定義標題欄
private TopBar mTopBar;

//設定監聽事件,實現介面回撥
mTopBar.setOnTopBarClickListener(new TopBar.topbarClickListener() {
    @Override
    public void leftClick() {
        Toast.makeText(MainActivity.this, "left", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void rightClick() {
        Toast.makeText(MainActivity.this, "right", Toast.LENGTH_SHORT).show();
    }
});

同樣可以使用公共方法來動態地修改 UI 模板中的 UI,這樣就進一步提高了模板的可定製性,程式碼如下:

/**
* 設定按鈕的顯示與否通過 id 區分按鈕,flag 區分是否顯示

*
* @param id id 為0代表左邊的按鈕,不為0代表右邊的按鈕

* @param flag 是否顯示的標誌

*/
public void setButtonVisible(int id, boolean flag) {
    if (id == 0) {
        mLeftButton.setVisibility(flag ? VISIBLE : GONE);
    } else {
        mRightButton.setVisibility(flag ? VISIBLE : GONE);
    }
}

通過上面的程式碼,呼叫者通過下面程式碼就可以動態地控制按鈕的顯示。

//控制 TopBar 上元件的狀態
mTopBar.setButtonVisible(0, true);
mTopBar.setButtonVisible(1, false);

3、引用 UI 模板

最後一步,自然是在需要使用的地方引用 UI 模板,我們可以將這個 UI 模板寫到一個佈局檔案當中,程式碼如下:

<com.example.test.MyTopBar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:id="@+id/topBar"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:background="#FFFFFF"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    custom:mLeftBackground="@drawable/back"
    custom:mLeftTextColor="#FFFFFF"
    custom:mRightBackground="@drawable/more"
    custom:mRightTextColor="#FFFFFF"
    custom:mTitle="自定義標題"
    custom:mTitleTextColor="#123412"
    custom:mTitleTextSize="5sp">

</com.example.test.MyTopBar>

通過上面的程式碼,我們就可以在其他的佈局檔案中,直接通過標籤來引用這個 UI 模板 View了,即:

<include layout="@layout/custom_top_bar" />

3.6.3 重寫 View 來實現全新的控制元件

當 Android 系統原生的控制元件無法滿足我們的需求時,我們就可以完全建立一個新的自定義 View 來實現需要的功能。建立一個自定義 View,難點在於繪製控制元件和實現互動,這也是評價一個自定義 View 優劣的標準之一。通常需要繼承 View 類,並重寫它的 onDraw()、onMeasure() 等方法來實現繪製邏輯,同時通過重寫 onTouchEvent() 等觸控事件來實現互動邏輯。當然,我們還可以像實現組合控制元件方式那樣,通過引入自定義屬性,豐富自定義 View 的可定製性。
下面通過幾個例項,讓大家瞭解下如何建立一個自定義 View,不過為了讓程式儘可能簡單,這裡就不去自定義屬性值了。

例項一:弧線展示圖

這裡寫圖片描述

這個比例圖非常清楚地展示一個專案所佔的比例,簡潔明瞭。因此,實現這樣一個自定義 View 用在我們的程式中,可以讓整個程式實現比較清晰的資料展示效果。那麼該如何建立一個這樣的自定義 View 呢?很明顯,這個自定義 View 其實分為三個部分,分別是中間的圓形、中間顯示的文字和外圈的弧線。具體實現的程式碼如下所示:

package com.example.test;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

/**
* 自定義弧線展示圖

* Created by HourGlassRemember on 2016/9/7.
*/
public class ArcDiagram extends View {

    //螢幕寬度和高度
    private int measureWidth, measureHeight;

    //繪製圓的畫筆
    private Paint mCirclePaint;
    //圓心x軸和y軸的座標
    private float mCircleXY;
    //圓的半徑
    private float mRadius;

    //繪製圓弧的畫筆
    private Paint mArcPaint;
    //圓弧外輪廓矩形區域
    private RectF mArcRectF;
    //圓弧掃過的角度
    private float mSweepAngle;
    //圓弧掃過的預設值,這裡預設值設定為25
    private float mSweepValue = 25;

    //繪製中間文字的畫筆
    private Paint mTextPaint;
    //中間的文字
    private String mShowText;
    //中間文字的文字大小
    private float mShowTextSize;

    public ArcDiagram(Context context) {
        this(context, null);
    }

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureWidth = getMeasuredWidth();
        measureHeight = getMeasuredHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //繪製圓——引數:圓心的x軸座標;圓心的y軸座標;半徑;繪製圓的畫筆。
        canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);

        //繪製弧線——引數:指定圓弧的外輪廓矩形區域;圓弧起始角度,但是為度;圓弧掃過的角度,順時針方向,單位為度;
        //如果為true時,在繪製圓弧時將圓心包括在內,通常用來繪製扇形;繪製圓弧的畫筆。
        canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);

        //繪製文字
        canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint);
    }

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

    private void initView() {
        //這裡為了簡單,我們把 View 的繪製長度直接設定為螢幕的寬度
        float length = measureHeight >= measureWidth ? measureWidth : measureHeight;

//        繪製圓的引數
        //計算出圓心的x軸座標和y軸座標
        mCircleXY = length / 2;
        //計算出半徑大小
        mRadius = length / 4.0f;
        mCirclePaint = new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setColor(getResources().getColor(android.R.color.holo_blue_bright));

//        繪製弧線,需要指定其橢圓的外接矩形
        //獲得圓弧外輪廓矩形區域
        mArcRectF = new RectF(length * 0.1f, length * 0.1f, length * 0.9f, length * 0.9f);
        //設定圓弧掃過的預設角度
        mSweepAngle = (mSweepValue / 100f) * 360f;
        mArcPaint = new Paint();
        mArcPaint.setAntiAlias(true);
        mArcPaint.setStyle(Paint.Style.STROKE);
        mArcPaint.setStrokeWidth(length * 0.1f);
        mArcPaint.setColor(getResources().getColor(android.R.color.holo_blue_bright));

//        繪製文字
        //中間顯示文字的內容
        mShowText = "Android Skill";
        //中間顯示文字的大小
        mShowTextSize = 50;
        mTextPaint = new Paint();
        mTextPaint.setTextSize(mShowTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    /**
    * 設定掃描過的數值

    *
    * @param sweepValue
    */
    public void setSweepValues(float sweepValue) {
        mSweepValue = sweepValue != 0 ? sweepValue : 25;
        this.invalidate();
    }

}

佈局檔案程式碼:

<com.example.test.ArcDiagram
    android:id="@+id/arc_diagram"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:layout_marginTop="250dp" />

MainActivity的程式碼:

ArcDiagram arcDiagram = (ArcDiagram) findViewById(R.id.arc_diagram);
arcDiagram.setSweepValues(70);

例項二:音訊條形圖

靜態音訊條形圖
圖:靜態音訊條形圖
這個功能是實現類似PC上某些音樂播放器上根據音訊音量大小顯示的音訊條形圖。由於只是演示自定義 View 的用法,我們就不去真實地監聽音訊輸入了,隨機模擬一些數字即可。具體實現的程式碼如下所示:

package com.example.test;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;

/**
* 自定義音訊條形圖

* Created by HourGlassRemember on 2016/9/7.
*/
public class AudioBarChart extends View {

    //畫筆

    private Paint mPaint;
    //音訊條形數

    private int mRectCount;
    //每個小條形的寬度

    private int mRectWidth;
    private int mWidth, mHeight;
    private int offset = 5;
    //線性漸變

    private LinearGradient mLinearGradient;

    public AudioBarChart(Context context) {
        this(context, null);
    }

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

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

    private void initView() {
        mPaint = new Paint();
        mPaint.setColor(Color.BLUE);
        mPaint.setStyle(Paint.Style.FILL);
        mRectCount = 12;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getWidth();
        mHeight = getHeight();
        mRectWidth = (int) (mWidth * 0.6 / mRectCount);
        //給音訊條形圖增加漸變效果

        mLinearGradient = new LinearGradient(0, 0, mWidth, mHeight, Color.RED, Color.GRAY, Shader.TileMode.CLAMP);
        mPaint.setShader(mLinearGradient);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //下面是一種計算座標的方法

        for (int i = 0; i < mRectCount; i++) {
            //讓每個小矩形的高度隨機變化

            float currentHeight = (float) (Math.random() * mHeight);
            canvas.drawRect(mWidth * 0.4f / 2 + mRectWidth * i + offset, currentHeight, mWidth * 0.4f / 2 + mRectWidth * (i + 1), mHeight, mPaint);
        }
        //設定每300毫秒重新整理一次 View
        postInvalidateDelayed(500);
    }

}

3.7 自定義 ViewGroup

ViewGroup 存在的目的是為了對其子 View 進行管理,為其子 View 新增顯示、響應的規則。因此,自定義 ViewGroup 通常需要重寫 onMeasure() 方法來對子 View 進行測量,重寫 onLayout() 方法來確定子 View 的位置,重寫 onTouch() 方法增加響應事件。
下面通過一個例項,來看看如何自定義一個 ViewGroup。本例準備實現一個類似 Android 原生控制元件 ScrollView 的自定義 ViewGroup,自定義 ViewGroup 可以實現ScrollView 所具有的上下滑動功能,但是在滑動的過程中,增加一個黏性的效果,即當一個子 View 向上滑動大於一定的距離後,鬆開手指,它將自動向上滑動,顯示下一個子 View,同理,如果滑動距離小於一定的距離,鬆開手指,它將自動滑動到開始的位置。這個功能就很像手機螢幕向上滑動解鎖的功能。下面是程式碼部分:

自定義 ViewGroup 類:

package com.hourglassremember;

import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Scroller;

/**
* Created by HourGlassRemember on 2016/9/14.
*/
public class MyScrollView extends ViewGroup {

    //ViewGroup 的左上點與螢幕左上點的差值,正值代表 ViewGroup 的左上點位於螢幕左上點的上面,反之為下面
    private int mStart;
    //手指最後一次那個座標點在螢幕的值,數值越大代表離螢幕左上點越遠,反之越近
    private int mLastY;
    //View 滑動的輔助類,可以實現一種慣性的滾動過程和回彈效果,要注意的是,Scroller 本身不會去移動 View,它只是一個移動計算輔助類,
    //用於跟蹤控制元件滑動的軌跡,只相當於一個滾動軌跡記錄工具,最終還是通過 View 的 scrollTo、scrollBy 方法完成 View 的移動
    private Scroller mScroller;
    //螢幕高度
    private int mScreenHeight;

    public MyScrollView(Context context) {
        this(context, null);
    }

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

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(getContext());
    }

    /**
    * 初始化
    *
    * @param context
    */
    private void init(Context context) {
        //初始化螢幕高度
        //獲得視窗管理器
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        //新建一個 DisplayMetrics
        DisplayMetrics dm = new DisplayMetrics();
        //從視窗管理器中獲得資料放到 DisplayMetrics 中
        wm.getDefaultDisplay().getMetrics(dm);
        //獲得實際顯示的畫素值並賦值給 mScreenHeight
        mScreenHeight = dm.heightPixels;
        //建立一個滾動類 Scroller
        mScroller = new Scroller(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //使用遍歷的方式來測量每一個子 View 的大小
        for (int i = 0; i < getChildCount(); i++) {
            measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //獲得子 View 的個數
        int childCount = getChildCount();
        //MarginLayoutParams 主要用於定於和邊緣的空白
        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
        //設定 ViewGroup 的高度,即子 View 的高度乘以子 View 的個數,因為每個子 View 都是充滿螢幕的
        mlp.height = mScreenHeight * childCount;
        setLayoutParams(mlp);
        //使用遍歷的方式來設定子View 需要放置的位置
        for (int i = 0; i < childCount; i++) {
            //獲得所有的子 View
            View childView = getChildAt(i);
            //如果 View 的顯示狀態為 gone 時,不計算它的位置
            if (childView.getVisibility() != View.GONE) {
                //主要修改每個子 View 的 top 和 bottom 屬性,讓他們能依次排列下來
                childView.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //手指觸碰時在 y 軸方向的位置
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN://手指按下時
                //手指按下時,最新位置是當前位置y
                mLastY = y;
                //getScrollX():代表 ViewGroup 在水平方向上的滾動距離,getScrollX()>0表示向左滾動,getScrollX()<0表示向右滾動
                //getScrollY():代表 ViewGroup 在垂直方向上的滾動距離,getScrollY()>0表示向上滾動,getScrollY()<0表示向下滾動
                //記錄手指按下時 ViewGroup 當前的滾動距離
                mStart = getScrollY();
                break;
            case MotionEvent.ACTION_MOVE://手指滑動時
                //首先判斷 mScroller 是否已經完成動作了,沒有完成動作的話,立即停止動畫
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                //計算本次滑動了多大的距離,即當前手指的座標減去上一次停止時的座標
                int dy = mLastY - y;
                //如果 ViewGroup 當前的滾動距離小於0 ,就代表 ViewGroup 的左上點位於螢幕左上點的下方,此時的狀態應該是顯示第一頁,
                // 並向下拉,這時候需要復位,因為第一頁下滑後還是第一頁,故設定本次滑動的距離為0
                if (getScrollY() < 0) {
                    dy = 0;
                }
                //如果 ViewGroup 當前的滾動距離大於 ViewGroup 能夠滾動的最大距離,就代表 ViewGroup 的左上點位於螢幕左上點的上方,
                //此時的狀態應該是顯示最後一頁,並向上拉,這時候需要復位,因為最後一頁上