1. 程式人生 > >Android自定義view詳解

Android自定義view詳解

this boolean mar 處理 都是 並且 jdk text 命名

從繼承開始

懂點面向對象語言知識的都知道:封裝,繼承和多態,這是面向對象的三個基本特征,所以在自定義View的時候,最簡單的方法就是繼承現有的View

技術分享

通過上面這段代碼,我定義了一個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。

技術分享

Measure->Layout->Draw

在學會如何寫一個自定義控件之前,了解一個控件的繪制流程是必要的,在Android裏,一個view的繪制流程包括:Measure,Layout和Draw,通過onMeasure知道一個view要占界面的大小,然後通過onLayout知道這個控件應該放在哪個位置,最後通過onDraw方法將這個控件繪制出來,然後才能展現在用戶面前,下面我將挨個分析一下這三個方法的作用。

  • onMeasure 測量,通過測量知道一個一個view要占的大小,方法參數是兩個int型的值,我們都知道,在java中,int型由4個字節(32bit)組成,在MeasureSpce中,用前兩位表示mode,用後30位表示size

技術分享

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讓這個圓動起來,廢話不多說,直接上代碼:

技術分享

(只貼了核心代碼,完整代碼會在文章最後給鏈接)

可以看到在onDraw()方法裏,我調用了canvas的drawCircle方法畫了一個圓,圓心的位置是又畫布的位置決定的,位於畫布的中心,width和height參數是在onLayout()方法裏拿到的,在此之前取到的height和width都是不準確的,這點要註意。圓的半徑是xml文件中定義的size*scale,而這個scale是通過一個ValueAnimator確定的,變化範圍是從1到2,ValueAnimator的值發生改變會賦給scale同時調用postInvalidate()方法,這個方法的作用就是重繪,然後圓的半徑就會發生改變,這樣刷新就會實現動畫的效果。

技術分享

requstLayout和invalidate

在自定義view時,時常用到刷新view的方法,這時候就會有三個方法供我們選擇:requestLayout()、invalidate()、postInvalidate(),其實invalidate和postInvalidate這兩個方法作用是一樣的,唯一不同的是invalidate用在主線程,而postInvalidate用在異步線程,下面對比一下requestLayout和invalidate的內部實現:

技術分享

從代碼可以看出,其實這兩個方法內部都是調用的scheduleTraversals()方法,不同的是,requestLayout方法將mLayoutRequested標示置為true,scheduleTraversals這個方法以後找機會再細分析,現在只簡單說下結論,

  • requestLayout會調用measure和layout 等一系列操作,然後根據布局是否發生改變,surface是否被銷毀,來決定是否調用draw,也就是說requestlayout肯定會調用measure和layout,但不一定調用draw,讀者可以試著改下我上面寫的那個小程序,將postInvalidate改成requestlayout,動畫效果就消失了,因為布局沒有發生改變。

  • invalidate 只會調用draw,而且肯定會調,即使什麽都沒有發生改變,它也會重新繪制。

所以如果有布局需要發生改變,需要調用requestlayout方法,如果只是刷新動畫,則只需要調用invalidate方法。

技術分享

Android自定義view詳解