#### **五、自定義View**
在自定義View 時,我們通常會去重寫onDraw()方法來繪制View的顯示內容。如果該View還需要使用wrap_content屬性,那么還必須重寫onMeasure()方法。另外,通過自定義attrs屬性,還可以設置新的屬性配置值。
自定義View時有一些比較重要的回調方法如下:
~~~
onFinishInflate();//從xml加載組件后回調
onSizeChanged();//組件大小改變時回調
onMeasure();//回調該方法進行測量
onLayout();//回調該方法來確定顯示的位置
onTouchEvent();//監聽到觸摸事件回調
~~~
>[info] 注意:當然,創建自定義View的時候,并不需要重寫所有的方氈,只需要重寫特定條件的回調方法即可。這也是Android控件架構靈活性的體現。
**自定義View通常有三種情況**:
**1、對現有控件進行拓展**:
這是一個非常重要的自定義View方法,它可以在原生控件的基礎上進行拓展,增加新的功能、修改顯示的UI等。一般來說,會在**onDraw**()方法中對原生控件行為進行拓展
~~~
@Override
protected void onDraw(Canvas canvas) {
//在回調父類方法前,實現自己的邏輯,對TextView來說即是在繪制文本內容前
super.onDraw(canvas);
//在回調父類方法后,實現自己的邏輯,對TextView來說即是在繪制文本內容后
}
~~~
**程序調用super.onDraw(canvas)方法來實現原生控件的功能,但是在調用super.onDraw(canvas)方法之前和之后,我們都可以實現自己的邏輯,分別在系統繪制文字前后,完成自己的操作。**
**示例1:自定義修改TextView**
:-: 
圖7 自定義修改TextView
代碼如下所示
**[MyTextView](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架構/SystemWidget/app/src/main/java/com/imooc/systemwidget/MyTextView.java)**
~~~
public class MyTextView extends TextView {
private Paint mPaint1, mPaint2;
public MyTextView(Context context) {
super(context);
initView();
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MyTextView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mPaint1 = new Paint();
mPaint1.setColor(getResources().getColor(
android.R.color.holo_blue_light));
mPaint1.setStyle(Paint.Style.FILL);
mPaint2 = new Paint();
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制外層矩形
canvas.drawRect(
0,
0,
getMeasuredWidth(),
getMeasuredHeight(),
mPaint1);
// 繪制內層矩形
canvas.drawRect(
10,
10,
getMeasuredWidth() - 10,
getMeasuredHeight() - 10,
mPaint2);
canvas.save();
// 繪制文字前平移10像素
canvas.translate(10, 0);
// 父類完成的方法,即繪制文本
super.onDraw(canvas);
canvas.restore();
}
}
~~~
而代碼中最重要的部分則是.在onDraw()方法中,為了改變原生的繪制行為,在系統調用super.onDraw(canvas)方法前,也就是在繪制文字之下,繪制兩個不同大小的矩形,形成一個重疊效果,再讓系統調用super.onDraw(canvas)方法,執行繪制文字的工作。這樣,我們就通過改變控件繪制行為,創建了一個新的控件。
**示例2:閃動的文字效果**
:-: 
圖8:閃動的文字
要想實現這樣一個效果,可以充分利用Android中Paint 對象的Shader渲染器。通過設置一個不斷變化的LinearGradient(Shader的子類),并使用帶有該屬性的Paint對象來繪制耍顯示的文字。
代碼如下所示
**[ShineTextView](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架構/SystemWidget/app/src/main/java/com/imooc/systemwidget/ShineTextView.java)**
~~~
public class ShineTextView extends TextView {
private LinearGradient mLinearGradient;
private Matrix mGradientMatrix;
private Paint mPaint;
private int mViewWidth = 0;
private int mTranslate = 0;
public ShineTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@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) {
mPaint = getPaint();//TextView自身的方法,返回TextPaint,text文本的畫筆
mLinearGradient = new LinearGradient(
0,
0,
mViewWidth,
0,
new int[]{
Color.BLUE, 0xffffffff,
Color.BLUE},
null,
Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
mGradientMatrix = new Matrix();
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mGradientMatrix != null) {
mTranslate += mViewWidth / 5;
if (mTranslate > 2 * mViewWidth) {
mTranslate = -mViewWidth;
}
mGradientMatrix.setTranslate(mTranslate, 0);
mLinearGradient.setLocalMatrix(mGradientMatrix);
postInvalidateDelayed(100);
}
}
}
~~~
首先,在onSizeChanged()方法中進行一些對象的初始化工作,并根據View的寬帶設置一個LinearGradient漸變渲染器,其中最關鍵的就是使用getPaint()方法獲取當前繪制TextView的Paint對象,并給這個Paint對象設置原生TextView沒有的LinearGradient屬性。最后,在onDraw()方法中,通過矩陣的方式來不斷平移漸變效果,從而在繪制文字時,產生動態的閃動效果。
**2、通過組合來實現新的控件:**
這種方式通常需要繼承一個合適的ViewGroup,再給它添加指定功能的控件,從而組合成新的復合控件。通過這種方式創建的控件,我們一般會給它指定一些可配置的屬性,讓它具有更強的拓展性。下面就以一個TopBar為示例,講解如何創建復合控件。
效果如圖所示
:-: 
圖9 Topbar
代碼如下所示
**[TopBar](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架構/SystemWidget/app/src/main/java/com/imooc/systemwidget/TopBar.java)**
~~~
public class TopBar extends RelativeLayout {
// 包含topbar上的元素:左按鈕、右按鈕、標題
private Button mLeftButton, mRightButton;
private TextView mTitleView;
// 布局屬性,用來控制組件元素在ViewGroup中的位置
private LayoutParams mLeftParams, mTitlepParams, mRightParams;
// 左按鈕的屬性值,即我們在atts.xml文件中定義的屬性
private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
// 右按鈕的屬性值,即我們在atts.xml文件中定義的屬性
private int mRightTextColor;
private Drawable mRightBackground;
private String mRightText;
// 標題的屬性值,即我們在atts.xml文件中定義的屬性
private float mTitleTextSize;
private int mTitleTextColor;
private String mTitle;
// 映射傳入的接口對象
private topbarClickListener mListener;
public TopBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public TopBar(Context context) {
super(context);
}
public TopBar(Context context, AttributeSet attrs) {
super(context, attrs);
// 設置topbar的背景
setBackgroundColor(0xFFF59563);
// 通過這個方法,將你在atts.xml中定義的declare-styleable
// 的所有屬性的值存儲到TypedArray中
TypedArray ta = context.obtainStyledAttributes(attrs,
R.styleable.TopBar);
// 從TypedArray中取出對應的值來為要設置的屬性賦值
mLeftTextColor = ta.getColor(
R.styleable.TopBar_leftTextColor, 0);
mLeftBackground = ta.getDrawable(
R.styleable.TopBar_leftBackground);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
mRightTextColor = ta.getColor(
R.styleable.TopBar_rightTextColor, 0);
mRightBackground = ta.getDrawable(
R.styleable.TopBar_rightBackground);
mRightText = ta.getString(R.styleable.TopBar_rightText);
mTitleTextSize = ta.getDimension(
R.styleable.TopBar_titleTextSize, 10);
mTitleTextColor = ta.getColor(
R.styleable.TopBar_titleTextColor, 0);
mTitle = ta.getString(R.styleable.TopBar_title);
// 獲取完TypedArray的值后,一般要調用
// recyle方法來避免重新創建的時候的錯誤
ta.recycle();
mLeftButton = new Button(context);
mRightButton = new Button(context);
mTitleView = new TextView(context);
// 為創建的組件元素賦值
// 值就來源于我們在引用的xml文件中給對應屬性的賦值
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackground(mLeftBackground);
mLeftButton.setText(mLeftText);
mRightButton.setTextColor(mRightTextColor);
mRightButton.setBackground(mRightBackground);
mRightButton.setText(mRightText);
mTitleView.setText(mTitle);
mTitleView.setTextColor(mTitleTextColor);
mTitleView.setTextSize(mTitleTextSize);
mTitleView.setGravity(Gravity.CENTER);
// 為組件元素設置相應的布局元素
mLeftParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
// 添加到ViewGroup
addView(mLeftButton, mLeftParams);
mRightParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
addView(mRightButton, mRightParams);
mTitlepParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(mTitleView, mTitlepParams);
// 按鈕的點擊事件,不需要具體的實現,
// 只需調用接口的方法,回調的時候,會有具體的實現
mRightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.rightClick();
}
});
mLeftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.leftClick();
}
});
}
// 暴露一個方法給調用者來注冊接口回調
// 通過接口來獲得回調者對接口方法的實現
public void setOnTopbarClickListener(topbarClickListener mListener) {
this.mListener = mListener;
}
/**
* 設置按鈕的顯示與否 通過id區分按鈕,flag區分是否顯示
*
* @param id id
* @param flag 是否顯示
*/
public void setButtonVisable(int id, boolean flag) {
if (flag) {
if (id == 0) {
mLeftButton.setVisibility(View.VISIBLE);
} else {
mRightButton.setVisibility(View.VISIBLE);
}
} else {
if (id == 0) {
mLeftButton.setVisibility(View.GONE);
} else {
mRightButton.setVisibility(View.GONE);
}
}
}
// 接口對象,實現回調機制,在回調方法中
// 通過映射的接口對象調用接口中的方法
// 而不用去考慮如何實現,具體的實現由調用者去創建
public interface topbarClickListener {
// 左按鈕點擊事件
void leftClick();
// 右按鈕點擊事件
void rightClick();
}
}
~~~
通常情況下,需要添加標題欄的界面都會抽象出來一個這樣的TopBar,同時給TopBar增加相應的接口,可以更加靈活地控制TopBar,所以需要創建這樣一個UI模板,首先,摸板應該具有通用性與可定制性。也就是說,我們需要給調用者以豐富的接口,讓他們可以更改模板中的文字、顏色、行為等信息,而不是所有的模板都一樣,那樣就失去模扳的意義。
**(1)、定義屬性**
為一個View提供可自定義的屬性非常簡單,只需要在res資源目錄的values日錄下創建一個attrs.xml的屬性定義文件,并在該文件中通過如下代碼定義相應的屬性即可。
~~~
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TopBar">
<attr name="title" format="string" />
<attr name="titleTextSize" format="dimension" />
<attr name="titleTextColor" format="color" />
<attr name="leftTextColor" format="color" />
<attr name="leftBackground" format="reference|color" />
<attr name="leftText" format="string" />
<attr name="rightTextColor" format="color" />
<attr name="rightBackground" format="reference|color" />
<attr name="rightText" format="string" />
</declare-styleable>
</resources>
~~~
通過<declare-styleable>標簽聲明了使用自定義屬性,并通過name屬性來確定引用的名稱,最后通過`<attr>`來聲明具體的自定義屬性。比如此處定義了標題文字的字體、大小、顏色,左邊按鈕的文字顏色、背景、字體,右邊按鈕的文字顏色、背景、字體等屬性,并通過format屬性來指定屬性的類型。要注意的是,有些屬性可以是顏色屬性,也可以是引用屬性。比如按鈕的背景,可以把它指定為具體的顏色,也可以把它指定為一張圖片,所以使用“|”來分隔不同的屬——“reference|color”。
在構造方位中,通過如下代碼來獲取在XML布局文件中自定義的那些屬性,即與我們使用系統提供的那些屬性一樣。
~~~
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
~~~
TypedArray數據結構相當于Map,是鍵值對的映射,通過TypedArray的getColor()、getString()等方法來獲取這些定義的屬性值。最后記得調用ta.recycle();方法來進行資源的回收,否則會對下次的使用造成影響。
**(2)、組合控件**
UI模板TopBar實際上由三個控件組成,即左邊的點擊按鈕mLeftButton,右邊的點擊按鈕mRightButton和中間的標題欄mTitleView。通過動態添加控件的方式,使用addView()方法將這3個控件加入到定義的TopBar模板中,并給它們設置前面所獲取到的具體的屬性值,比如標題的文字顏色、大小等。如上面的構造方法中的代碼所述。
那么如何來給這兩個左、右按鈕設計點擊事件呢?既然是UI模板,那么每個調用者所需要這些按鈕能夠實現的功能都是不一樣的。因此,不能直接在UI模板中添加具體的實現邏輯,只能通過接口回調的思想,將具體的實現邏輯交給調用者,實現過程如下所示。
**①、定義接口**
在UI模版類中定義一個左右按鈕點擊的接口,并創建兩個方法,分別用于左邊按鈕的點擊和右邊按鈕的點擊,代碼如下所示。
~~~
// 接口對象,實現回調機制,在回調方法中
// 通過映射的接口對象調用接口中的方法
// 而不用去考慮如何實現,具體的實現由調用者去創建
public interface topbarClickListener {
// 左按鈕點擊事件
void leftClick();
// 右按鈕點擊事件
void rightClick();
}
~~~
**②、暴露接口給調用者**
在模板方法中,為左、右按鈕增加點擊事件,但不去實現具體的邏輯,而是調用接口中相應的點擊方法,代碼如下所示。
~~~
// 按鈕的點擊事件,不需要具體的實現,
// 只需調用接口的方法,回調的時候,會有具體的實現
mRightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.rightClick();
}
});
mLeftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.leftClick();
}
});
}
// 暴露一個方法給調用者來注冊接口回調
// 通過接口來獲得回調者對接口方法的實現
public void setOnTopbarClickListener(topbarClickListener mListener) {
this.mListener = mListener;
}
~~~
**③、實現接口回惆**
在調用者的代碼巾,調用者需要實現這樣一個接口, 并完成接口中的方法,確定具體的實現邏輯,井使用第二步中暴露的方法,將接口的對象傳遞進去,從而完成回調。通常悄況下,可以使用匿名內部類的形式來實現接口中的方法,代碼如下所示。
**[TopBarTest](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架構/SystemWidget/app/src/main/java/com/imooc/systemwidget/TopBarTest.java)**
~~~
public class TopBarTest extends Activity {
private TopBar mTopbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.topbar_test);
// 獲得我們創建的topbar
mTopbar = (TopBar) findViewById(R.id.topBar);
// 為topbar注冊監聽事件,傳入定義的接口
// 并以匿名類的方式實現接口內的方法
mTopbar.setOnTopbarClickListener(
new TopBar.topbarClickListener() {
@Override
public void rightClick() {
Toast.makeText(TopBarTest.this,
"right", Toast.LENGTH_SHORT)
.show();
}
@Override
public void leftClick() {
Toast.makeText(TopBarTest.this,
"left", Toast.LENGTH_SHORT)
.show();
}
});
// 控制topbar上組件的狀態
mTopbar.setButtonVisable(0, true);
mTopbar.setButtonVisable(1, false);
}
}
~~~
除了通過接口回調的方式來實現動態的控制UI模板,同樣可以使用公共方法來動態地修改UI模板中的UI,這樣就進一步提高模板的可定制性,代碼如下所示。
~~~
/**
* 設置按鈕的顯示與否 通過id區分按鈕,flag區分是否顯示
*
* @param id id
* @param flag 是否顯示
*/
public void setButtonVisable(int id, boolean flag) {
if (flag) {
if (id == 0) {
mLeftButton.setVisibility(View.VISIBLE);
} else {
mRightButton.setVisibility(View.VISIBLE);
}
} else {
if (id == 0) {
mLeftButton.setVisibility(View.GONE);
} else {
mRightButton.setVisibility(View.GONE);
}
}
}
~~~
通過如上所示代間,當調用者通過TopBar 對象調用這個方法后,根據參數,調用者就可以動態地控制按鈕的顯示。
**(3)、引用UI模板**
~~~
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="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:padding="5dp"
tools:context=".MainActivity">
<!-- <include layout="@layout/topbar" /> -->
<com.imooc.systemwidget.TopBar
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
custom:leftBackground="@drawable/blue_button"
custom:leftText="Back"
custom:leftTextColor="#FFFFFF"
custom:rightBackground="@drawable/blue_button"
custom:rightText="More"
custom:rightTextColor="#FFFFFF"
custom:title="自定義標題"
custom:titleTextColor="#123412"
custom:titleTextSize="10sp" />
</RelativeLayout>
~~~
當然也可以將這個UI模板寫到一個布局文件中,如
**topbar.xml**
~~~
<com.xys.mytopbar.Topbar 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"
custom:leftBackground="@drawable/blue_button"
custom:leftText="Back"
custom:leftTextColor="#FFFFFF"
custom:rightBackground="@drawable/blue_button"
custom:rightText="More"
custom:rightTextColor="#FFFFFF"
custom:title="自定義標題"
custom:titleTextColor="#123412"
custom:titleTextSize="15sp">
</com.xys.mytopbar.Topbar>
~~~
在其他布局文件中直接通過include標簽來引用這個UI模板,如下所示
~~~
<include layout="@layout/topbar" />
~~~
**3、重寫View來實現全新的控件:**
當Android系統原生的控件無法滿足我們的需求時,就需要創建一個全新的自定義View了。通常需要繼承View類,并重寫它的onDraw()、onMeasure()等方法實現繪制邏輯,同時通過重寫onTouchEvent()等觸控事件來實現交互邏輯,還可以引入自定義屬性,豐富自定義View的可定制性。
**(1)弧線展示圖**
:-: 
圖10 弧線展示圖
很明顯,這個自定義view分為3個部分,分別是中間的圓形,中間顯示的文字和外圈的弧線,既然有了思路,只要在onDraw()方法中一個個去繪制就可以了。
>[info] 注意:這里簡單處理,把view的繪制長度直接設置為屏幕的寬度
代碼如下所示
**[CircleProgressView](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架構/SystemWidget/app/src/main/java/com/imooc/systemwidget/CircleProgressView.java)**
~~~
public class CircleProgressView extends View {
private int mMeasureHeigth;
private int mMeasureWidth;
private Paint mCirclePaint;
private float mCircleXY;//圓心坐標
private float mRadius;//半徑
private Paint mArcPaint;
private RectF mArcRectF;//橢圓形的邊界
private float mSweepAngle;//
private float mSweepValue = 66;
private Paint mTextPaint;
private String mShowText;
private float mShowTextSize;
public CircleProgressView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CircleProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CircleProgressView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
mMeasureWidth = MeasureSpec.getSize(widthMeasureSpec);
mMeasureHeigth = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(mMeasureWidth, mMeasureHeigth);
initView();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制圓
canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
// 繪制弧線
canvas.drawArc(mArcRectF,270, mSweepAngle, false, mArcPaint);
// 繪制文字
canvas.drawText(mShowText, 0, mShowText.length(),
mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint);
}
private void initView() {
float length = 0;
if (mMeasureHeigth >= mMeasureWidth) {
length = mMeasureWidth;
} else {
length = mMeasureHeigth;
}
//初始化圓形的參數
mCircleXY = length / 2;
mRadius = (float) (length * 0.5 / 2);
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(true);//抗鋸齒
mCirclePaint.setColor(getResources().getColor(android.R.color.holo_blue_bright));
//用來定義弧線的形狀和大小
mArcRectF = new RectF(
(float) (length * 0.1),
(float) (length * 0.1),
(float) (length * 0.9),
(float) (length * 0.9));
mSweepAngle = (mSweepValue / 100f) * 360f;
mArcPaint = new Paint();
mArcPaint.setAntiAlias(true);
mArcPaint.setColor(getResources().getColor(android.R.color.holo_blue_bright));
mArcPaint.setStrokeWidth((float) (length * 0.1));
mArcPaint.setStyle(Style.STROKE);//空心,不設置style。默認是實心
mShowText = setShowText();
mShowTextSize = setShowTextSize();
mTextPaint = new Paint();
mTextPaint.setTextSize(mShowTextSize);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
private float setShowTextSize() {
this.invalidate();
return 50;
}
private String setShowText() {
this.invalidate();
return "Android Skill";
}
public void forceInvalidate() {
this.invalidate();
}
public void setSweepValue(float sweepValue) {
if (sweepValue != 0) {
mSweepValue = sweepValue;
} else {
mSweepValue = 25;
}
this.invalidate();
}
}
~~~
首先,在初始化時,設置好繪制3個圖形的參數,圓的代碼如下
~~~
mCircleXY = length / 2;
mRadius = (float) (length * 0.5 / 2);
~~~
繪制弧線,需要指定其橢圓的外接矩形,參數如下所示
~~~
//用來定義弧線的形狀和大小
mArcRectF = new RectF(
(float) (length * 0.1),
(float) (length * 0.1),
(float) (length * 0.9),
(float) (length * 0.9));
~~~
繪制文字,只需要設置好文字的起始繪制位置即可
接下來,在onDraw()方法中進行繪制就行了
~~~
// 繪制圓
canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
// 繪制弧線
canvas.drawArc(mArcRectF,270, mSweepAngle, false, mArcPaint);
// 繪制文字
canvas.drawText(mShowText, 0, mShowText.length(),
mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint);
~~~
>[info] 總結:相信這些圖形如果單獨讓你去繪制,是非常容易的事情,只是這里進行一下組合,就創建了一個新的View。其實,不論是多么復雜的圖形、控件,它都是由這些最基本的圖形繪制出來的,關鍵就在于如何去分解、設計這些圖形,當你的腦海中有了一幅設計圖之后,剩下的事情就只是對坐標的計算。
**(2)音頻條形圖**
效果如下所示
:-: 
圖11 音頻條形圖
源碼如下所示
[VolumeView](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架構/SystemWidget/app/src/main/java/com/imooc/systemwidget/VolumeView.java)
~~~
/**
* 音頻條形圖
*/
public class VolumeView extends View {
private int mWidth;
private int mRectWidth;
private int mRectHeight;
private Paint mPaint;
private int mRectCount;
private int offset = 5;
private double mRandom;
private LinearGradient mLinearGradient;//線性著色器 渲染器
public VolumeView(Context context) {
super(context);
initView();
}
public VolumeView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public VolumeView(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();
mRectHeight = getHeight();
mRectWidth = (int) (mWidth * 0.6 / mRectCount);
mLinearGradient = new LinearGradient(
0,
0,
mRectWidth,
mRectHeight,
Color.YELLOW,
Color.BLUE,
Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);//shader著色器,渲染器,實現一系列的漸變渲染效果
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mRectCount; i++) {
mRandom = Math.random();
float currentHeight = (float) (mRectHeight * mRandom);
canvas.drawRect(
(float) (mWidth * 0.4 / 2 + mRectWidth * i + offset),
currentHeight,
(float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)),
mRectHeight,
mPaint);
}
postInvalidateDelayed(300);
}
}
~~~
**分析:**
如果,我們取某一幀的靜態圖來繪制,其實就是繪制一個個的矩形,每一個矩形之間稍微偏移一點舉例即可,代碼如下所示
~~~
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mRectCount; i++) {
mRandom = Math.random();
float currentHeight = (float) (mRectHeight * mRandom);
canvas.drawRect(
(float) (mWidth * 0.4 / 2 + mRectWidth * i + offset),
currentHeight,//每一個小矩形的高
(float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)),
mRectHeight,
mPaint);
}
postInvalidateDelayed(300);
}
~~~
通過循環創建這些小矩形,通過橫坐標的不斷偏移,就可繪制出這些小矩形。下面讓這些小矩形高度隨機變化,
~~~
mRandom = Math.random();
float currentHeight = (float) (mRectHeight * mRandom);
~~~
然后每隔一段時間, `postInvalidateDelayed(300);`進行View重繪,給人造成一種視覺錯覺,感覺是音頻條在移動(其實只是它的高度變化了,再加上刷新頻率),而且在繪制小矩形的時候,給繪制的Paint對象增加一個LinearGradient漸變效果,這樣不同高度的矩形就會有不同的漸變效果,更加逼真。
漸變效果的源碼如下所示
~~~
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getWidth();
mRectHeight = getHeight();
mRectWidth = (int) (mWidth * 0.6 / mRectCount);
mLinearGradient = new LinearGradient(
0,
0,
mRectWidth,
mRectHeight,
Color.YELLOW,
Color.BLUE,
Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);//shader著色器,渲染器,實現一系列的漸變渲染效果
}
~~~
**總之,不管多么復雜的自定義View都是慢慢迭代起來的功能**