Android 控制元件架構與自定義控制元件詳解

lee_lgw發表於2021-09-09

架構:

·         PhoneWindow 將一個 DecorView 設定為整個應用視窗的根 View,這裡面所有 View 的監聽事件,都透過 WindowManagerService 來接收。DecorView 分為 TitleView 和 ContentView,ContentView 是一個 ID 為 content 的 FrameLayout

·         在 onCreate() 方法中呼叫 setContentView() 方法後,ActivityManagerService 會回撥onResume() 方法,此時系統才會把整個 DecorView 新增到 PhoneWindow 中,並讓其顯示出來,從而完成最終的介面繪製。

View 的測量:

測量 View 的類:MeasureSpec 類,它是一個32位的 int 值,高兩位為測量模式,低30位為測量大小,使用位運算提高並最佳化效率。

重寫 onMeasure() 後,最終要做的是把測量後的寬高值作為引數設定給 setMeasureDimension() 方法。

[程式碼]java程式碼:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){

    setMeasureDimension(measureWidth(widthMeasureSpec),   measureHeight(heightMeasureSpec));

}

 

//可作為模板程式碼!

private int measureWidth(int measureSpec){

    int result = 0;

    int specMode =   MeasureSpec.getMode(measureSpec);

    int specSize =   MeasureSpec.getSize(measureSpec);

    if(specMode ==   MeasureSpec.EXACTLY){//精確值模式,指定具體數值

        result   = specSize;

    }else{

        result   = 200;//先設定一個預設大小

        //最大值模式,layout_width 或   layout_height 為 wrap_content 時,控制元件大小隨控制元件的內容變化而變化,此時控制元件尺寸只要不超過父控制元件允許的最大尺寸即可。

        if(specMode   == MeasureSpec.AT_MOST){

            result   = Math.min(result, specSize);//取出我們指定的大小和 specSize 中最小的一個來作為最後的測量值

        }

        //MeasureSpec.UNSPECIFIED   不指定其大小,View 想多大就多大

    }

    return result;

}

 

即,如果不重寫 onMeasure() 方法,系統則會不知道該預設多大尺寸,就會預設填充整個父佈局,所以,重寫 onMeasure() 方法的目的,就是為了能夠給 View 一個 wrap_content 屬性下的預設大小

View 的繪製

onDraw() 中的引數,就是 Canvas 物件,使用該物件進行繪圖,而在其他地方,則需要 new 出該物件:

[程式碼]java程式碼:

1

Canvas canvas = new Canvas(bitmap);

 

傳進去的 bitmap 是與這個 bitmap 建立的 Canvas 畫布緊密聯絡的,這個過程稱為裝載畫布。該 bitmap 用來儲存所有繪製在 Canvas 上的畫素資訊。所有的 Canvas.drawXXX 方法都發生在這個 bitmap 上。

[程式碼]java程式碼:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

@Override

protected void onDraw(Canvas canvas){

    //...

    //在 onDraw 方法中繪製兩個 bitmap

    canvas.drawBitmap(bitmap1, 0,   0, null);

    canvas.drawBitmap(bitmap2, 0,   0, null);

    //...

}

private void otherMethod(){

    //將 bitmap2 裝載到另一個 Canvas 物件中

    Canvas mCanvas = new Canvas(bitmap2);

    //其他地方使用 Canvas 物件的繪圖方法在裝載   bitmap2 的 Canvas 物件上進行繪圖

    mCanvas.drawXXX

}

 

透過 mCanvas 將繪製效果作用在了 bitmap2 上,再重新整理 View 的時候,就會發現透過 onDraw()方法畫出來的 bitmap2 已經改變,因為 bitmap2 承載了在 mCanvas 上所進行的繪圖操作。我們沒有將圖形直接繪製在 onDraw() 方法制定的那塊畫布上,而是透過改變 bitmap,讓 View 重繪,從而顯示改變之後的 bitmap

ViewGroup 的測量

當 ViewGroup 的大小為 wrap_content 時,ViewGroup 需要對子 View 進行遍歷,以便獲得所有子 View 大小從而決定自己的大小,即呼叫子 View 的 Measure 方法來獲得每一個子 View 的測量結果。

子 View 測量完畢後,ViewGroup 執行 Layout 過程時,同樣是遍歷呼叫子 View 的 Layout 方法,並指定其具體顯示的位置,從而來決定其佈局位置。

自定義 View

[程式碼]java程式碼:

1

2

3

4

5

6

@Override

protected void onDraw(Canvas canvas){

    //在回撥父類方法前,對 TextView 來說是在繪製文字內容之前,實現邏輯

    super.onDraw(canvas);

    //之後,繪製文字之後

}

 

在 attrs.xml 中透過使用標籤宣告使用了自定義屬性,使用如下程式碼獲得在佈局檔案中自定義的那些屬性

[程式碼]java程式碼:

1

2

3

4

TypedArray ta = context.obtainStyledAttributes(attrs,   R.styleable.TopBar);

mLeftColor =   ta.getColor(R.styleable.TopBar_leftTextColor, 0);

//完成資源回收,避免重新建立的時候的錯誤

ta.recycle();

 

自定義 ViewGroup

重寫 onMeasure() 來對子 View 進行測量,重寫 onLayout() 確定子 View 位置,重寫onTouchEvent() 增加響應事件。

例項需求:自定義 ViewGroup 實現類似 ScrollView 上下滑動,同時增加粘性效果。即,當一個子 View 向上滑動大於一定距離後,鬆開將自動上滑,顯示下一個子 View,否則回到原始位置。

步驟一:先實現類似 ScrollView 功能:

[程式碼]java程式碼:

01

02

03

04

05

06

07

08

09

10

@override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){

    super.onMeasure(widthMeasureSpec,   heightMeasureSpec);

    int count = getChildCount();

    //遍歷通知子 View 對其自身進行測量

    for(int i = 0;i

        View   childView = getChildAt(i);

        measureChild(childView,   widthMeasureSpec, heightMeasureSpec);

    }

}

 

步驟二:再對子 View 進行放置位置設定,讓每個子 View 都顯示完整的一屏。所以,本例中ViewGroup 的高度就是子 View 的個數乘以螢幕高度,然後遍歷設定每個子 View 放置的位置

[程式碼]java程式碼:

1

2

3

4

5

6

7

8

9

@Override

protected void onLayout(boolean changed, int l, int t,   int r, int b){

    int childCount =   getChildCount();

    //設定 ViewGroup 高度

    MarginLayoutParams mlp =   (MarginLayoutParams)getLayoutParams();

    mlp.height =   mScreenHeight.childCount;

    setLayoutParams(mlp);

    //修改子 View 的 top 和 bottom 屬性,使它們依次排列

    for(int i=0; i;>

 

步驟三:

[程式碼]java程式碼:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

@Override

public boolean onTouchEvent(MotionEvent event){

    int y = (int)event.getY();

    switch(event.getAction()){

        case MotionEvent.ACTION_DOWN:

            mLastY   = y;

            mStart   = getScrollY();//記錄按下位置

            break;

        case MotionEvent.ACTION_MOVE:

            if(!mScroller.isFinished()){

                mScroller.abortAnimation();

            }

            int   dy = mLastY - y;

            if(getScrollY()  

                dy   = 0;

            }

            if(getScrollY()   >getHeight - mScreenHeight){

                dy   = 0;

            }

            scrollBy(0,   dy);//隨手指滾動 dy

            mLastY   = y;

            break;

        case MotionEvent.ACTION_UP:

            mEnd   = getScrollY();

            int   dScrollY = mEnd - mStart;

            if(dScrollY   > 0){//上滑

                if(dScrollY  

                    mScroller.startScroll(0,getScrollY(),   0, -dScrollY);

                }else{//大於,則滾動完剩餘的距離

                    mScroller.startScroll(0,getScrollY(),   0, mScreenHeight-dScrollY);

                }

            }else{//同理

                if(-dScrollY  

                    mScroller.startScroll(0,   getScrollY(), 0, -dScrollY);

                }else{

                    mScroller.startScroll(0,   getScrollY(), 0, -mScreenHeight - dScrollY);

                }

            }

            break;

    }

    postInvalidata();

    return true;

}

 

@Override

public void computeScroll(){

    super.computeScroll();

    if(mScroller.computeScrollOffset()){

        scrollTo(0,   mScroller.getCurrY());

        postInvalidate();

    }

}

 

事件攔截機制

點選 View 的 log:

[程式碼]java程式碼:

1

2

3

4

5

6

7

8

ViewGroupA dispatchTouchEvent

ViewGroupA onInterceptTouchEvent

ViewGroupB dispatchTouchEvent

ViewGroupB onInterceptTouchEvent

View dispatchTouchEvent

View onTouchEvent //last event, will back to parent

ViewGroupB onTouchEvent

ViewGroupA onTouchEvent

 

所以事件傳遞順序是:先執行 dispatchTouchEvent() 然後是 onInterceptTouchEvent()。返回值:True,攔截,不繼續;False,不攔截,繼續流程。初始返回是 false

事件處理順序是:onTouchEvent()。返回值:True,處理了,不稽核;False,給上級處理。初始返回是 false

即:

·         分發、攔截:如果某個 ViewGroup 直接使用 dispatchTouchEvent() 返回了 true ,則分發攔截結束,不再向其子 View 傳遞,則,直接執行該 ViewGroup 的 onTouchEvent(),然後繼續向上處理對應 ViewGroup 的 onTouchEvent()。

·         處理:如果某個 View 直接在 onTouchEvent() 中返回了 true。則上級不再執行onTouchEvent()。所有的處理在此結束。

原文連結:http://www.apkbus.com/blog-705730-61427.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1020/viewspace-2814781/,如需轉載,請註明出處,否則將追究法律責任。

相關文章