1. 程式人生 > >Android面試準備:自定義控制元件(二)

Android面試準備:自定義控制元件(二)

LayoutInflater介紹

  1. LayoutInflater的作用
    Android中使用LayoutInflater類來載入佈局(在Activity中常見的載入佈局方法setContentView()它的底層就是使用了LayoutInflater來載入佈局的。)
  2. LayoutInflater的基本用法
    1、獲取到LayoutInflater的例項(有兩種方式獲得LayoutInflater例項的方法)
//第一種
LayoutInflater layoutInflater = LayoutInflater.from(context);
//第二種(第一種方式其實是對第二種方式的封裝)
LayoutInflater layoutInflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

2、通過LayoutInflater的例項呼叫inflate()方法來載入佈局了

layoutInflater.inflate(resourceId, root); 

inflate()方法一般接收兩個引數,第一個引數就是要載入的佈局id,第二個引數是指給該佈局的外部再巢狀一層父佈局,如果不需要就直接傳null,返回值為載入佈局的View物件。

3、例如,將一個Button載入到LinearLayout佈局中:

public class MainActivity extends Activity {    
    private LinearLayout mainLayout;  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        mainLayout = (LinearLayout) findViewById(R.id.main_layout);  
        LayoutInflater layoutInflater = LayoutInflater.from(this
); View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null); mainLayout.addView(buttonLayout); } }

4、LayoutInflater廣泛應用於需要動態載入佈局的地方,譬如說我們建立的為ListView中每一個Item載入佈局的時候 + 我們平時使用的幾種常用的佈局其實它們都是有父佈局的,父佈局的名稱是FrameLayout,這就是為什麼我們平時簡單的佈局會有一行標題欄。

檢視View的繪製流程

任何一個檢視要顯示在螢幕上,都要經過相應的繪製流程後才能顯示出來。檢視的繪製流程主要涉及到的方法有:measure方法,用於測量檢視的大小,也就是說父檢視會根據onMeasure這個方法來測量(計算)子檢視的大小;還有就是layout方法,用於給檢視佈局的,也就是確定檢視在父檢視中的位置;最後的話會呼叫draw方法繪製檢視。
1. onMeasure():就是用於測量檢視的大小,也就是說父檢視會根據onMeasure這個方法來測量(計算)子檢視的大小。
1、View的繪製流程首先會呼叫measure()方法。measure()方法接收兩個引數,widthMeasureSpec和heightMeasureSpec,這兩個值分別用於確定檢視的寬度、高度的規格(specMode)和大小(pecSize)
2、,widthMeasureSpec和heightMeasureSpec這兩個值又是從哪裡得到的呢?通常情況下,這兩個值都是由父檢視經過計算後傳遞給子檢視的,說明父檢視會在一定程度上決定子檢視的大小。
3、measure()方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
    if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||  
            widthMeasureSpec != mOldWidthMeasureSpec ||  
            heightMeasureSpec != mOldHeightMeasureSpec) {  
        mPrivateFlags &= ~MEASURED_DIMENSION_SET;  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);  
        }  
        onMeasure(widthMeasureSpec, heightMeasureSpec);  
        if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {  
            throw new IllegalStateException("onMeasure() did not set the"  
                    + " measured dimension by calling"  
                    + " setMeasuredDimension()");  
        }  
        mPrivateFlags |= LAYOUT_REQUIRED;  
    }  
    mOldWidthMeasureSpec = widthMeasureSpec;  
    mOldHeightMeasureSpec = heightMeasureSpec;  

①、measure()這個方法是final的,因此我們無法在子類中去重寫這個方法,說明Android是不允許我們改變View的measure框架的。然後在第9行呼叫了onMeasure()方法,這裡才是真正去測量並設定View大小的地方,預設會呼叫getDefaultSize()方法來獲取檢視的大小。
②、當然,一個介面的展示可能會涉及到很多次的measure,因為一個佈局中一般都會包含多個子檢視,每個檢視都需要經歷一次measure過程。
③、onMeasure()方法是可以重寫的,也就是說,如果你不想使用系統預設的測量方式,可以按照自己的意願進行定製
④、檢視大小的控制是由父檢視、佈局檔案、以及檢視本身共同完成的,父檢視會提供給子檢視參考的大小,而開發人員可以在XML檔案中指定檢視的大小,然後檢視本身會對最終的大小進行排版;
2. onLayout()方法:用於給檢視佈局的,也就是確定檢視的位置。
1、ViewRoot再測量好檢視大小之後,接著會呼叫View的layout()方法來指定檢視的位置
2、layout根據一個初始座標點 + measure方法中測量的檢視的高度和寬度來確定檢視在父檢視中的的位置

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight); 

3、View中的onLayout()方法就是一個空方法,因為onLayout()過程是為了確定檢視在佈局中所在的位置,而這個操作應該是由佈局來完成的,即父檢視決定子檢視的顯示位置
4、ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味著所有ViewGroup的子類都必須重寫這個方法。像LinearLayout、RelativeLayout等佈局,都是重寫了這個方法,然後在內部按照各自的規則對子檢視進行佈局的。
3. onDraw:在這裡才真正地開始對檢視進行繪製
1、measure和layout的過程都結束後,接下來就進入到draw的過程了。
2、ViewRoot中的程式碼會繼續執行並創建出一個Canvas物件,然後呼叫View的draw()方法來執行具體的繪製工作。draw()方法內部的繪製過程總共可以分為六步,其中第二步和第五步在一般情況下很少用到,因此這裡我們只分析簡化後的繪製過程。

public void draw(Canvas canvas) {  
    if (ViewDebug.TRACE_HIERARCHY) {  
        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);  
    }  
    final int privateFlags = mPrivateFlags;  
    final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&  
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  
    mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;  
    // Step 1, draw the background, if needed  
    int saveCount;  
    if (!dirtyOpaque) {  
        final Drawable background = mBGDrawable;  
        if (background != null) {  
            final int scrollX = mScrollX;  
            final int scrollY = mScrollY;  
            if (mBackgroundSizeChanged) {  
                background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);  
                mBackgroundSizeChanged = false;  
            }  
            if ((scrollX | scrollY) == 0) {  
                background.draw(canvas);  
            } else {  
                canvas.translate(scrollX, scrollY);  
                background.draw(canvas);  
                canvas.translate(-scrollX, -scrollY);  
            }  
        }  
    }  
    final int viewFlags = mViewFlags;  
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;  
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;  
    if (!verticalEdges && !horizontalEdges) {  
        // Step 3, draw the content  
        if (!dirtyOpaque) onDraw(canvas);  
        // Step 4, draw the children  
        dispatchDraw(canvas);  
        // Step 6, draw decorations (scrollbars)  
        onDrawScrollBars(canvas);  
        // we're done...  
        return;  
    }  
}  

具體每一步執行的流程為:
①、繪製檢視背景
③、對檢視的內容進行繪製。可以看到,這裡去呼叫了一下onDraw()方法,這也是一個空方法,在自定義控制元件的時候可能要重寫這個方法
④、繪製子檢視
⑥、繪製滾動條(檢視都有這個滾動條,只是沒有顯示而已)
3、onDraw方法主要是依靠canvas、paint物件來繪製View。

檢視狀態及重繪流程分析

  1. 檢視狀態
    根據檢視的不同狀態定製不同的效果。譬如我們常見的一個按鈕點選之後,有點選的效果,這就是因為根據不同的檢視狀態設定不同的背景圖片。
    1、幾種常見的檢視狀態
    ①、enabled狀態:當前檢視是否可用,可用就代表能響應onTouch事件。
    ②、 focused狀態:表示當前檢視是否獲得到焦點
    ③、 selected狀態:表示當前檢視是否處於選中狀態
    ④、 pressed狀態:表示當前檢視是否處於按下狀態
  2. 我們可以在專案的drawable目錄下建立一個selector檔案:compose_bg.xml,在這裡配置每種狀態下檢視對應的背景圖片。
<selector xmlns:android="http://schemas.android.com/apk/res/android">  
    <item android:drawable="@drawable/compose_pressed" android:state_pressed="true"></item>  
    <item android:drawable="@drawable/compose_pressed" android:state_focused="true"></item>  
    <item android:drawable="@drawable/compose_normal"></item>  
</selector>  

1、當檢視處於正常狀態的時候就顯示compose_normal這張背景圖,當檢視獲得到焦點或者被按下的時候就顯示compose_pressed這張背景圖。
2、在佈局檔案中使用

    <Button   
        android:id="@+id/compose"  
        android:layout_width="60dp"  
        android:layout_height="40dp"  
        android:layout_gravity="center_horizontal"  
        android:background="@drawable/compose_bg"  
        />  
  1. 底層實現步驟:
protected void drawableStateChanged() {  
    //獲得selector物件
    Drawable d = mBGDrawable;  
    if (d != null && d.isStateful()) { 
    //獲取檢視狀態 + 根據不同的狀態更新檢視
        d.setState(getDrawableState());  
    }  
}

1、獲得selector物件
2、獲取檢視狀態
3、根據不同的狀態更新檢視

檢視重繪

假如要求動態更新檢視,譬如說之前檢視狀態的改變來顯示不同的背景這就涉及到檢視重繪操作。要對檢視進行重繪,可以呼叫invalidate()方法來實現。
1、根據檢視的狀態來改變檢視其實底層就是呼叫了invalidate()來實現檢視重繪的。
2、invalidate()方法雖然會呼叫到performTraversals()方法,也就是一個檢視的繪製流程方法,不過它底層不會再呼叫measure方法和layout方法了,而只是呼叫draw方法來重新繪製檢視。

自定義View的三種實現方式

  1. 繼承View類,重寫onDraw方法,呼叫invalidate方法重新繪製View(譬如說計數器)
  2. 組合控制元件,即將幾種控制元件組合起來形成一個新的控制元件,這個新的組合控制元件就會整合了原來每一個控制元件的功能(譬如說新浪微博中ListView第一行上面的狀態列)
  3. 繼承某一個控制元件,在該控制元件的基礎之上新增新的功能。(譬如說在ListView中每一個Item設定滑動監聽,顯示刪除按鈕)