Android的自定義View深入解析

前言

自定義View是每個Android開發人員,都必備的技能。當SDK提供的常規控件如TextView、Button等沒法滿足我們日常開發需求時候,就需要我們進行View的自定義。本文就從View的繪製過程、自定義View的分類、自定義View的自定義屬性、Canvas的簡單使用、View的事件分發體系、View的滑動衝突等幾個方面,簡單講解一下,如何自定義一個我們想要的View。

一、View的繪製過程

    在我們進行自定義View之前,需要簡單瞭解一下,一個View是如何被繪製出來的。
    首先View體系中有兩個概念,View和ViewGroup,View是所有UI組件的基類,ViewGroup也是View派生出來的子類。然後,我們再講三個東西,WindowManager、DecorView、ViewRoot。通過這三個東西,我們就能對View的底層原理有個大概的瞭解了

WindowManager
是Android的GUI中非常重要的一環,他連接了手機屏幕和View,負責將View顯示在屏幕上,以及將屏幕接受到的各個事件,如點擊,滑動等,傳給界面最上層的DecorView,然後通過View的事件分發體系,分發到各個View中去。
DecorView
是整個手機屏幕最頂級的View,代表了整個屏幕,包含了三個部分,通知欄、標題欄、內容顯示欄,一般情況下它內部有一個豎直方向的LinerLayout,其分成了兩個部分,一個是上面的標題欄,另一個就是下面的內容顯示欄content。我們是給一個activity設置佈局時,setContentView()方法,其實就是將我們的佈局添加到了content中。
ViewRoot
連接DecorView和WindowManager的紐帶,負責他們兩個的交互。比如把WindowManger收到的事件傳遞給View等。

最後,我們來說說View的繪製過程,
View的繪製過程是從ViewRoot的peformTraversals方法發起的,該方法會分別調用測量寬高、確認在父容器中的位置、和繪製在屏幕上這三個過程。
View的measure方法,完成對View寬高的測量,然後會調用onMeasure,對它所有的子控件進行寬高的測量
View的layout方法,確認控件在父容器中的擺放位置,會調用onLayout方法,對它所有的子控件進行layout過程
View的draw方法,最終將控件繪製在屏幕上,它也會對它所有的子控件進行繪製
經過上面的三個步驟,我們就完成了View的繪製過程。

A、View的measure過程
該過程大致就是,通過解析得到的含測量模式的寬度和高度,該值由父容器的MeasureSpec和自身的LayoutParam來共同決定,根據測量模式的不同,對View的寬和高進行不同的賦值。
首先,解釋下幾種模式UNSPECIFIED、AT_MOST、EXACTLY :
如果是UNSPECIFIED模式,表示父容器沒有對該view進行任何限制,即要多大給多大
如果是AT_MOST模式(最大化),父容器指定了一個可用大小,View的大小不能大於這個值
如果是EXACTLY 模式(精確模式),父容器已經檢測出View所需要的精確大小。最終View大小就是這個值

View的測量模式和具體賦值可根據下表得來
<Image_1>
(注:第一列爲View的LayoutParam屬性,第一個行父容器的SpecMode)

上表可總結如下:
當View採用固定寬高時,不管父容器的模式是什麼,View的MeasureSize都是精確模式,並且其大小遵循Layoutparams中的大小
當View的寬高是match_parent時,如果父容器的模式是精確模式,那麼View也是精確模式並且大小是父容器的剩餘空間,如果父容器
是最大化模式,那麼View也是最大化模式,並且大小不能超過父容器的剩餘空間
當View的寬高是wrap_content時,不管父容器的模式是精確還是最大化,View的模式總是最大化並且大小不能超過超過父容器的剩餘
空間
備註:UNSPECIFIED模式,主要用於系統內部多少Measure的情形,一般來說我們不需要關注此模式

B、View的layout過程
該過程的作用就是確定View在父容器中的擺放位置,當ViewGroup確定了自己的擺放位置之後,會通過onMeasure方法依次確認遍歷所有子控件,並且通過layout方法確認子控件的擺放位置。但是需要注意的就是layout過程需要考慮View的屬性,比如如果是一個LinearLayout,就需要考慮是水平的還是豎直的,還有pading和margin值等。

C、View的draw過程
View的draw過程(View的draw方法),就是講界面繪製在屏幕上面,它遵循以下幾步:
1、繪製背景  background.draws(canvas)
2、繪製自己  onDraw
3、繪製children  dispathDraw
通過調用dispathDraw,依次對子控件進行draw
4、繪製裝飾 onDrawForeground 
一個控件可能包括了滾動條等裝飾,該方法就是繪製View的裝飾物的。
如果我們需要自定義控件的話,就可以在draw過程中,通過Canvas(畫板)、Paint(畫筆)等進行繪製 。

二、自定義View的分類

在上面,我們講解了一個View是如何繪製出來的,第二部分我們講解一下自定義View的分類。其分類方式有很多,個人比較傾向於以下這種。
 A、繼承View重寫onDraw方法
這種方法主要用於實現一些不規則的效果,即某些效果不方便通過佈局的組合方式來達到,往往需要靜態或者動態地顯示一些不規則的圖形
這時候就需要通過繪製的方式來實現,即重寫onDraw。
 B、繼承ViewGroup派生特殊的Layout
這種方法主要用於實現自定義的佈局,當某種效果看起來很像幾個View組合做一起的時候,可以採用這種方 法來實現。採用這種方式較複雜,需要
合適地處理ViewGroup的測量,佈局這兩個過程,並同時處理子元素的 測量和佈局過程。
 C、繼承特定的View
這種方法比較常見,一般是用於擴展某種已有的View的功能,比如TextView,這種方法比較容易實現
 D、繼承特定的ViewGroup
這種方法也比較常見,指將已有的view控件,進行組合來實現想要的效果,比如在一個Linerlayout中
所有的自定義控件,都能劃入這四類中來,比如做一個視頻播放器,我們需要自定義一個控件MyPlayer,讓他實現SurfaceView控件,他就是屬於第三
類,或者我們寫一個帶清除功能的EditText,我們可以通過一個EditText和一個ImageView進行組合的方式來實現,這種屬於第四種。

三、View的自定義屬性

我們在xml中使用傳統的控件時,可以很方便的設置這個View的一些屬性
   <TextView
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_marginLeft="15dp"
         android:text="Hello World"
         android:textColor="#333333"
         android:textSize="15sp" />

如上代碼,我們在xml中寫了一個控件TextView,並且設置了它的屬性如width、height、text、textSize等,那我們自己定義一個View,如何實現這種可以直接在xml佈局中設置View屬性呢。
其實自定義屬性並不複雜,我們可以通過以下幾個步驟,來完成View的自定義屬性。

A、res文件夾下的values文件夾中下建立一個xml,比如attr.xml,然後在這個xml裏面自定義一個屬性集合,就是存放指定控件的自定義屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
        <attr name="fontColor" format="enum">
        <enum name="blue" value="1"/>
        <enum name="red" value="2"/>
   </attr>
    </declare-styleable>
</resources>

上述代碼就創建了一個名爲CircleView的自定義屬性的集合,然後在這個集合裏面創建我們想要設置的自定義屬性項,比如circle_color就定義了一個名
爲circle_color,類型爲color的自定義屬性。這裏的類型包括有reference(資源id)、dmension指尺寸、string、integer、color等,上述代碼也創建了一
個枚舉類型的屬性fontColor,開發者在設置xml屬性的時候,可以在我們規定的範圍內進行選擇。

B、在我們的自定義View中,對這些屬性進行讀取和解析
public MyView(Context context, AttributeSet attrs) { 
        super(context, attrs); 
        //第二個參數就是我們在styles.xml文件中的<declare-styleable>標籤 
        //即屬性集合的標籤,在R文件中名稱爲R.styleable+name 
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView); 
        //第一個參數爲屬性集合裏面的屬性,R文件名稱:R.styleable+屬性集合名稱+下劃線+屬性名稱 
        //第二個參數爲,如果沒有設置這個屬性,則設置的默認的值 
        mFontColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); 
        //最後記得將TypedArray對象回收 
        a.recycle(); 
 } 

C、 然後就是在xml文件中使用自定義屬性
首先爲了可以使用自定義屬性,我們需要添加schems聲明。
 	xmlns:app="http://schemas.android.com/apk/res-auto"
這樣我們才能知道去資源文件中找自定義好的屬性集合,然後就可以使用自定義屬性了
<com.example.lenovo.myapplication.MyView
        app:circle_color="#333333"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
通過以上的步驟,我們就完成了一個View的自定義屬性。怎麼樣?是不是很簡單?

四、Canvas的簡單使用

我們在自定義View的分類以及View的繪製過程中講到了通過Canvas和Paint來繪製我們想要的View。這一部分,我們就簡單來介紹一下Canvas在自定義View中的簡單實用。
在自定義View的時候,我們可以通過draw方法獲取到Canvas對象,所以不需要自己new對象了
 @Override
 protected void onDraw(Canvas canvas) {
        
 }
可以通過查看api的方法的知,canvas能繪製的對象有弧線(arcs)、填充顏色(argb和color)、Bitmap、圓(circle和oval)、點(point)、線(line)、矩形(Rect)
、圖片(Picture)、圓角矩形(RoundRect)、文本(text)、頂點(Vertices)、路徑(Path)。我們可以通過組合這些對象畫出我們想要的圖形出來。但是光有這
些還不夠,當我們想操作Canvas進行位置轉換時,可以通過它提供的(rorate、scale、translate、skew(扭曲))等方法來完成一些操作。也可以通過
getMatrix方法直接操作矩陣(關於矩陣操作,就涉及到另一個知識點了,因爲動畫裏面也會涉及到這部分的知識點,所以詳見另一片筆記)。

做進行canvas的使用前,首先我們要知道RectF是做什麼用的 RectF其實就是通過left、top、right、bottom四個座標點來確定
一個矩形

下面就進行一些canvas的簡單用法
A、比如畫一個圓
@Override
    protected void onDraw(Canvas canvas) {
        Paint paint = new Paint();//new一個畫筆對象,並且設置爲藍色
        paint.setColor(Color.BLUE);
        canvas.drawCircle(100,100,100,paint);//通過canvas畫圓
    }
效果如下
<Image_2>

B、比如畫一段弧形
@Override
    protected void onDraw(Canvas canvas) {
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        RectF rectF = new RectF(0,0,100,100);//確定一個100 * 100的矩形
        canvas.drawArc(rectF,//在這個矩形範圍內畫一段圓弧
                0,//開始角度
                90,//掃過的角度
                true,//是否使用中心
                paint);
    }
效果如下
<Image_3>

C、通過path繪製一個五角星
    @Override
    protected void onDraw(Canvas canvas) {
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        Path path = new Path();
        path.moveTo(131,5);
        path.lineTo(88.357f,63.725f);
        path.lineTo(19,42);
        path.lineTo(62.002f,100);
        path.lineTo(19,159);
        path.lineTo(88.357f,136.275f);
        path.lineTo(131,195);
        path.lineTo(131,122.419f);
        path.lineTo(200,100);
        path.lineTo(131,77.581f);
        path.lineTo(131,5);
        canvas.drawPath(path,paint);
    }
效果如下
<Image_4>

通過上述Canvas的介紹,我們就可以通過Canvas和Paint來繪製我們想要樣子的自定義控件了。更多更復雜的用法,請大家自己去查詢相關資料。

五、View的事件分發體系

View的事件分發是核心知識點,更是難點。所謂的事件就是指MotionEvent,當一個MotionEvent產生了之後,系統需要把這個事件傳遞給一個具體的
View,而這個傳遞的過程就是事件的分發。事件的分發由三個很重要的方法來完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。

public boolean  dispatchTouchEvent(MotionEvent event):
該方法用來進行事件的分發,如果該事件能傳遞到當前View,那麼此方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatch
TouchEvent方法的影響,表示是否消耗該事件

public boolean onInterceptTouchEvent(MotionEvent event):
在上述方法內部調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列中,此方法不會被再次
調用,返回結果表示
是否攔截當前事件。

public boolean onTouchEvent(MotionEvent  event):
在dispatichTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,
當前View無法再次接
收到事件
他們的關係可以用下列僞代碼來體現
public boolean dispatchTouchEvent(MotionEvent event) {
        boolean consume = false;
        if (onInterceptTouchEvent(event)){
            consume = onTouchEvent(event);
        }else {
            consume = child.dispatchTouchEvent(event);
        }
        return consume;
  }
事件傳遞的大致規則如下:
對於一個根ViewGroup來說,點擊事件產生後,首先會傳給它,此時它的dispatchTouchEvent就會被調用,如果這個ViewGroup的onInterceptTouchEvent方法返回true,就表示它要攔截當前事件,接着事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被調用;如
果這個ViewGroup的oinInterceptTouchEvent方法返回false,就表示它不攔截當前事件,這時當前事件就會繼續傳遞給它的子元素,接着子元素的
dispatchToucEvent方法就會被調用,如此反覆直到事件被最終處理。如果根ViewGroup的所有子View都不處理事件,即onTouchEvent返回false,那麼
它的父容器的onTouchEvent將會倍調用,以此類推,如果所有的View都不處理這個事件,那麼這個事件將會最終傳遞給Activity。

一些關鍵點:
1、同一事件序列:是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結束,在這個過程中所產生的一系列事件,這個事件序
列以down事件開始,中間含有數量不定的move事件,最終以up事件結束。

2、ViewGroup默認不攔截任何事件,onInterceptTouchEvent默認返回false。

3、View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它,那麼它的onTouchEvent方法就會被調用。

4、事件的傳遞過程總是先傳遞給父元素,然後再由父元素分發給子View,通過requestDisallowInterceptTouchEvent方法,可以在子
元素中干預父元素
的事件分發過程,但是ACTION_DOWN事件除外,如果DOWN事件被攔截了,那麼該事件序列的其他事件都不會再傳遞下去。

總結

到這裏,本篇文件關於自定義View的內容,就講解的差不多了。當然,本篇文章只是比較簡單的從跟自定義View密切相關的幾個方面,對View體系進行
了一番講解,如想自定義View必須先了解View的繪製過程,然後你得明白常見的自定義View是如何分類,當我們對我們想要的自定義控件進行分類之後
,就能比較清晰的找到一個實現思路,比如是通過Canvas去畫這個控件的樣子、還是通過控件的組合。然後,你的自定義控件肯定需要自己的自定義屬
性,這樣便於xml中直接使用。再然後,你得學會一點Canvas的簡單使用。這樣便於你繪製自己想要的View的形狀。再然後,你通過常用控件組裝起自
己的自定義View時,你得知道點擊、滑動等事件是如何進行分發的。這樣才能清楚的讓所有子控件各司其職,不會發生錯亂。

以上就是本文的全部內容,View的繪製、自定義View的分類、View的自定義屬性、Canvas的簡單使用、View的事件分發體系。本文所有內容來自於自己
平時的總結以及其他人分享的經驗,感謝所有無私奉獻的同行。本文並沒有對View的繪製、Canvas的使用等內容進行更深入的解析,如果有這方面需求
的,請查閱相關內容。
因個人水平水平有限,難免有不足和錯誤之處,請大家指正。謝謝。。。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章