1. 程式人生 > >《Android群英傳》學習筆記之Android控制元件架構與自定義控制元件詳解

《Android群英傳》學習筆記之Android控制元件架構與自定義控制元件詳解

一、Android控制元件架構:

  • 控制元件大致分為兩類:ViewGroup控制元件與View控制元件。View是繪製在螢幕上的使用者能與之互動的一個物件。而ViewGroup則是一個用於存放其他View(和ViewGroup)物件的佈局容器。其中,View是所有UI元件的基類,而 ViewGroup是容納這些元件的容器,其本身也是從View派生出來的

    • (1)View:View物件是Android平臺中使用者介面體現的基礎單位,它是用來建立互動性的UI元件(如:按鈕文字框等等)的widgets的父類。它們提供了諸如文字輸入框和按鈕之類的UI物件的完整實。
    • (2)ViewGroup:ViewGroup繼承自View,是一種特殊的View,它可以裝其他的Views(或其他的ViewGroup)。ViewGroup是佈局(layouts)和views containers的父類。它的直接子類有: FrameLayout、GridLayout、LinearLayout等等。
    • 下面列出View及ViewGroup的所有子類
      • View派生出的直接子類有 ImageView,ProgressBar,TextView,ViewGroup,AnalogClock,KeyboardView,ViewStub,SurfaceView;
      • View派生出的間接子類有 Button,CheckBox,AbsoluteLayout,AdapterView,AdapterViewAnimator,AdapterViewFlipper,AppWidgetHostView,AutoCompleteTextView,CalendarView,CheckedTextView,Chronometer,AbsListView,AbsSeekBar,AbsSpinner,CompoundButton;
      • ViewGroup派生出的直接子類有 AbsoluteLayout,FrameLayout,LinearLayout,RelativeLayout,AdapterView,FragmentBreadCrumbs,SlidingDrawer;
      • ViewGroup派生出的間接子類有 ListView,GridView,AbsListView,AbsSpinner,AdapterViewAnimator,AdapterViewFlipper,AppWidgetHostView,CalendarView,DatePicker,DialerFilter,ExpandableListView,Gallery,GestureOverlayView,HorizontalScrollView,ImageSwitcher;
  • View樹結構 (eg:通常在Activty中使用的findViewById()方法就是在控制元件樹中以樹的深度優先遍歷來查詢對應元素) image

  • UI介面架構圖(DecorView被設定為整個應用視窗的根View) image

  • 標準檢視樹 image

  • 在程式碼中,當程式在onCreate()方法中呼叫setContentView()方法後,ActivityManagerService會回撥OnResume()方法,此時系統會把整個DecorView新增到PhoneWindow中,並讓其顯示出來

二、View/ViewGroup的測量與繪製

1、View的測量

  • onMeasure() 方法中測量,藉助MeasureSpec類來幫助測量View
  • 測量模式:
    • EXACTLY(精確值模式):指定數值(在onMeasure()方法中,如果不重寫方法,只能使用這種測量模式)
    • AT_MOST(最大值模式):例如wrap_content,只要不超過父控制元件允許的最大尺寸即可
    • UNSPECIFIED(不指定大小):通常在繪製自定義View時使用
  • 當View需要使用wrap_content屬性時,重寫onMeasure()方法,最終要做的工作就是把測量後的寬高作為引數設定給setMeasuredDimension(寬,高) 方法
  • 自定義測量值
    • 1.從MeasureSpec物件中提取出具體的測量模式和大小
      int specMode = MeasureSpec.getMode(measureSpec)
      int specSize = MeasureSpec.getSize(measureSpec)
      
    • 2.通過測量的模式給出不同的測量值

2、View的繪製

  • 系統2D繪圖API需使用Canvas物件繪製,Canvas像畫板,使用Paint就可以在上面作畫。
  • 通常需要繼承View並重寫的它的onDraw() 方法進行繪圖
  • 要在建立Canvas物件的時候同時傳入一個bitmap物件。因為傳進去的bitmap與通過這個bitmap建立的Canvas畫布是緊緊聯絡在一起的,這個過程稱之為裝載畫布。這個bitmap用來儲存所有繪製在Canvas上的畫素資訊。
        Canvas canvas = new Canvas(bitmap);
        
        //如果在onDraw()方法中,可將bitmap1再裝載到其他Canvas物件中,重新整理後會重新裝載畫布
        canvas.drawBitmap(bitmap1,0,0,null);
        
    

3、ViewGroup的測量:

  • ViewGroup在測量時通過遍歷所有子View,從而呼叫子View的Measure方法來獲得每一個子View的測量結果,前面所說的對View的測量,就是在這裡進行的。

4、ViewGroup的繪製:

  • ViewGroup如果不是被指定了背景色,通常不需要繪製。但是ViewGroup會使用dispatchDraw() 方法遍歷所有的子View,並通過呼叫子View來完成繪製工作。

三、自定義View

通常情況下,有以下三種方法來實現自定義控制元件

  • 對現有控制元件進行拓展
  • 通過組合來實現新的控制元件
  • 重寫View來實現全新的控制元件

1、對現有控制元件進行拓展

  • 一般來說,在onDraw() 方法中進行對原生控制元件的拓展
    @Override
    protected void onDraw(Canvas canvas) {
        //在回撥父類方法前,實現自己的邏輯,對TextView來說,即是在繪製文字內容前
        super.onDraw();
        //在回撥父類方法後,實現自己的邏輯,對TextView來說,即是在繪製文字內容後
    }
    
    • 程式呼叫super.onDraw(canvas) 方法來實現原生控制元件的功能。
    • 通過canvas、paint等物件實現邏輯
  • 一般在構造方法中完成物件的初始化工作,如初始化畫筆
    mPaint1 = new Paint();
    mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
    mPaint1.setStyle(Paint.Style.FILL);
    
  • 可通過getPaint() 方法獲取到當前繪製原生控制元件的Paint物件,然後對這個物件設定一些原生控制元件沒有的屬性

2、建立複合控制元件

  • 這種方式通常需要繼承一個合適的ViewGroup,再給他新增指定功能的控制元件,從而組合成新的複合控制元件

Ⅰ、定義屬性

確定屬性:在res資源目錄的values目錄下建立一個attrs.xml的屬性定義檔案

<resources>
    <declare-styleable name="TopBar">
        <attr name = "title" format = "string" />
        <attr name = "titeTextSize" format = "dimension" />
        <attr name = "titleTextColor" format = "color" />
        <attr name = "rightBackground" format = "reference|color"
        ......
    </declare-styleable>
</resources>
  • 通過 標籤聲明瞭使用自定義屬性,並通過name屬性來確定引用的名稱
  • 通過 標籤來宣告具體的自定義屬性,並通過format屬性來指定屬性的型別
  • 需要注意的是,有的屬性可以是顏色屬性,也可以是引用屬性。比如按鈕的背景,可以指定為具體的顏色,也可以把它指定為一張具體的圖片,所以使用“|”來分隔不同的屬性

獲取屬性

  • 系統提供了TypedArray這樣的資料結構來獲取自定義屬性集,然後通過TypedArray物件的getString()、getColor() 等方法,就可以獲取這些定義的屬性值。當獲取完所有屬性值後,需要呼叫TypedArray的recycle方法來完成資源的回收。
//通過這個方法,將在attrs.xml中定義的declare-styleable的所有屬性的值儲存到TypedArray中。
//第一個引數為xml名,第二個引數為style的name
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TopBar);

//從TypedArray中取出對應的值來為要設定的屬性賦值
mLeftTextColor = ta.getColor(R.styleable.TopBar_leftText,0);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftText);

//獲取玩TypedArray的值後,一般要呼叫recyle的方法來避免重新建立時的錯誤
ta.recycle();

Ⅱ、組合控制元件

  • 通過動態新增控制元件的方式,使用addView() 的方法將重置屬性後的系統控制元件放入自定義的模板中
//為元件元素設定相應的佈局元素
mLeftParams = new LayoutParams(LayoutParamas.WRAP_CONTENT, LayoutParamas.MATCH_PARENT);
mLeftParamas.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
//新增到ViewGroup
addView(mLeftButton, mLeftParams);
  • 實現自定義控制元件的點選事件

定義介面

//介面物件,實現回撥機制,在回撥方法中,通過對映的介面物件呼叫介面中的方法,而不用去考慮如何具體實現
public interface topbarClickListener {
    //左按點選事件
    void leftClick();
    //右按點選事件
    void rightClick();
}

暴露介面給呼叫者(使用者)

//按鈕的點選事件,不需要具體的實現
//秩序呼叫介面的方法,回撥的時候,會有具體的實現
mRightButton.setOnClickListener(new OnClickListener() {
   @Override
   public void onClick(View v) {
       mListener.rightClick();
   }
});

mLeftButton.setOnClickListener(new OnClickListener() {
   @Override
   public void onClick(View v) {
       mListener.leftClick();
   }
});

//暴露一個方法給呼叫者來註冊介面回撥
//通過介面來獲得回撥者對介面方法的實現
public void setOnTopbarClickListener(topbarClickListener mListener) {
    this.mListener = mListener;
}

實現介面回撥(定義具體實現者) 在呼叫者的程式碼中,呼叫者需要實現這樣一個介面,並完成介面中的方法,確定具體實現邏輯,並使用第二步中暴露的方法,將介面的物件傳遞進去,從而完成回撥。

mTopbar.setOnTopbarClickListener(
        new TopBar.topbarClickListener(){
        
            @Override
            public void rightClick(){
                Toast.makeText(TopBarTest.this, "right", Toast.LENGTH_SHORT).show();
            }
            
            @Override
            public void leftClick(){
                Toast.makeText(TopBarTest.this, "left", Toast.LENGTH_SHORT).show();
            }
            
        }
    )

Ⅲ、引用UI模板

  • 在引用前,需要指定引用第三方控制元件的名稱空間
    //假設將引入的第三方控制元件的名字空間取名為custom
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    
  • 使用自定義View與系統原生的View 的最大區別就是在申明控制元件時,需要指定完整的包名,而在引用自定義的屬性時,需要使用自定義的xmlns名字。
  • 通過include引用佈局檔案模板
    <include layout="@layout/topbar" />
    

3、重寫View來實現全新的控制元件

  • 建立一個自定義View,難點在於繪製控制元件和實現互動
    • 通常需要繼承View類,並重寫它的onDraw()、onMeasure()等方法來實現繪製邏輯。
    • 同時通過onTouchEvent()等觸控事件來實現互動邏輯。

四、自定義ViewGroup

  • ViewGroup存在的目的就是為了對其子View進行管理,為其子View新增顯示、響應的規則。
    • 通常需要重寫onMeasure() 方法對子View進行測量
    • 重寫onLayout() 方法來確定子View的位置
    • 重寫onTouchEvent() 方法來增加響應事件

五、事件攔截機制分析

  • Android為觸控事件封裝了一個類:MotionEvent
  • 觸控事件先按檢視樹的順序從上到下順序對事件進行分發、攔截,再按逆序進行事件處理或稽核。
    • 事件傳遞的返回值:True,攔截,不繼續;false,不攔截,繼續流程。
    • 事件處理的返回值:True,處理了,不用稽核了;false,給上級處理
  • ViewGroup與View的方法:
    • ViewGroup
      //事件分發
      public boolean dispatchTouchEvent(MotionEvent ev)
      //事件攔截
      public boolean onInterceptTouchEvent(MotionEvent ev)
      //事件稽核
      public boolean onTouchEvent(MotionEvent ev)
      
    • View(位於最底層,無事件攔截的方法)
      //事件分發
      public boolean dispatchTouchEvent(MotionEvent ev)
      //事件處理
      public boolean onTouchEvent(MotionEvent ev)