因為工作原因,想寫一篇自定義view的初級心得。
一、一般而言寫自定義view有大體6個步驟(以下順序不分先后):
- 繼承View的某個子類,包括ViewGroup的子類(畢竟ViewGroup也是View的子類嘛╮(╯_╰)╭) 2. 重寫繼承的父類View的一些特定函數(shù)及常用的三個:(測量measure),(放置layout),(繪制draw)3.為自定義View類增加屬性(主要是在那三個重寫的構(gòu)造方法里)4.繪制控件(代碼形式導(dǎo)入布局)5.響應(yīng)用戶事件(單擊、輸入文字、觸摸、滑動等等~~)6.定義回調(diào)函數(shù)(相當(dāng)于反饋信息嘛)
二、針對繼承對象的不同自定義View分為繼承View 與ViewGroup兩種的情況,我上面2里的所說的常用三個使用上有所區(qū)別。
測量measure:
View:
普通View的onMeasure邏輯大同小異,基本都是測量自身內(nèi)容和背景,然后根據(jù)父View傳遞過來的MeasureSpec進行最終的大小判定,例如TextView會根據(jù)文字的長度,文字的大小,文字行高,文字的行寬,顯示方式,背景圖片,以及父View傳遞過來的模式和大小最終確定自身的大小。具體的View寬高測量是調(diào)用了 setMeasuredDimension() 方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
onMeasure通過父View傳遞過來的大小和模式,以及自身的背景圖片的大小得出自身最終的大小,通過setMeasuredDimension()方法設(shè)置給mMeasuredWidth和mMeasuredHeight。 ViewGroup:
ViewGroup本身沒有實現(xiàn)onMeasure(但是!有setMeasuredDimension()方法),但是他的子類(比如:四大布局控件)都有各自的實現(xiàn),通常他們都是通過measureChildWithMargins()這種測量內(nèi)部子view的方法來遍歷內(nèi)部,測量子View。當(dāng)所有的子View都測量完畢后,才根據(jù)父View傳遞過來的模式和大小來最終決定自身的大小。
** 注意事項:如果子View被GONE的將不參與測量。**
ViewGroup一般都在測量完所有子View后才會調(diào)用setMeasuredDimension()設(shè)置自身大小。
經(jīng)過measure 完成后,我們就可以通過getMeasuredWidth/Height 獲取View 的寬高。 放置layout:
View:
普通View中的onLayout()這個函數(shù)為空函數(shù)。所以不用理會,想想也是的吧,如果你繼承的是view,你還有擺放你里面的內(nèi)容嗎?如果里面有東西需要你的擺放,那么,這個view不就是父view了!這個不就該是繼承的是ViewGroup。好的,往下看。
ViewGroup:
對于ViewGroup而言,循環(huán)遍歷所有子View是主要的思想?。?!因此如果我們繼承ViewGroup 我們需要遍歷執(zhí)行所有的child.layout()。
Layout方法中接受四個參數(shù),是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置時通常會根據(jù)子View在measure中測量的大小來決定。注意事項:子View的位置通常還受到父View的orientation,gravity,padding,子View的margin等等屬性的影響哦,我相信寫過在xml寫過布局的各位大大肯定是了解的吧。
ViewGroup中的onLayout()方法:
@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
抽象就表示了繼承ViewGroup的子類布局控件,都要去重寫。而這個重寫也就導(dǎo)致了,不同的布局方式。怎么重寫呢?舉個例子:我這里將第一個子控件通過layout()放置到左上角0,0 寬高是測量值。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
View childView = getChildAt(0); childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); }
繪制draw draw()的過程就是繪制View到屏幕上的過程,draw()的執(zhí)行遵循如下步驟:
- 繪制背景
2.保存畫布的圖層來準備色變 - 繪制內(nèi)容
4.繪制children
5.畫出褪色的邊緣和恢復(fù)層 - 繪制裝飾 比如scollbar
2和5 可以跳過的。
View:
view中onDraw()是個空函數(shù),也就是說需要每個視圖根據(jù)想要展示的內(nèi)容來自行繪制,View是不會幫我們繪制內(nèi)容部分的,因此需要每個視圖根據(jù)想要展示的內(nèi)容來自行繪制。如果你去觀察TextView、ImageView等類的源碼,你會發(fā)現(xiàn)它們都有重寫onDraw()這個方法,并且在里面執(zhí)行了相當(dāng)不少的繪制邏輯: 在TextView中在該方法中繪制文字、光標和CompoundDrawable;ImageView中相對簡單,只是繪制了圖片。繪制的方式主要是借助Canvas這個類,它會作為參數(shù)傳入到onDraw()方法中,供給每個視圖使用。Canvas這個類的用法非常豐富,基本可以把它當(dāng)成一塊畫布,在上面繪制任意的東西,那么我們就來嘗試一下吧。
View 的繪制主要通過dispatchDraw(),先根據(jù)自身的padding剪裁畫布,所有的子View都將在畫布剪裁后的區(qū)域繪制。遍歷所有子View,調(diào)用子View的computeScroll對子View的滾動值進行計算。根據(jù)滾動值和子View在父View中的坐標進行畫布原點坐標的移動,根據(jù)子在父View中的坐標計算出子View的視圖大小,然后對畫布進行剪裁,請看下面的示意圖。
ViewGroup:
對于ViewGroup則不需要實現(xiàn)該函數(shù),因為作為容器是“沒有內(nèi)容“的(但必須ViewGroup要有實現(xiàn)dispatchDraw()函數(shù),告訴子view去繪制自己)。注意事項:dispatchDraw的邏輯其實比較復(fù)雜,但ViewGroup已經(jīng)處理好了,我們不必要重載該方法對子View進行繪制事件的派遣分發(fā)。
三、其他一些可以用來重寫的方法:
onTouchEvent定義觸屏事件來響應(yīng)用戶操作。 onKeyDown 當(dāng)按下某個鍵盤時
onKeyUp 當(dāng)松開某個鍵盤時
onTrackballEvent 當(dāng)發(fā)生軌跡球事件時
onSizeChange() 當(dāng)該組件的大小被改變時
onFinishInflate() 回調(diào)方法,當(dāng)應(yīng)用從XML加載該組件并用它構(gòu)建界面之后調(diào)用的方法
onWindowFocusChanged(boolean) 當(dāng)該組件得到、失去焦點時
onAttachedToWindow() 當(dāng)把該組件放入到某個窗口時
onDetachedFromWindow() 當(dāng)把該組件從某個窗口上分離時觸發(fā)的方法
onWindowVisibilityChanged(int): 當(dāng)包含該組件的窗口的可見性發(fā)生改變時觸發(fā)的方法
四、View的繪制流程
繪制流程函數(shù)調(diào)用關(guān)系如下圖(取來用之):
五:requestLayout() 、invalidate()、postInvalidate()
requestLayout(): 當(dāng)view確定自身已經(jīng)不再適合現(xiàn)有的區(qū)域時,該view本身調(diào)用requestLayout()方法來要求parent view(父類的視圖)重新調(diào)用他的measure和layout來重新設(shè)置自己位置。特別是當(dāng)view的layoutparameter發(fā)生改變,并且它的值還沒能應(yīng)用到view上時,這時候適合調(diào)用這個方法。注意,并不會不執(zhí)行ondraw。
invalidate()、postInvalidate(): 調(diào)用invalidate()、postInvalidate()會 界面刷新,執(zhí)行 draw 過程。區(qū)別就是Invalidate不能直接在線程中調(diào)用,因為他是違背了單線程模型:Android UI操作并不是線程安全的,并且這些操作必須在UI線程中調(diào)用。 鑒于此,如果要使用invalidate的刷新,那我們就得配合handler的使用,使異步非ui線程轉(zhuǎn)到ui線程中調(diào)用,如果要在非ui線程中直接使用就調(diào)用postInvalidate方法即可,這樣就省去使用handler的煩惱。
六、自定義控件的三種方式
1、 繼承已有的控件當(dāng)要實現(xiàn)的控件和已有的控件在很多方面比較類似, 通過對已有控件的擴展來滿足要求。即:繼承TextView、Button這樣已有的View(包括項目里已有的自定義View)。
2、 繼承一個布局文件一般用于自定義組合控件,在構(gòu)造函數(shù)中通過inflater和addView()方法加載自定義控件的布局文件形成圖形界面(不需要onDraw方法),就好像是把activity的xml變成用自定義view的xml來表示。
3、繼承view通過onDraw方法來繪制出組件界面。即繼承View,得到和TextView、Button這樣等級的View 。
七、自定義屬性的兩種方法
1、在布局文件中直接加入屬性,在構(gòu)造函數(shù)中去獲得。
布局文件:
<?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"
>
<rcjs.com.customview.ZYView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Text="rcjs"
/>
</RelativeLayout>
獲取屬性值:
public ZYView(Context context, AttributeSet attrs) {
super(context, attrs);
int textId = attrs.getAttributeResourceValue(null, "Text", 0);
String text = context.getResources().getText(textId).toString();
}
2、在res/values/ 下建立一個attrs.xml 來聲明自定義view的屬性。
可以定義的屬性有:
<declare-styleable name="名稱">//參考某一資源ID (name可以隨便命名)
<attr name="background" format="reference"/>
//顏色值
<attr name="textColor" format="color"/>
//布爾值
<attr name="focusable" format="boolean"/>
//尺寸值
<attr name="layout_width" format="dimension"/>
//浮點值
<attr name="fromAlpha" format="float"/>
//整型值
<attr name="frameDuration" format="integer"/>
//字符串
<attr name="text" format="string"/>
//百分數(shù)
<attr name="pivotX" format="fraction"/>
//枚舉值
<attr name="orientation">
<enum name="horizontal" value="0"/>
<enum name="vertical" value="1"/>
</attr>
//位或運算
<attr name="windowSoftInputMode">
<flag name="stateUnspecified" value="0"/>
<flag name="stateUnchanged" value="1"/>
</attr>
//多類型
<attr name="background" format="reference|color"/>
</declare-styleable>
attrs.xml進行屬性聲明
declare-styleable的name 就是自定義的名稱用于布局文件里去
attr的name是屬性名稱
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="zyView">
<attr name="Text" format="string"/>
<attr name="textColor" format="color"/>
</declare-styleable>
</resources>
添加到布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:zyView="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<rcjs.com.customview.ZYView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
zyView:Text="rcjs"
/>
</RelativeLayout>
注意事項:
**命名空間: **
xmlns:前綴=”http://schemas.android.com/apk/res/包名(或res-auto)”,
前綴:+使用屬性。
在構(gòu)造函數(shù)中獲取屬性值,注意!??!我想有一些人應(yīng)該會很郁悶 ,復(fù)制粘貼了自定義view.class后,發(fā)現(xiàn)自定義view的構(gòu)造方法里面獲得資源文件里的屬性時****,看到R.styleable.XXX這個,然后點擊時****找不到具體寫的地方。其實這個就在res -> values ->attrs里。所以要記得去copy哦。
public class ZYView extends View {
public ZYView(Context context) {
super(context);
}
public ZYView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//獲取資源文件里面的屬性,由于這里只有一個屬性值,不用遍歷數(shù)組,直接通過R文件拿出color值
//把屬性放在資源文件里,方便設(shè)置和復(fù)用
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.zyView); String text = a.getString(R.styleable.zyView_Text);
int textColor = a.getColor(R.styleable.zyView_textColor, Color.WHITE); a.recycle();
}
}
八、結(jié)尾
這只是讓大家知道自定義view的制作需要什么和要哪些步驟。像我這種完全一竅不通的,然后一下子去接觸自定義view的,是會很糊涂的,所以,在此,小僧稍微筆記一波,助人助己。而具體的對自定義view的學(xué)習(xí),請待續(xù)。。。
當(dāng)然,現(xiàn)在已有大佬們寫了很多博客。請參考這篇總的去學(xué)習(xí):
http://www.lxweimin.com/p/6aea80e1fa22