Android自定義view詳解
對於我這樣一個Android初級開發者來說,自定義View一直是一個遙不可及的東西,每次看到別人做的特別漂亮的控制元件,自己心裡那個癢癢啊,可是又生性懶惰,自己不肯努力去看書,只能望而興嘆,每次做需求用到自定義控制元件,就直接去Github上找,找到合適的就用,找不到合適的,湊合也用,反正從來沒想過要自己來做這樣的東西,可是畢業以後到了新公司,為了自己的榮譽,這次不得不硬著頭皮自己來了,一個月的緊張開發過後,回頭再看,自定義控制元件也無非那麼回事,只要記得幾個要領,幾乎是手到擒來。
從繼承開始
懂點面嚮物件語言知識的都知道:封裝,繼承和多型,這是物件導向的三個基本特徵,所以在自定義View的時候,最簡單的方法就是繼承現有的View
public class SketchView extends View{ public SketchView(Context context) { super(context); } public SketchView(Context context, AttributeSet attrs) { super(context, attrs); } public SketchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }
通過上面這段程式碼,我定義了一個SketchView,繼承自View物件,並且複寫了它的三個構造方法,我主要來分析一下這三個構造方法:
- 第一個構造方法就是我們普通在程式碼中新建一個view用到的方法,例如
SketchView sketchView = new SketchView(this);
就這樣,一個自定義的view就被新建出來了,然後可以根據需求新增到佈局裡
- 第二個構造方法就是我們一般在xml檔案裡新增一個view
<me.shaohui.androidpractise.widget.SketchView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginRight="16dp" android:layout_marginTop="16dp" />
這樣,我們就把一個SketchView新增到佈局檔案裡,並且加了一些佈局屬性,寬高屬性以及margin屬性,這些屬性會存放在第二個建構函式的AttributeSet引數裡
- 第三個建構函式比第二個建構函式多了一個int型的值,名字叫defStyleAttr,從名稱上判斷,這是一個關於自定義屬性的引數,實際上我們的猜測也是正確的,第三個建構函式不會被系統預設呼叫,而是需要我們自己去顯式呼叫,比如在第二個建構函式裡呼叫呼叫第三個函式,並將第三個引數設為0。
關於第三個引數defStyleAttr,其實也可以拿出來說一整篇文章,有想詳細瞭解的讀者可以去看下本篇文章最後的第三個參考連結,我在這裡只是簡單的說一下:defStyleAttr指定的是在Theme style定義的一個attr,它的型別是reference,主要生效在obtainStyledAttributes方法裡,obtainStyledAttributes方法有四個引數,第三個引數是defStyleAttr,第四個引數是自己指定的一個style,當且僅當defStyleAttr為0或者在Theme中找不到defStyleAttr指定的屬性時,第四個引數才會生效,這些指的都是預設屬性,當在xml裡面定義的,就以在xml檔案裡指定的為準,所以優先順序大概是:xml>style>defStyleAttr>defStyleRes>Theme指定,當defStyleAttr為0時,就跳過defStyleAttr指定的reference,所以一般用0就能滿足一些基本開發。
Measure->Layout->Draw
在學會如何寫一個自定義控制元件之前,瞭解一個控制元件的繪製流程是必要的,在Android裡,一個view的繪製流程包括:Measure,Layout和Draw,通過onMeasure知道一個view要佔介面的大小,然後通過onLayout知道這個控制元件應該放在哪個位置,最後通過onDraw方法將這個控制元件繪製出來,然後才能展現在使用者面前,下面我將挨個分析一下這三個方法的作用。
- onMeasure 測量,通過測量知道一個一個view要佔的大小,方法引數是兩個int型的值,我們都知道,在java中,int型由4個位元組(32bit)組成,在MeasureSpce中,用前兩位表示mode,用後30位表示size
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int measuredHeight, measuredWidth; if (widthMode == MeasureSpec.EXACTLY) { measuredWidth = widthSize; } else { measuredWidth = SIZE; } if (heightMode == MeasureSpec.EXACTLY) { measuredHeight = heightSize; } else { measuredHeight = SIZE; } setMeasuredDimension(measuredWidth, measuredHeight); }
MeasureSpce的mode有三種:EXACTLY, AT_MOST,UNSPECIFIED,除卻UNSPECIFIED不談,其他兩種mode:當父佈局是EXACTLY時,子控制元件確定大小或者match_parent,mode都是EXACTLY,子控制元件是wrap_content時,mode為AT_MOST;當父佈局是AT_MOST時,子控制元件確定大小,mode為EXACTLY,子控制元件wrap_content或者match_parent時,mode為AT_MOST。所以在確定控制元件大小時,需要判斷MeasureSpec的mode,不能直接用MeasureSpec的size。在進行一些邏輯處理以後,呼叫setMeasureDimension()方法,將測量得到的寬高傳進去供layout使用。
需要明白的一點是 ,測量所得的寬高不一定是最後展示的寬高,最後寬高確定是在onLayout方法裡,layou(left,top,right,bottom),不過一般都是一樣的。
- onLayout 實際上,我在自定義SketchView的時候是沒有重寫onLayout方法的,因為SketchView只是一個單純的view,它不是一個view容器,沒有子view,而onLayout方法裡主要是具體擺放子view的位置,水平擺放或者垂直襬放,所以在單純的自定義view是不需要重寫onLayout方法,不過需要注意的一點是,子view的margin屬性是否生效就要看parent是否在自身的onLayout方法進行處理,而view得padding屬性是在onDraw方法中生效的。
其實在onLayout方法裡有一個屬性我一直關注並且沒有弄得很明白,就是第一個引數boolean:changed,標示這個view的大小是否發生改變,後續瞭解到,會回來補坑。
- onDraw 終於說到了重頭戲,一般自定義控制元件耗費心思最多的就是這個方法了,需要在這個方法裡,用Paint在Canvas上畫出你想要的圖案,這樣一個自定義view才算結束。下面會詳細講如何在畫布上畫出自己想要的圖案。
關於onDraw方法,在補充一句,如果是直接繼承的View,那麼在重寫onDraw的方法是時候完全可以把super.ondraw(canvas)刪掉,因為它的預設實現是空。
得到一個正方形的View
上一部分主要說了一下,view的繪製流程,從這三個方法中,我們可以知道如何測量一個控制元件,如何擺放控制元件的子元素,如何繪製圖案,下面我說一下自己通過onMeasure學到的一點小技巧。
在日常開發中,我們偶爾會需要一個正方形的imageView,一般都是通過指定寬高,但是當寬高不確定時,我們就只能寄希望於Android原聲支援定義view的比例,但是現實是殘酷的,系統好像是沒有提供類似屬性的,所以我們就只能自己去實現,其實自己寫起來也特別的簡單,只需要改一個引數就OK了,
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, widthMeasureSpec); }
不仔細觀察是看不出來其中的奧妙的,雖然這裡複寫了view的onMeasure,但是貌似沒有做任何處理,直接呼叫了super方法,但是仔細觀察的話就會發現,在呼叫super方法的時候,第二個引數變了,本來應該是heightMeasureSpec卻換成了widthMeasureSpec,這樣view的高度就是view的寬度,一個SquareView就實現了,甚至如果通過自定義屬性實現一個自定義比例view。
自定義屬性
自定義view沒有自定義屬性怎麼得了,要給view支援自定義屬性,需要在values/attrs.xml 檔案裡定義一個name為自己定義view名字的declare-styleable
<resources> <declare-styleable name="SketchView"> <attr name="background_color" format="color"/> <attr name="size" format="dimension"/> </declare-styleable> </resources>
這樣就可以在xml檔案裡使用自己定義的屬性了
<me.shaohui.androidpractise.widget.SketchView xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginRight="16dp" android:layout_marginTop="16dp" app:background_color="@color/colorPrimary" app:size="24dp"/>
別忘了在前面加上自定義的名稱空間,到這來看,增加自定義屬性並不算什麼,不過要自定義屬性生效還是要耗費一些功夫的,這時候前面留下的伏筆:第三個構造方法的defStyleAttr引數就要登場了。
public SketchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SketchView, defStyleAttr, R.style.AppTheme); custom_size = a.getDimensionPixelSize(R.styleable.SketchView_size, SIZE); custon_background = a.getColor(R.styleable.SketchView_background_color, DEFAULT_COLOR); a.recycle(); }
經過好一番操作,才能把xml定義的屬性拿出來,具體取得的值為多少,我在前面已經解釋過,就不在這裡多說,接下來的操作就是拿著這些屬性幹你想幹的事吧。
實戰:一個動態view
下面將簡單介紹一下如何在onDraw(Canvas canvas) 在畫布的中心位置畫一個自定義顏色的圓,並且通過一個ValueAnimator讓這個圓動起來,廢話不多說,直接上程式碼:
@Override protected void onDraw(Canvas canvas) { canvas.drawCircle(mWidth/2, mHeight/2, custom_size * scale, mPaint); } private ValueAnimator mAnimator; public void startAnimation() { mAnimator = ValueAnimator.ofFloat(1, 2); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { scale = (float) animation.getAnimatedValue(); postInvalidate(); } }); // 重複次數 -1表示無限迴圈 mAnimator.setRepeatCount(-1); // 重複模式, RESTART: 重新開始 REVERSE:恢復初始狀態再開始 mAnimator.setRepeatMode(ValueAnimator.REVERSE); mAnimator.start(); }
(只貼了核心程式碼,完整程式碼會在文章最後給連結)
可以看到在onDraw()方法裡,我呼叫了canvas的drawCircle方法畫了一個圓,圓心的位置是又畫布的位置決定的,位於畫布的中心,width和height引數是在onLayout()方法裡拿到的,在此之前取到的height和width都是不準確的,這點要注意。圓的半徑是xml檔案中定義的size*scale,而這個scale是通過一個ValueAnimator確定的,變化範圍是從1到2,ValueAnimator的值發生改變會賦給scale同時呼叫postInvalidate()方法,這個方法的作用就是重繪,然後圓的半徑就會發生改變,這樣重新整理就會實現動畫的效果。
requstLayout和invidious
在自定義view時,時常用到重新整理view的方法,這時候就會有三個方法供我們選擇:requestLayout()、invalidate()、postInvalidate(),其實invalidate和postInvalidate這兩個方法作用是一樣的,唯一不同的是invalidate用在主執行緒,而postInvalidate用在非同步執行緒,下面對比一下requestLayout和invalidate的內部實現:
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } } void invalidate() { mDirty.set(0, 0, mWidth, mHeight); if (!mWillDrawSoon) { scheduleTraversals(); } }
從程式碼可以看出,其實這兩個方法內部都是呼叫的scheduleTraversals()方法,不同的是,requestLayout方法將mLayoutRequested標示置為true,scheduleTraversals這個方法以後找機會再細分析,現在只簡單說下結論,
- requestLayout會呼叫measure和layout 等一系列操作,然後根據佈局是否發生改變,surface是否被銷燬,來決定是否呼叫draw,也就是說requestlayout肯定會呼叫measure和layout,但不一定呼叫draw,讀者可以試著改下我上面寫的那個小程式,將postInvalidate改成requestlayout,動畫效果就消失了,因為佈局沒有發生改變。
- invalidate 只會呼叫draw,而且肯定會調,即使什麼都沒有發生改變,它也會重新繪製。
所以如果有佈局需要發生改變,需要呼叫requestlayout方法,如果只是重新整理動畫,則只需要呼叫invalidate方法。
自定義view的狀態儲存
自定義view的狀態儲存和Activity的狀態儲存是類似的,都是在onSaveInstanceState()儲存,然後在onRestoreInstanceState將資料安全取出,之所以在這是還是多說一嘴,主要是怕自己忘,給自己提個醒,也順便給各位看客叨擾幾句,還有一個就是,如果一個view沒有id,這個view的狀態是不會儲存的。
事件處理onTouchEvent
Android的事件處理太過複雜,我會在以後另起一篇文章來好好聊一下Android裡的事件處理。
In The End
自定義view其實是一個很重的知識點,裡面包含了很多view的知識,不是一篇文章就能聊完的,但我又不習慣連載,有什麼話總喜歡一口氣說完,所以都寫在這一篇文章裡了,我這裡只是簡單的和大家開個頭,具體深知裡面的細節還是要看很多東西。文章裡有什麼不對的地方,望各位指出。
原始碼地址:Github地址
參考連結
- http://ghui.me/post/2015/10/view-measure/
- http://blog.csdn.net/wzy_1988/article/details/49619773
- http://www.cnblogs.com/angeldevil/p/3479431.html
相關文章
- Android 自定義 view 詳解AndroidView
- 【朝花夕拾】Android自定義View篇之(四)自定義View的三種實現方式及自定義屬性詳解AndroidView
- Android自定義View:View(二)AndroidView
- Android 自定義viewAndroidView
- Android: 自定義ViewAndroidView
- 深入瞭解View實現原理以及自定義View詳解View
- Android 自定義View 滑動解鎖AndroidView
- Android 中自定義 View、ViewGroup 理論基礎詳解AndroidView
- Android中自定義View、ViewGroup理論基礎詳解AndroidView
- Android自定義View之(一)View繪製流程詳解——向原始碼要答案AndroidView原始碼
- Android自定義View整合AndroidView
- 自定義View進階篇《十》——Matrix詳解View
- Android自定義view-自繪ViewAndroidView
- android自定義view(自定義數字鍵盤)AndroidView
- android自定義View&自定義ViewGroup(下)AndroidView
- android自定義View&自定義ViewGroup(上)AndroidView
- 重拾Android自定義ViewAndroidView
- HenCoder Android 開發進階: 自定義 View 1-2 Paint 詳解AndroidViewAI
- Android自定義View播放Gif動畫AndroidView動畫
- Android 自定義View基礎(一)AndroidView
- Android自定義View:ViewGroup(三)AndroidView
- android自定義View——座標系AndroidView
- Android自定義View之捲尺AndroidView
- Android 自定義View之下雨動畫AndroidView動畫
- Android自定義View之分貝儀AndroidView
- Android自定義View注意事項AndroidView
- Android自定義View 水波氣泡AndroidView
- Android 自定義View 字型變色AndroidView
- Android 自定義View 點贊效果AndroidView
- Android自定義View-卷軸AndroidView
- Android自定義View 屬性新增AndroidView
- [原] Android 自定義View步驟AndroidView
- Android 自定義View:深入理解自定義屬性(七)AndroidView
- 自定義VIEWView
- Android自定義View:MeasureSpec的真正意義與View大小控制AndroidView
- Android圖解建立外部lib庫及自定義ViewAndroid圖解View
- Canvas類的最全面詳解 - 自定義View應用系列CanvasView
- Android 自定義 View 最少必要知識AndroidView