目錄

1. 自定義View基礎(chǔ)
1.1 分類
自定義View的實(shí)現(xiàn)方式有以下幾種
| 類型 | 定義 |
|---|---|
| 自定義組合控件 | 多個(gè)控件組合成為一個(gè)新的控件,方便多處復(fù)用 |
| 繼承系統(tǒng)View控件 | 繼承自TextView等系統(tǒng)控件,在系統(tǒng)控件的基礎(chǔ)功能上進(jìn)行擴(kuò)展 |
| 繼承View | 不復(fù)用系統(tǒng)控件邏輯,繼承View進(jìn)行功能定義 |
| 繼承系統(tǒng)ViewGroup | 繼承自LinearLayout等系統(tǒng)控件,在系統(tǒng)控件的基礎(chǔ)功能上進(jìn)行擴(kuò)展 |
| 繼承ViewViewGroup | 不復(fù)用系統(tǒng)控件邏輯,繼承ViewGroup進(jìn)行功能定義 |
1.2 View繪制流程
View的繪制基本由measure()、layout()、draw()這個(gè)三個(gè)函數(shù)完成
| 函數(shù) | 作用 | 相關(guān)方法 |
|---|---|---|
| measure() | 測量View的寬高 | measure(),setMeasuredDimension(),onMeasure() |
| layout() | 計(jì)算當(dāng)前View以及子View的位置 | layout(),onLayout(),setFrame() |
| draw() | 視圖的繪制工作 | draw(),onDraw() |
1.3 坐標(biāo)系
在Android坐標(biāo)系中,以屏幕左上角作為原點(diǎn),這個(gè)原點(diǎn)向右是X軸的正軸,向下是Y軸正軸。如下所示:

除了Android坐標(biāo)系,還存在View坐標(biāo)系,View坐標(biāo)系內(nèi)部關(guān)系如圖所示。

View獲取自身高度
由上圖可算出View的高度:
- width = getRight() - getLeft();
- height = getBottom() - getTop();
View的源碼當(dāng)中提供了getWidth()和getHeight()方法用來獲取View的寬度和高度,其內(nèi)部方法和上文所示是相同的,我們可以直接調(diào)用來獲取View得寬高。
View自身的坐標(biāo)
通過如下方法可以獲取View到其父控件的距離。
- getTop();獲取View到其父布局頂邊的距離。
- getLeft();獲取View到其父布局左邊的距離。
- getBottom();獲取View到其父布局頂邊的距離。
- getRight();獲取View到其父布局左邊的距離。
1.4 構(gòu)造函數(shù)
無論是我們繼承系統(tǒng)View還是直接繼承View,都需要對構(gòu)造函數(shù)進(jìn)行重寫,構(gòu)造函數(shù)有多個(gè),至少要重寫其中一個(gè)才行。如我們新建TestView,
public class TestView extends View {
/**
* 在java代碼里new的時(shí)候會(huì)用到
* @param context
*/
public TestView(Context context) {
super(context);
}
/**
* 在xml布局文件中使用時(shí)自動(dòng)調(diào)用
* @param context
*/
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 不會(huì)自動(dòng)調(diào)用,如果有默認(rèn)style時(shí),在第二個(gè)構(gòu)造函數(shù)中調(diào)用
* @param context
* @param attrs
* @param defStyleAttr
*/
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 只有在API版本>21時(shí)才會(huì)用到
* 不會(huì)自動(dòng)調(diào)用,如果有默認(rèn)style時(shí),在第二個(gè)構(gòu)造函數(shù)中調(diào)用
* @param context
* @param attrs
* @param defStyleAttr
* @param defStyleRes
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
1.5 自定義屬性
Android系統(tǒng)的控件以android開頭的都是系統(tǒng)自帶的屬性。為了方便配置自定義View的屬性,我們也可以自定義屬性值。
Android自定義屬性可分為以下幾步:
- 自定義一個(gè)View
- 編寫values/attrs.xml,在其中編寫styleable和item等標(biāo)簽元素
- 在布局文件中View使用自定義的屬性(注意namespace)
- 在View的構(gòu)造方法中通過TypedArray獲取
實(shí)例說明
- 自定義屬性的聲明文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="test">
<attr name="text" format="string" />
<attr name="testAttr" format="integer" />
</declare-styleable>
</resources>
- 自定義View類
public class MyTextView extends View {
private static final String TAG = MyTextView.class.getSimpleName();
//在View的構(gòu)造方法中通過TypedArray獲取
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test);
String text = ta.getString(R.styleable.test_testAttr);
int textAttr = ta.getInteger(R.styleable.test_text, -1);
Log.e(TAG, "text = " + text + " , textAttr = " + textAttr);
ta.recycle();
}
}
- 布局文件中使用
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res/com.example.test"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.test.MyTextView
android:layout_width="100dp"
android:layout_height="200dp"
app:testAttr="520"
app:text="helloworld" />
</RelativeLayout>
屬性值的類型format
(1). reference:參考某一資源ID
- 屬性定義:
<declare-styleable name = "名稱">
<attr name = "background" format = "reference" />
</declare-styleable>
- 屬性使用:
<ImageView android:background = "@drawable/圖片ID"/>
(2). color:顏色值
- 屬性定義:
<attr name = "textColor" format = "color" />
- 屬性使用:
<TextView android:textColor = "#00FF00" />
(3). boolean:布爾值
- 屬性定義:
<attr name = "focusable" format = "boolean" />
- 屬性使用:
<Button android:focusable = "true"/>
(4). dimension:尺寸值
- 屬性定義:
<attr name = "layout_width" format = "dimension" />
- 屬性使用:
<Button android:layout_width = "42dip"/>
(5). float:浮點(diǎn)值
- 屬性定義:
<attr name = "fromAlpha" format = "float" />
- 屬性使用:
<alpha android:fromAlpha = "1.0"/>
(6). integer:整型值
- 屬性定義:
<attr name = "framesCount" format="integer" />
- 屬性使用:
<animated-rotate android:framesCount = "12"/>
(7). string:字符串
- 屬性定義:
<attr name = "text" format = "string" />
- 屬性使用:
<TextView android:text = "我是文本"/>
(8). fraction:百分?jǐn)?shù)
- 屬性定義:
<attr name = "pivotX" format = "fraction" />
- 屬性使用:
<rotate android:pivotX = "200%"/>
(9). enum:枚舉值
- 屬性定義:
<declare-styleable name="名稱">
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
- 屬性使用:
<LinearLayout
android:orientation = "vertical">
</LinearLayout>
注意:枚舉類型的屬性在使用的過程中只能同時(shí)使用其中一個(gè),不能 android:orientation = “horizontal|vertical"
(10). flag:位或運(yùn)算
- 屬性定義:
<declare-styleable name="名稱">
<attr name="gravity">
<flag name="top" value="0x01" />
<flag name="bottom" value="0x02" />
<flag name="left" value="0x04" />
<flag name="right" value="0x08" />
<flag name="center_vertical" value="0x16" />
...
</attr>
</declare-styleable>
- 屬性使用:
<TextView android:gravity="bottom|left"/>
注意:位運(yùn)算類型的屬性在使用的過程中可以使用多個(gè)值
(11). 混合類型:屬性定義時(shí)可以指定多種類型值
- 屬性定義:
<declare-styleable name = "名稱">
<attr name = "background" format = "reference|color" />
</declare-styleable>
- 屬性使用:
<ImageView
android:background = "@drawable/圖片ID" />
或者:
<ImageView
android:background = "#00FF00" />
2. View繪制流程
這一章節(jié)偏向于解釋View繪制的源碼實(shí)現(xiàn),可以更好地幫助我們掌握整個(gè)繪制過程。
View的繪制基本由measure()、layout()、draw()這個(gè)三個(gè)函數(shù)完成
| 函數(shù) | 作用 | 相關(guān)方法 |
|---|---|---|
| measure() | 測量View的寬高 | measure(),setMeasuredDimension(),onMeasure() |
| layout() | 計(jì)算當(dāng)前View以及子View的位置 | layout(),onLayout(),setFrame() |
| draw() | 視圖的繪制工作 | draw(),onDraw() |
2.1 Measure()
MeasureSpec
MeasureSpec是View的內(nèi)部類,它封裝了一個(gè)View的尺寸,在onMeasure()當(dāng)中會(huì)根據(jù)這個(gè)MeasureSpec的值來確定View的寬高。
MeasureSpec的值保存在一個(gè)int值當(dāng)中。一個(gè)int值有32位,前兩位表示模式mode后30位表示大小size。即MeasureSpec = mode + size。
在MeasureSpec當(dāng)中一共存在三種mode:UNSPECIFIED、EXACTLY 和
AT_MOST。
對于View來說,MeasureSpec的mode和Size有如下意義
| 模式 | 意義 | 對應(yīng) |
|---|---|---|
| EXACTLY | 精準(zhǔn)模式,View需要一個(gè)精確值,這個(gè)值即為MeasureSpec當(dāng)中的Size | match_parent |
| AT_MOST | 最大模式,View的尺寸有一個(gè)最大值,View不可以超過MeasureSpec當(dāng)中的Size值 | wrap_content |
| UNSPECIFIED | 無限制,View對尺寸沒有任何限制,View設(shè)置為多大就應(yīng)當(dāng)為多大 | 一般系統(tǒng)內(nèi)部使用 |
使用方式
// 獲取測量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 獲取測量大?。⊿ize)
int specSize = MeasureSpec.getSize(measureSpec)
// 通過Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
在View當(dāng)中,MeasureSpace的測量代碼如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
//當(dāng)父View要求一個(gè)精確值時(shí),為子View賦值
case MeasureSpec.EXACTLY:
//如果子view有自己的尺寸,則使用自己的尺寸
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//當(dāng)子View是match_parent,將父View的大小賦值給子View
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
//如果子View是wrap_content,設(shè)置子View的最大尺寸為父View
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局給子View了一個(gè)最大界限
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//如果子view有自己的尺寸,則使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 父View的尺寸為子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//父View的尺寸為子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局對子View沒有做任何限制
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//如果子view有自己的尺寸,則使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//因父布局沒有對子View做出限制,當(dāng)子View為MATCH_PARENT時(shí)則大小為0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//因父布局沒有對子View做出限制,當(dāng)子View為WRAP_CONTENT時(shí)則大小為0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
這里需要注意,這段代碼只是在為子View設(shè)置
MeasureSpec參數(shù)而不是實(shí)際的設(shè)置子View的大小。子View的最終大小需要在View中具體設(shè)置。
從源碼可以看出來,子View的測量模式是由自身LayoutParam和父View的MeasureSpec來決定的。
在測量子View大小時(shí):
| 父View mode | 子View |
|---|---|
| UNSPECIFIED | 父布局沒有做出限制,子View有自己的尺寸,則使用,如果沒有則為0 |
| EXACTLY | 父布局采用精準(zhǔn)模式,有確切的大小,如果有大小則直接使用,如果子View沒有大小,子View不得超出父view的大小范圍 |
| AT_MOST | 父布局采用最大模式,存在確切的大小,如果有大小則直接使用,如果子View沒有大小,子View不得超出父view的大小范圍 |
onMeasure()
整個(gè)測量過程的入口位于View的measure方法當(dāng)中,該方法做了一些參數(shù)的初始化之后調(diào)用了onMeasure方法,這里我們主要分析onMeasure。
onMeasure方法的源碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
很簡單這里只有一行代碼,涉及到了三個(gè)方法我們挨個(gè)分析。
- setMeasuredDimension(int measuredWidth, int measuredHeight) :該方法用來設(shè)置View的寬高,在我們自定義View時(shí)也會(huì)經(jīng)常用到。
- getDefaultSize(int size, int measureSpec):該方法用來獲取View默認(rèn)的寬高,結(jié)合源碼來看。
/**
* 有兩個(gè)參數(shù)size和measureSpec
* 1、size表示View的默認(rèn)大小,它的值是通過`getSuggestedMinimumWidth()方法來獲取的,之后我們再分析。
* 2、measureSpec則是我們之前分析的MeasureSpec,里面存儲(chǔ)了View的測量值以及測量模式
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//從這里我們看出,對于AT_MOST和EXACTLY在View當(dāng)中的處理是完全相同的。所以在我們自定義View時(shí)要對這兩種模式做出處理。
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
- getSuggestedMinimumWidth():getHeight和該方法原理是一樣的,這里只分析這一個(gè)。
//當(dāng)View沒有設(shè)置背景時(shí),默認(rèn)大小就是mMinWidth,這個(gè)值對應(yīng)Android:minWidth屬性,如果沒有設(shè)置時(shí)默認(rèn)為0.
//如果有設(shè)置背景,則默認(rèn)大小為mMinWidth和mBackground.getMinimumWidth()當(dāng)中的較大值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
ViewGroup的測量過程與View有一點(diǎn)點(diǎn)區(qū)別,其本身是繼承自View,它沒有對View的measure方法以及onMeasure方法進(jìn)行重寫。
為什么沒有重寫onMeasure呢?ViewGroup除了要測量自身寬高外還需要測量各個(gè)子View的大小,而不同的布局測量方式也都不同(可參考LinearLayout以及FrameLayout),所以沒有辦法統(tǒng)一設(shè)置。因此它提供了測量子View的方法measureChildren()以及measureChild()幫助我們對子View進(jìn)行測量。
measureChildren()以及measureChild()的源碼這里不再分析,大致流程就是遍歷所有的子View,然后調(diào)用View的measure()方法,讓子View測量自身大小。具體測量流程上面也以及介紹過了
measure過程會(huì)因?yàn)椴季值牟煌蛘咝枨蟮牟煌尸F(xiàn)不同的形式,使用時(shí)還是要根據(jù)業(yè)務(wù)場景來具體分析,如果想再深入研究可以看一下LinearLayout的onMeasure方法。
2.2 Layout()
要計(jì)算位置首先要對Android坐標(biāo)系有所了解,前面的內(nèi)容我們也有介紹過。
layout()過程,對于View來說用來計(jì)算View的位置參數(shù),對于ViewGroup來說,除了要測量自身位置,還需要測量子View的位置。
layout()方法是整個(gè)Layout()流程的入口,看一下這部分源碼
/**
* 這里的四個(gè)參數(shù)l、t、r、b分別代表View的左、上、右、下四個(gè)邊界相對于其父View的距離。
*
*/
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//這里通過setFrame或setOpticalFrame方法確定View在父容器當(dāng)中的位置。
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//調(diào)用onLayout方法。onLayout方法是一個(gè)空實(shí)現(xiàn),不同的布局會(huì)有不同的實(shí)現(xiàn)。
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
}
}
從源碼我們知道,在layout()方法中已經(jīng)通過setOpticalFrame(l, t, r, b)或 setFrame(l, t, r, b)方法對View自身的位置進(jìn)行了設(shè)置,所以onLayout(changed, l, t, r, b)方法主要是ViewGroup對子View的位置進(jìn)行計(jì)算。
有興趣的可以看一下
LinearLayout的onLayout源碼,可以幫助加深理解。
2.3 Draw()
draw流程也就是的View繪制到屏幕上的過程,整個(gè)流程的入口在View的draw()方法之中,而源碼注釋也寫的很明白,整個(gè)過程可以分為6個(gè)步驟。
- 如果需要,繪制背景。
- 有過有必要,保存當(dāng)前canvas。
- 繪制View的內(nèi)容。
- 繪制子View。
- 如果有必要,繪制邊緣、陰影等效果。
- 繪制裝飾,如滾動(dòng)條等等。
通過各個(gè)步驟的源碼再做分析:
public void draw(Canvas canvas) {
int saveCount;
// 1. 如果需要,繪制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2. 有過有必要,保存當(dāng)前canvas。
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 3. 繪制View的內(nèi)容。
if (!dirtyOpaque) onDraw(canvas);
// 4. 繪制子View。
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 6. 繪制裝飾,如滾動(dòng)條等等。
onDrawForeground(canvas);
// we're done...
return;
}
}
/**
* 1.繪制View背景
*/
private void drawBackground(Canvas canvas) {
//獲取背景
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
//獲取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,則會(huì)在平移后的canvas上面繪制背景。
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
/**
* 3.繪制View的內(nèi)容,該方法是一個(gè)空的實(shí)現(xiàn),在各個(gè)業(yè)務(wù)當(dāng)中自行處理。
*/
protected void onDraw(Canvas canvas) {
}
/**
* 4. 繪制子View。該方法在View當(dāng)中是一個(gè)空的實(shí)現(xiàn),在各個(gè)業(yè)務(wù)當(dāng)中自行處理。
* 在ViewGroup當(dāng)中對dispatchDraw方法做了實(shí)現(xiàn),主要是遍歷子View,并調(diào)用子類的draw方法,一般我們不需要自己重寫該方法。
*/
protected void dispatchDraw(Canvas canvas) {
}
3. 自定義組合控件
自定義組合控件就是將多個(gè)控件組合成為一個(gè)新的控件,主要解決多次重復(fù)使用同一類型的布局。如我們頂部的HeaderView以及dailog等,我們都可以把他們組合成一個(gè)新的控件。
我們通過一個(gè)自定義HeaderView實(shí)例來了解自定義組合控件的用法。
1. 編寫布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="@+id/header_root_layout"
android:layout_height="45dp"
android:background="#827192">
<ImageView
android:id="@+id/header_left_img"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_alignParentLeft="true"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:src="@drawable/back"
android:scaleType="fitCenter"/>
<TextView
android:id="@+id/header_center_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:lines="1"
android:maxLines="11"
android:ellipsize="end"
android:text="title"
android:textStyle="bold"
android:textColor="#ffffff"/>
<ImageView
android:id="@+id/header_right_img"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_alignParentRight="true"
android:src="@drawable/add"
android:scaleType="fitCenter"
android:paddingRight="12dp"
android:paddingLeft="12dp"/>
</RelativeLayout>
布局很簡單,中間是title的文字,左邊是返回按鈕,右邊是一個(gè)添加按鈕。
2. 實(shí)現(xiàn)構(gòu)造方法
//因?yàn)槲覀兊牟季植捎肦elativeLayout,所以這里繼承RelativeLayout。
//關(guān)于各個(gè)構(gòu)造方法的介紹可以參考前面的內(nèi)容
public class YFHeaderView extends RelativeLayout {
public YFHeaderView(Context context) {
super(context);
}
public YFHeaderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
3. 初始化UI
//初始化UI,可根據(jù)業(yè)務(wù)需求設(shè)置默認(rèn)值。
private void initView(Context context) {
LayoutInflater.from(context).inflate(R.layout.view_header, this, true);
img_left = (ImageView) findViewById(R.id.header_left_img);
img_right = (ImageView) findViewById(R.id.header_right_img);
text_center = (TextView) findViewById(R.id.header_center_text);
layout_root = (RelativeLayout) findViewById(R.id.header_root_layout);
layout_root.setBackgroundColor(Color.BLACK);
text_center.setTextColor(Color.WHITE);
}
4. 提供對外的方法
可以根據(jù)業(yè)務(wù)需求對外暴露一些方法。
//設(shè)置標(biāo)題文字的方法
private void setTitle(String title) {
if (!TextUtils.isEmpty(title)) {
text_center.setText(title);
}
}
//對左邊按鈕設(shè)置事件的方法
private void setLeftListener(OnClickListener onClickListener) {
img_left.setOnClickListener(onClickListener);
}
//對右邊按鈕設(shè)置事件的方法
private void setRightListener(OnClickListener onClickListener) {
img_right.setOnClickListener(onClickListener);
}
5. 在布局當(dāng)中引用該控件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.yf.view.YFHeaderView
android:layout_width="match_parent"
android:layout_height="45dp">
</com.example.yf.view.YFHeaderView>
</LinearLayout>
到這里基本的功能已經(jīng)有了。除了這些基礎(chǔ)功能外,我們還可以做一些功能擴(kuò)展,比如可以在布局時(shí)設(shè)置我的View顯示的元素,因?yàn)榭赡苡行┬枨蟛⒉恍枰疫叺陌粹o。這時(shí)候就需要用到自定義屬性來解決了。
前面已經(jīng)簡單介紹過自定義屬性的相關(guān)知識,我們之間看代碼
1.首先在values目錄下創(chuàng)建attrs.xml
內(nèi)容如下:
<resources>
<declare-styleable name="HeaderBar">
<attr name="title_text_clolor" format="color"></attr>
<attr name="title_text" format="string"></attr>
<attr name="show_views">
<flag name="left_text" value="0x01" />
<flag name="left_img" value="0x02" />
<flag name="right_text" value="0x04" />
<flag name="right_img" value="0x08" />
<flag name="center_text" value="0x10" />
<flag name="center_img" value="0x20" />
</attr>
</declare-styleable>
</resources>
這里我們定義了三個(gè)屬性,文字內(nèi)容、顏色以及要顯示的元素。
2.在java代碼中進(jìn)行設(shè)置
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);
//獲取title_text屬性
String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);
if (!TextUtils.isEmpty(title)) {
text_center.setText(title);
}
//獲取show_views屬性,如果沒有設(shè)置時(shí)默認(rèn)為0x26
showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views, 0x26);
text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));
mTypedArray.recycle();
showView(showView);
}
private void showView(int showView) {
//將showView轉(zhuǎn)換為二進(jìn)制數(shù),根據(jù)不同位置上的值設(shè)置對應(yīng)View的顯示或者隱藏。
Long data = Long.valueOf(Integer.toBinaryString(showView));
element = String.format("%06d", data);
for (int i = 0; i < element.length(); i++) {
if(i == 0) ;
if(i == 1) text_center.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
if(i == 2) img_right.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
if(i == 3) ;
if(i == 4) img_left.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
if(i == 5) ;
}
}
3.在布局文件中進(jìn)行設(shè)置
<com.example.yf.view.YFHeaderView
android:layout_width="match_parent"
android:layout_height="45dp"
app:title_text="標(biāo)題"
app:show_views="center_text|left_img|right_img">
</com.example.yf.view.YFHeaderView>
OK,到這里整個(gè)View基本定義完成。整個(gè)YFHeaderView的代碼如下
public class YFHeaderView extends RelativeLayout {
private ImageView img_left;
private TextView text_center;
private ImageView img_right;
private RelativeLayout layout_root;
private Context context;
String element;
private int showView;
public YFHeaderView(Context context) {
super(context);
this.context = context;
initView(context);
}
public YFHeaderView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
initView(context);
initAttrs(context, attrs);
}
public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
initView(context);
initAttrs(context, attrs);
}
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);
String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);
if (!TextUtils.isEmpty(title)) {
text_center.setText(title);
}
showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views, 0x26);
text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));
mTypedArray.recycle();
showView(showView);
}
private void showView(int showView) {
Long data = Long.valueOf(Integer.toBinaryString(showView));
element = String.format("%06d", data);
for (int i = 0; i < element.length(); i++) {
if(i == 0) ;
if(i == 1) text_center.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
if(i == 2) img_right.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
if(i == 3) ;
if(i == 4) img_left.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
if(i == 5) ;
}
}
private void initView(final Context context) {
LayoutInflater.from(context).inflate(R.layout.view_header, this, true);
img_left = (ImageView) findViewById(R.id.header_left_img);
img_right = (ImageView) findViewById(R.id.header_right_img);
text_center = (TextView) findViewById(R.id.header_center_text);
layout_root = (RelativeLayout) findViewById(R.id.header_root_layout);
layout_root.setBackgroundColor(Color.BLACK);
text_center.setTextColor(Color.WHITE);
img_left.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(context, element + "", Toast.LENGTH_SHORT).show();
}
});
}
private void setTitle(String title) {
if (!TextUtils.isEmpty(title)) {
text_center.setText(title);
}
}
private void setLeftListener(OnClickListener onClickListener) {
img_left.setOnClickListener(onClickListener);
}
private void setRightListener(OnClickListener onClickListener) {
img_right.setOnClickListener(onClickListener);
}
}
4. 繼承系統(tǒng)控件
繼承系統(tǒng)的控件可以分為繼承View子類(如TextVIew等)和繼承ViewGroup子類(如LinearLayout等),根據(jù)業(yè)務(wù)需求的不同,實(shí)現(xiàn)的方式也會(huì)有比較大的差異。這里介紹一個(gè)比較簡單的,繼承自View的實(shí)現(xiàn)方式。
業(yè)務(wù)需求:為文字設(shè)置背景,并在布局中間添加一條橫線。
因?yàn)檫@種實(shí)現(xiàn)方式會(huì)復(fù)用系統(tǒng)的邏輯,大多數(shù)情況下我們希望復(fù)用系統(tǒng)的onMeaseur和onLayout流程,所以我們只需要重寫onDraw方法 。實(shí)現(xiàn)非常簡單,話不多說,直接上代碼。
public class LineTextView extends TextView {
//定義畫筆,用來繪制中心曲線
private Paint mPaint;
/**
* 創(chuàng)建構(gòu)造方法
* @param context
*/
public LineTextView(Context context) {
super(context);
init();
}
public LineTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public LineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
}
//重寫draw方法,繪制我們需要的中間線以及背景
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
mPaint.setColor(Color.BLUE);
//繪制方形背景
RectF rectF = new RectF(0,0,width,height);
canvas.drawRect(rectF,mPaint);
mPaint.setColor(Color.BLACK);
//繪制中心曲線,起點(diǎn)坐標(biāo)(0,height/2),終點(diǎn)坐標(biāo)(width,height/2)
canvas.drawLine(0,height/2,width,height/2,mPaint);
}
}
對于View的繪制還需要對
Paint()、canvas以及Path的使用有所了解,不清楚的可以稍微了解一下。
這里的實(shí)現(xiàn)比較簡單,因?yàn)榫唧w實(shí)現(xiàn)會(huì)與業(yè)務(wù)環(huán)境密切相關(guān),這里只是做一個(gè)參考。
5. 直接繼承View
直接繼承View會(huì)比上一種實(shí)現(xiàn)方復(fù)雜一些,這種方法的使用情景下,完全不需要復(fù)用系統(tǒng)控件的邏輯,除了要重寫onDraw外還需要對onMeasure方法進(jìn)行重寫。
我們用自定義View來繪制一個(gè)正方形。
- 首先定義構(gòu)造方法,以及做一些初始化操作
ublic class RectView extends View{
//定義畫筆
private Paint mPaint = new Paint();
/**
* 實(shí)現(xiàn)構(gòu)造方法
* @param context
*/
public RectView(Context context) {
super(context);
init();
}
public RectView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setColor(Color.BLUE);
}
}
- 重寫draw方法,繪制正方形,注意對padding屬性進(jìn)行設(shè)置
/**
* 重寫draw方法
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//獲取各個(gè)編劇的padding值
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//獲取繪制的View的寬度
int width = getWidth()-paddingLeft-paddingRight;
//獲取繪制的View的高度
int height = getHeight()-paddingTop-paddingBottom;
//繪制View,左上角坐標(biāo)(0+paddingLeft,0+paddingTop),右下角坐標(biāo)(width+paddingLeft,height+paddingTop)
canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint);
}
之前我們講到過View的measure過程,再看一下源碼對這一步的處理
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
在View的源碼當(dāng)中并沒有對AT_MOST和EXACTLY兩個(gè)模式做出區(qū)分,也就是說View在wrap_content和match_parent兩個(gè)模式下是完全相同的,都會(huì)是match_parent,顯然這與我們平時(shí)用的View不同,所以我們要重寫onMeasure方法。
- 重寫
onMeasure方法
/**
* 重寫onMeasure方法
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//處理wrap_contentde情況
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, 300);
}
}
整個(gè)自定義View的代碼如下:
public class RectView extends View {
//定義畫筆
private Paint mPaint = new Paint();
/**
* 實(shí)現(xiàn)構(gòu)造方法
*
* @param context
*/
public RectView(Context context) {
super(context);
init();
}
public RectView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setColor(Color.BLUE);
}
/**
* 重寫onMeasure方法
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, 300);
}
}
/**
* 重寫draw方法
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//獲取各個(gè)編劇的padding值
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//獲取繪制的View的寬度
int width = getWidth() - paddingLeft - paddingRight;
//獲取繪制的View的高度
int height = getHeight() - paddingTop - paddingBottom;
//繪制View,左上角坐標(biāo)(0+paddingLeft,0+paddingTop),右下角坐標(biāo)(width+paddingLeft,height+paddingTop)
canvas.drawRect(0 + paddingLeft, 0 + paddingTop, width + paddingLeft, height + paddingTop, mPaint);
}
}
整個(gè)過程大致如下,直接繼承View時(shí)需要有幾點(diǎn)注意:
1、在onDraw當(dāng)中對padding屬性進(jìn)行處理。
2、在onMeasure過程中對wrap_content屬性進(jìn)行處理。
3、至少要有一個(gè)構(gòu)造方法。
6. 繼承ViewGroup
自定義ViewGroup的過程相對復(fù)雜一些,因?yàn)槌艘獙ψ陨淼拇笮『臀恢眠M(jìn)行測量之外,還需要對子View的測量參數(shù)負(fù)責(zé)。
需求實(shí)例
實(shí)現(xiàn)一個(gè)類似于Viewpager的可左右滑動(dòng)的布局。
代碼比較多,我們結(jié)合注釋分析。
public class HorizontaiView extends ViewGroup {
private int lastX;
private int lastY;
private int currentIndex = 0;
private int childWidth = 0;
private Scroller scroller;
private VelocityTracker tracker;
/**
* 1.創(chuàng)建View類,實(shí)現(xiàn)構(gòu)造函數(shù)
* 實(shí)現(xiàn)構(gòu)造方法
* @param context
*/
public HorizontaiView(Context context) {
super(context);
init(context);
}
public HorizontaiView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public HorizontaiView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
scroller = new Scroller(context);
tracker = VelocityTracker.obtain();
}
/**
* 2、根據(jù)自定義View的繪制流程,重寫`onMeasure`方法,注意對wrap_content的處理
* 重寫onMeasure方法
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//獲取寬高的測量模式以及測量值
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//測量所有子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
//如果沒有子View,則View大小為0,0
if (getChildCount() == 0) {
setMeasuredDimension(0, 0);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
View childOne = getChildAt(0);
int childWidth = childOne.getMeasuredWidth();
int childHeight = childOne.getMeasuredHeight();
//View的寬度=單個(gè)子View寬度*子View個(gè)數(shù),View的高度=子View高度
setMeasuredDimension(getChildCount() * childWidth, childHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
View childOne = getChildAt(0);
int childWidth = childOne.getMeasuredWidth();
//View的寬度=單個(gè)子View寬度*子View個(gè)數(shù),View的高度=xml當(dāng)中設(shè)置的高度
setMeasuredDimension(getChildCount() * childWidth, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
View childOne = getChildAt(0);
int childHeight = childOne.getMeasuredHeight();
//View的寬度=xml當(dāng)中設(shè)置的寬度,View的高度=子View高度
setMeasuredDimension(widthSize, childHeight);
}
}
/**
* 3、接下來重寫`onLayout`方法,對各個(gè)子View設(shè)置位置。
* 設(shè)置子View位置
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int left = 0;
View child;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
childWidth = child.getMeasuredWidth();
child.layout(left, 0, left + childWidth, child.getMeasuredHeight());
left += childWidth;
}
}
}
}
到這里我們的View布局就已經(jīng)基本結(jié)束了。但是要實(shí)現(xiàn)Viewpager的效果,還需要添加對事件的處理。事件的處理流程之前我們有分析過,在制作自定義View的時(shí)候也是會(huì)經(jīng)常用到的,不了解的可以參考之前的文章Android Touch事件分發(fā)超詳細(xì)解析。
/**
* 4、因?yàn)槲覀兌x的是ViewGroup,從onInterceptTouchEvent開始。
* 重寫onInterceptTouchEvent,對橫向滑動(dòng)事件進(jìn)行攔截
* @param event
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercrpt = false;
//記錄當(dāng)前點(diǎn)擊的坐標(biāo)
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - lastX;
int delatY = y - lastY;
//當(dāng)X軸移動(dòng)的絕對值大于Y軸移動(dòng)的絕對值時(shí),表示用戶進(jìn)行了橫向滑動(dòng),對事件進(jìn)行攔截
if (Math.abs(deltaX) > Math.abs(delatY)) {
intercrpt = true;
}
break;
}
lastX = x;
lastY = y;
//intercrpt = true表示對事件進(jìn)行攔截
return intercrpt;
}
/**
* 5、當(dāng)ViewGroup攔截下用戶的橫向滑動(dòng)事件以后,后續(xù)的Touch事件將交付給`onTouchEvent`進(jìn)行處理。
* 重寫onTouchEvent方法
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
tracker.addMovement(event);
//獲取事件坐標(biāo)(x,y)
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - lastX;
int delatY = y - lastY;
//scrollBy方法將對我們當(dāng)前View的位置進(jìn)行偏移
scrollBy(-deltaX, 0);
break;
//當(dāng)產(chǎn)生ACTION_UP事件時(shí),也就是我們抬起手指
case MotionEvent.ACTION_UP:
//getScrollX()為在X軸方向發(fā)生的便宜,childWidth * currentIndex表示當(dāng)前View在滑動(dòng)開始之前的X坐標(biāo)
//distance存儲(chǔ)的就是此次滑動(dòng)的距離
int distance = getScrollX() - childWidth * currentIndex;
//當(dāng)本次滑動(dòng)距離>View寬度的1/2時(shí),切換View
if (Math.abs(distance) > childWidth / 2) {
if (distance > 0) {
currentIndex++;
} else {
currentIndex--;
}
} else {
//獲取X軸加速度,units為單位,默認(rèn)為像素,這里為每秒1000個(gè)像素點(diǎn)
tracker.computeCurrentVelocity(1000);
float xV = tracker.getXVelocity();
//當(dāng)X軸加速度>50時(shí),也就是產(chǎn)生了快速滑動(dòng),也會(huì)切換View
if (Math.abs(xV) > 50) {
if (xV < 0) {
currentIndex++;
} else {
currentIndex--;
}
}
}
//對currentIndex做出限制其范圍為【0,getChildCount() - 1】
currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;
//滑動(dòng)到下一個(gè)View
smoothScrollTo(currentIndex * childWidth, 0);
tracker.clear();
break;
}
lastX = x;
lastY = y;
return true;
}
private void smoothScrollTo(int destX, int destY) {
//startScroll方法將產(chǎn)生一系列偏移量,從(getScrollX(), getScrollY()),destX - getScrollX()和destY - getScrollY()為移動(dòng)的距離
scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000);
//invalidate方法會(huì)重繪View,也就是調(diào)用View的onDraw方法,而onDraw又會(huì)調(diào)用computeScroll()方法
invalidate();
}
//重寫computeScroll方法
@Override
public void computeScroll() {
super.computeScroll();
//當(dāng)scroller.computeScrollOffset()=true時(shí)表示滑動(dòng)沒有結(jié)束
if (scroller.computeScrollOffset()) {
//調(diào)用scrollTo方法進(jìn)行滑動(dòng),滑動(dòng)到scroller當(dāng)中計(jì)算到的滑動(dòng)位置
scrollTo(scroller.getCurrX(), scroller.getCurrY());
//沒有滑動(dòng)結(jié)束,繼續(xù)刷新View
postInvalidate();
}
}
這部分代碼比較多,為了方便閱讀,在代碼當(dāng)中進(jìn)行了注釋。
之后就是在XML代碼當(dāng)中引入自定義View
<com.example.yf.view.HorizontaiView
android:id="@+id/test_layout"
android:layout_width="match_parent"
android:layout_height="400dp">
<ListView
android:id="@+id/list1"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
<ListView
android:id="@+id/list2"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
<ListView
android:id="@+id/list3"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</com.example.yf.view.HorizontaiView>
好了,可以運(yùn)行看一下效果了。
總結(jié)
本篇文章對常用的自定義View的方式進(jìn)行了總結(jié),并簡單分析了View的繪制流程。對各種實(shí)現(xiàn)方式寫了簡單的實(shí)現(xiàn)。