什么是Fragment
對(duì)于Fragment的一些理解
前言
Fragment想必大家不陌生吧,在日常開發(fā)中,對(duì)于Fragment的使用也很頻繁,現(xiàn)在主流的APP中,基本的架構(gòu)也都是一個(gè)主頁,然后每個(gè)Tab項(xiàng)用Fragment做布局,不同選項(xiàng)做切換,使用起來也方便。但是否對(duì)它有足夠的認(rèn)識(shí)嗎,谷歌推薦用Fragment來代替Activity,但又沒有明確說為什么要用Fragment來代替Activity,這里就引發(fā)爭(zhēng)議了,那到底是要不要用,是否使用Fragment完全替換Activity真的比常規(guī)開發(fā)模式更好嗎?如果要用的話,那需要了解為何要使用Fragment,F(xiàn)ragment是什么,它的生命周期如何,如何使用,通信又是怎樣,有什么缺點(diǎn)嗎?帶著這些問題,我們一一去解讀。
目錄
- Fragment為何要用
- Fragment是什么
- Fragment生命周期
- Fragment怎么用
- Fragment通信
- Fragment是否很完美
Fragment為何要用
Fragment是Android 3.0 (Honeycomb)被引入的。主要目的是為了給大屏幕(如平板電腦)上更加動(dòng)態(tài)和靈活的UI設(shè)計(jì)提供支持。由于平板電腦的屏幕比手機(jī)的屏幕大很多,因此可用于組合和交換的UI組件的空間更大,利用Fragment實(shí)現(xiàn)此類設(shè)計(jì)的時(shí),就無需管理對(duì)視圖層次結(jié)構(gòu)的復(fù)雜更改。
通過將 Activity 布局分成片段,您可以在運(yùn)行時(shí)修改 Activity 的外觀,并在由 Activity 管理的返回棧中保留這些更改。如果僅僅只有Activity布局,那是不夠的,不僅在手機(jī)上有一套布局,同時(shí)在平板上還需要設(shè)計(jì)一套布局,那樣維護(hù)起來也麻煩,代碼上也有一定的冗余,對(duì)于APP包的大小也有一定的壓力。Fragment的優(yōu)勢(shì)是布局在不同設(shè)備上的適配。
比如:

從圖中我們可以看到,在平板中,一個(gè)Activity A包含了兩個(gè)Fragment,分別是Fragment A和Fragment B,但在手機(jī)中呢,就需要兩個(gè)Activity,分別是Activity A包含F(xiàn)ragment A和Activity B包含F(xiàn)ragment B。同時(shí)每個(gè)Fragment都具有自己的一套生命周期回調(diào)方法,并各自處理自己的用戶輸入事件。 因此,在平板中使用一個(gè)Activity 就可以了,左側(cè)是列表,右邊是內(nèi)容詳情。
除此之外,使用Fragment還有這么幾個(gè)方面優(yōu)勢(shì):
- 代碼復(fù)用。特別適用于模塊化的開發(fā),因?yàn)橐粋€(gè)Fragment可以被多個(gè)Activity嵌套,有個(gè)共同的業(yè)務(wù)模塊就可以復(fù)用了,是模塊化UI的良好組件。
- Activity用來管理Fragment。Fragment的生命周期是寄托到Activity中,F(xiàn)ragment可以被Attach添加和Detach釋放。
- 可控性。Fragment可以像普通對(duì)象那樣自由的創(chuàng)建和控制,傳遞參數(shù)更加容易和方便,也不用處理系統(tǒng)相關(guān)的事情,顯示方式、替換、不管是整體還是部分,都可以做到相應(yīng)的更改。
- Fragments是view controllers,它們包含可測(cè)試的,解耦的業(yè)務(wù)邏輯塊,由于Fragments是構(gòu)建在views之上的,而views很容易實(shí)現(xiàn)動(dòng)畫效果,因此Fragments在屏幕切換時(shí)具有更好的控制。
Fragment是什么
說了半天的Fragment,也看到這么多次Fragment這個(gè)名詞出現(xiàn),那么Fragment到底是什么東東呢?定義又是如何?
Fragment也可以叫為“片段”,但我覺得“片段”中文叫法有點(diǎn)生硬,還是保持叫Fragment比較好,它可以表示Activity中的行為或用戶界面部分。我們可以在一個(gè)Activity中用多個(gè)Fragment組合來構(gòu)建多窗格的UI,以及在多個(gè)Activity中重復(fù)使用某個(gè)Fragment。它有自己的生命周期,能接受自己的輸入,并且可以在 Activity 運(yùn)行時(shí)添加或刪除Fragment(有點(diǎn)像在不同 Activity 中重復(fù)使用的“子 Activity”)。
簡(jiǎn)單來說,F(xiàn)ragment其實(shí)可以理解為一個(gè)具有自己生命周期的控件,只不過這個(gè)控件又有點(diǎn)特殊,它有自己的處理輸入事件的能力,有自己的生命周期,又必須依賴于Activity,能互相通信和托管。
Fragment生命周期
如圖:

這張圖是Fragment生命周期和Activity生命周期對(duì)比圖,可以看到兩者還是有很多相似的地方,比如都有onCreate(),onStart(),onPause(),onDestroy()等等,因?yàn)镕ragment是被托管到Activity中的,所以多了兩個(gè)onAttach()和onDetach()。這里講講與Activity生命周期不一樣的方法。
onAttach()
Fragment和Activity建立關(guān)聯(lián)的時(shí)候調(diào)用,被附加到Activity中去。
onCreate()
系統(tǒng)會(huì)在創(chuàng)建Fragment時(shí)調(diào)用此方法??梢猿跏蓟欢钨Y源文件等等。
onCreateView()
系統(tǒng)會(huì)在Fragment首次繪制其用戶界面時(shí)調(diào)用此方法。 要想為Fragment繪制 UI,從該方法中返回的 View 必須是Fragment布局的根視圖。如果Fragment未提供 UI,您可以返回 null。
onViewCreated()
在Fragment被繪制后,調(diào)用此方法,可以初始化控件資源。
onActivityCreated()
當(dāng)onCreate(),onCreateView(),onViewCreated()方法執(zhí)行完后調(diào)用,也就是Activity被渲染繪制出來后。
onPause()
系統(tǒng)將此方法作為用戶離開Fragment的第一個(gè)信號(hào)(但并不總是意味著此Fragment會(huì)被銷毀)進(jìn)行調(diào)用。 通??梢栽诖朔椒▋?nèi)確認(rèn)在當(dāng)前用戶會(huì)話結(jié)束后仍然有效的任何更改(因?yàn)橛脩艨赡懿粫?huì)返回)。
onDestroyView()
Fragment中的布局被移除時(shí)調(diào)用。
onDetach()
Fragment和Activity解除關(guān)聯(lián)的時(shí)候調(diào)用。
但需要注一點(diǎn)是:除了onCreateView,其他的所有方法如果你重寫了,必須調(diào)用父類對(duì)于該方法的實(shí)現(xiàn)。
還有一般在啟動(dòng)Fragment的時(shí)候,它的生命周期就會(huì)執(zhí)行這幾個(gè)方法。

Fragment怎么用
前面介紹了半天,不耐煩的人會(huì)說,這么多廢話,也不見的到底是如何使用,畢竟我們是開發(fā)者,需要的使用方式,那么現(xiàn)在就來說說用法如何吧。兩種方式:靜態(tài)用法和動(dòng)態(tài)用法。
靜態(tài)用法
1、繼承Fragment,重寫onCreateView決定Fragemnt的布局
2、在Activity中聲明此Fragment,就當(dāng)和普通的View一樣
首先是布局文件:fragment1.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ff00" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is fragment 1"
android:textColor="#000000"
android:textSize="25sp" />
</LinearLayout>
可以看到,這個(gè)布局文件非常簡(jiǎn)單,只有一個(gè)LinearLayout,里面加入了一個(gè)TextView。我們?cè)傩陆ㄒ粋€(gè)fragment2.xml :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffff00" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is fragment 2"
android:textColor="#000000"
android:textSize="25sp" />
</LinearLayout>
然后新建一個(gè)類Fragment1,這個(gè)類是繼承自Fragment的:
public class Fragment1 extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment1, container, false);
}
}
可以看到,在onCreateView()方法中加載了fragment1.xml的布局。同樣fragment2.xml也是一樣的做法,新建一個(gè)Fragment2類:
public class Fragment2 extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment2, container, false);
}
}
然后打開或新建activity_main.xml作為主Activity的布局文件,在里面加入兩個(gè)Fragment的引用,使用android:name前綴來引用具體的Fragment:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false" >
<fragment
android:id="@+id/fragment1"
android:name="com.example.fragmentdemo.Fragment1"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/fragment2"
android:name="com.example.fragmentdemo.Fragment2"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
最后新建MainActivity作為程序的主Activity,里面的代碼非常簡(jiǎn)單,都是自動(dòng)生成的:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
現(xiàn)在我們來運(yùn)行一次程序,就會(huì)看到,一個(gè)Activity很融洽地包含了兩個(gè)Fragment,這兩個(gè)Fragment平分了整個(gè)屏幕,效果圖如下:

動(dòng)態(tài)用法
上面僅僅是Fragment簡(jiǎn)單用法,它真正強(qiáng)大部分是在動(dòng)態(tài)地添加到Activity中,那么動(dòng)態(tài)用法又是如何呢?
還是在靜態(tài)用法代碼的基礎(chǔ)上修改,打開activity_main.xml,將其中對(duì)Fragment的引用都刪除,只保留最外層的LinearLayout,并給它添加一個(gè)id,因?yàn)槲覀円獎(jiǎng)討B(tài)添加Fragment,不用在XML里添加了,刪除后代碼如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false" >
</LinearLayout>
然后打開MainActivity,修改其中的代碼如下所示:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Display display = getWindowManager().getDefaultDisplay();
if (display.getWidth() > display.getHeight()) {
Fragment1 fragment1 = new Fragment1();
getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment1).commit();
} else {
Fragment2 fragment2 = new Fragment2();
getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment2).commit();
}
}
}
看到了沒,首先,我們要獲取屏幕的寬度和高度,然后進(jìn)行判斷,如果屏幕寬度大于高度就添加fragment1,如果高度大于寬度就添加fragment2。動(dòng)態(tài)添加Fragment主要分為4步:
1.獲取到FragmentManager,在Activity中可以直接通過getFragmentManager得到。
2.開啟一個(gè)事務(wù),通過調(diào)用beginTransaction方法開啟。
3.向容器內(nèi)加入Fragment,一般使用replace方法實(shí)現(xiàn),需要傳入容器的id和Fragment的實(shí)例。
4.提交事務(wù),調(diào)用commit方法提交。
現(xiàn)在運(yùn)行一下程序,效果如下圖所示:


要想管理 Activity 中的片段,需要使用 FragmentManager。要想獲取它,需要 Activity 調(diào)用 getFragmentManager()。
使用 FragmentManager 執(zhí)行的操作包括:
- 通過 findFragmentById()(對(duì)于在 Activity 布局中提供 UI 的片段)或 findFragmentByTag()(對(duì)于提供或不提供 UI 的片段)獲取 Activity 中存在的片段
- 通過 popBackStack()將片段從返回棧中彈出
- 通過 addOnBackStackChangedListener() 注冊(cè)一個(gè)偵聽返回棧變化的偵聽器
也可以使用 FragmentManager 打開一個(gè) FragmentTransaction,通過它來執(zhí)行某些事務(wù),如添加和刪除片段。
Fragment通信
盡管 Fragment 是作為獨(dú)立于 Activity的對(duì)象實(shí)現(xiàn),并且可在多個(gè) Activity 內(nèi)使用,但Fragment 的給定實(shí)例會(huì)直接綁定到包含它的 Activity。具體地說,F(xiàn)ragment 可以通過 getActivity() 訪問 Activity實(shí)例,并輕松地執(zhí)行在 Activity 布局中查找視圖等任務(wù)。如:
View listView = getActivity().findViewById(R.id.list);
同樣地,Activity 也可以使用 findFragmentById() 或 findFragmentByTag(),通過從 FragmentManager 獲取對(duì) Fragment 的引用來調(diào)用Fragment中的方法。例如:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
創(chuàng)建對(duì) Activity 的事件回調(diào)
在某些情況下,可能需要通過與 Activity 共享事件。執(zhí)行此操作的一個(gè)好方法是,在Fragment 內(nèi)定義一個(gè)回調(diào)接口,并要求宿主 Activity 實(shí)現(xiàn)它。 當(dāng) Activity 通過該接口收到回調(diào)時(shí),可以根據(jù)需要與布局中的其他Fragment共享這些信息。
例如,如果一個(gè)新聞應(yīng)用的 Activity 有兩個(gè)Fragment ,一個(gè)用于顯示文章列表(Fragment A),另一個(gè)用于顯示文章(Fragment B)—,那么Fragment A必須在列表項(xiàng)被選定后告知 Activity,以便它告知Fragment B 顯示該文章。 在本例中,OnArticleSelectedListener 接口在片段 A 內(nèi)聲明:
public static class FragmentA extends ListFragment {
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
}
然后,該Fragment的宿主 Activity 會(huì)實(shí)現(xiàn) OnArticleSelectedListener 接口并替代 onArticleSelected(),將來自Fragment A 的事件通知Fragment B。為確保宿主 Activity 實(shí)現(xiàn)此界面,F(xiàn)ragment A 的 onAttach() 回調(diào)方法(系統(tǒng)在向 Activity 添加Fragment時(shí)調(diào)用的方法)會(huì)通過轉(zhuǎn)換傳遞到 onAttach() 中的 Activity 來實(shí)例化 OnArticleSelectedListener 的實(shí)例:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
}
}
}
如果 Activity 未實(shí)現(xiàn)界面,則片段會(huì)引發(fā) ClassCastException。實(shí)現(xiàn)時(shí),mListener 成員會(huì)保留對(duì) Activity 的 OnArticleSelectedListener 實(shí)現(xiàn)的引用,以便Fragment A 可以通過調(diào)用 OnArticleSelectedListener 界面定義的方法與 Activity 共享事件。例如,如果Fragment A 是 ListFragment 的一個(gè)擴(kuò)展,則用戶每次點(diǎn)擊列表項(xiàng)時(shí),系統(tǒng)都會(huì)調(diào)用Fragment中的 onListItemClick(),然后該方法會(huì)調(diào)用 onArticleSelected() 以與 Activity 共享事件:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
mListener.onArticleSelected(noteUri);
}
}
Fragment是否很完美
因?yàn)镕ragment是由FragmentManager來管理,每一個(gè)Activity有一個(gè)FragmentManager,管理著一個(gè)Fragment的棧,Activity是系統(tǒng)級(jí)別的,由系統(tǒng)來管理ActivityManager,棧也是系統(tǒng)范圍的。而Fragment則是每個(gè)Activity范圍內(nèi)的,所以在使用Fragment的時(shí)候也有幾點(diǎn)要注意。
- 同一個(gè)Activity中,只能有一個(gè)ID或TAG標(biāo)識(shí)的Fragment實(shí)例。
這很容易理解,同一個(gè)范圍內(nèi),有標(biāo)識(shí)的實(shí)例肯定是要唯一才行(否則還要標(biāo)識(shí)干嘛)這個(gè)在布局中經(jīng)常犯錯(cuò),在布局中寫Fragment最好不要加ID或者TAG,否則很容易出現(xiàn)不允許創(chuàng)建的錯(cuò)誤。我的原則是如果放在布局中,就不要加ID和TAG,如果需要ID和TAG就全用代碼控制。創(chuàng)建新實(shí)例前先到FragmentManager中查找一番,這也正是有標(biāo)識(shí)的意義所在。 - 一個(gè)Activity中有一個(gè)Fragment池,實(shí)例不一定會(huì)被銷毀,可能會(huì)保存在池中。
這個(gè)跟第一點(diǎn)差不多。就好比系統(tǒng)會(huì)緩存Activity的實(shí)例一樣,F(xiàn)ragmentManager也會(huì)緩存Fragment實(shí)例,以方便和加速再次顯示。 - FragmentManager的作用范圍是整個(gè)Activity,所以,某一個(gè)布局ID,不能重復(fù)被Fragment替換。
通常顯示Fragment有二種方式,一種是層疊到某個(gè)布局上,或者把某個(gè)布局上面的Fragment替換掉,但是這個(gè)布局不能出現(xiàn)二次,比如布局A中有ID為id的區(qū)域,要顯示為Fragment,此布局A,只能在一個(gè)Activity中顯示一個(gè),否則第二個(gè)id區(qū)域不能被Fragment成功替換。因?yàn)殡m有二個(gè)ID布局的實(shí)例,但I(xiàn)D是相同的,對(duì)FragmentManager來說是一樣的,它會(huì)認(rèn)為只有一個(gè),因?yàn)樗吹氖遣季值腎D,而不是布局的實(shí)例。 - Fragment的生命周期反應(yīng)Activity的生命周期。
Fragment在顯示和退出時(shí)會(huì)走一遍完整的生命周期。此外,正在顯示時(shí),就跟Activity的一樣,Activity被onPause,里面的Fragment就onPause,以此類推,由此帶來的問題就是,比如你在onStart()里面做了一些事情,那么,當(dāng)宿主Activity被擋住,又出現(xiàn)時(shí)(比如接了個(gè)電話),F(xiàn)ragment的onStart也會(huì)被高到,所以你要想到,這些生命周期不單單在顯示和退出時(shí)會(huì)走到。 - Fragment的可見性。
這個(gè)問題出現(xiàn)在有Fragment棧的時(shí)候,也就是說每個(gè)Fragment不知道自己是否真的對(duì)用戶可見。比如現(xiàn)在是Fragment A,又在其上面顯示了Fragment B,當(dāng)B顯示后,A并不知道自己上面還有一個(gè),也不知道自己對(duì)用戶不可見了,同樣再有一個(gè)C,B也不知。C退出后,B依然不知自己已在棧頂,對(duì)用戶可見,B退后,A也不知。也就是說Fragment顯示或者退出,棧里的其他Fragment無法感知。這點(diǎn)就不如Activity,a被b蓋住后,a會(huì)走到onStop(),同樣c顯示后,b也能通過onStop()感知。Fragment可以從FragmentManager監(jiān)聽BackStackState的變化,但它只告訴你Stack變了,不告訴你是多了,還是少,還有你處的位置。有一個(gè)解決方案就是,記錄頁面的Path深度,再跟Fragment所在的Stack深度來比較,如果一致,那么這個(gè)Fragment就在棧頂。因?yàn)槊總€(gè)頁面的Path深度是固定的,而Stack深度是不變化的,所以這個(gè)能準(zhǔn)確的判斷Fragment是否對(duì)用戶可見,當(dāng)然,這個(gè)僅針對(duì)整個(gè)頁面有效,對(duì)于布局中的一個(gè)區(qū)域是無效的。 - Fragment的事件傳遞。
對(duì)于層疊的Fragment,其實(shí)就相當(dāng)于在一個(gè)FrameLayout里面加上一堆的View,所以,如果處于頂層的Fragment沒處理點(diǎn)擊事件,那么事件就會(huì)向下層傳遞,直到事件被處理。比如有二個(gè)Fragment A和B,B在A上面,B只有TextView且沒處理事件,那么點(diǎn)擊B時(shí),會(huì)發(fā)現(xiàn)A里的View處理了事件。這個(gè)對(duì)于Activity也不會(huì)發(fā)生,因?yàn)槭录荒芸绱绑w傳播,上面的Activity沒處理事件,也不會(huì)傳給下面的Activity,即使它可見。解決之法,就是讓上面的Fragment的根布局吃掉事件,為每個(gè)根ViewGroup添加onClick=“true”。 - 與第三方Activity交互。與第三方交互,仍要采用Android的標(biāo)準(zhǔn)startActivityForResult()和onActivityResult()這二個(gè)方法來進(jìn)行。但對(duì)于Fragment有些事情需要注意,F(xiàn)ragment也有這二個(gè)方法,但是為了能正確的讓Fragment收到onActivityResult(),需要:
- 宿主Activity要實(shí)現(xiàn)一個(gè)空的onActivityResult(),里面調(diào)用super.onActivityResult()
- 調(diào)用Fragment#startActivityForResult()而不是用Activity的 當(dāng)然,也可以直接使用Activity的startActivityForResult(),那樣的話,就只能在宿主Activity里處理返回的結(jié)果了。
android自定義View
1、概述
Android****自定義View / ViewGroup****的步驟大致如下:
- 自定義屬性;
- 選擇和設(shè)置構(gòu)造方法;
- 重寫onMeasure()方法;
- 重寫onDraw()方法;
- 重寫onLayout()方法;
- 重寫其他事件的方法(滑動(dòng)監(jiān)聽等)。</pre>
2、自定義屬性
Android自定義屬性主要有定義、使用和獲取三個(gè)步驟。
2.1、定義自定義屬性
我們通常將自定義屬性定義在/values/attr.xml文件中(attr.xml文件需要自己創(chuàng)建)。
先來看一段示例代碼:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="rightPadding" format="dimension" />
<declare-styleable name="CustomMenu">
<attr name="rightPadding" />
</declare-styleable>
</resources>
可以看到,我們先是定義了一個(gè)屬性rightPadding,然后又在CustomMenu中引用了這個(gè)屬性。下面說明一下:
- 首先,我們可以在declare-stylable標(biāo)簽中直接定義屬性而不需要引用外部定義好的屬性,但是為了屬性的重用,我們可以選擇上面的這種方法:先定義,后引用;
- declare-stylable標(biāo)簽只是為了給自定義屬性分類。一個(gè)項(xiàng)目中可能又多個(gè)自定義控件,但只能又一個(gè)attr.xml文件,因此我們需要對(duì)不同自定義控件中的自定義屬性進(jìn)行分類,這也是為什么declare-stylable標(biāo)簽中的name屬性往往定義成自定義控件的名稱;
- 所謂的在declare-stylable標(biāo)簽中的引用,就是去掉了外部定義的format屬性,如果沒有去掉format,則會(huì)報(bào)錯(cuò);如果外部定義中沒有format而在內(nèi)部引用中又format,也一樣會(huì)報(bào)錯(cuò)。
常用的format類型:
- string:字符串類型;
- integer:整數(shù)類型;
- float:浮點(diǎn)型;
- dimension:尺寸,后面必須跟dp、dip、px、sp等單位;
- Boolean:布爾值;
- reference:引用類型,傳入的是某一資源的ID,必須以“@”符號(hào)開頭;
- color:顏色,必須是“#”符號(hào)開頭;
- fraction:百分比,必須是“%”符號(hào)結(jié)尾;
- enum:枚舉類型</pre>
下面對(duì)format類型說明幾點(diǎn):
- format中可以寫多種類型,中間使用“|”符號(hào)分割開,表示這幾種類型都可以傳入這個(gè)屬性;
- enum類型的定義示例如下代碼所示:
<resources>
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<declare-styleable name="CustomView">
<attr name="orientation" />
</declare-styleable>
</resources>
使用時(shí)通過getInt()方法獲取到value并判斷,根據(jù)不同的value進(jìn)行不同的操作即可。
2.2、使用自定義屬性
在XML布局文件中使用自定義的屬性時(shí),我們需要先定義一個(gè)namespace。Android中默認(rèn)的namespace是android,因此我們通??梢允褂谩癮ndroid:xxx”的格式去設(shè)置一個(gè)控件的某個(gè)屬性,android這個(gè)namespace的定義是在XML文件的頭標(biāo)簽中定義的,通常是這樣的:
xmlns:android="http://schemas.android.com/apk/res/android"
我們自定義的屬性不在這個(gè)命名空間下,因此我們需要添加一個(gè)命名空間。
自定義屬性的命名空間如下:
xmlns:app="http://schemas.android.com/apk/res-auto"
可以看出來,除了將命名空間的名稱從android改成app之外,就是將最后的“res/android”改成了“res-auto”。
注意:自定義namespace的名稱可以自己定義,不一定非得是app。
2.3、獲取自定義屬性
在自定義View / ViewGroup中,我們可以通過TypedArray獲取到自定義的屬性。示例代碼如下:
public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomMenu, defStyleAttr, 0); int indexCount = a.getIndexCount(); for (int i = 0; i < indexCount; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.CustomMenu_rightPadding:
mMenuRightPadding = a.getDimensionPixelSize(attr, 0); break;
}
}
a.recycle();
}
這里需要說明一下:
- 獲取自定義屬性的代碼通常是在三個(gè)參數(shù)的構(gòu)造方法中編寫的(具體為什么是三個(gè)參數(shù)的構(gòu)造方法,下面的章節(jié)中會(huì)有解釋);
- 在獲取TypedArray對(duì)象時(shí)就為其綁定了該自定義View的自定義屬性集(CustomMenu),通過getIndexCount()方法獲取到自定義屬性的數(shù)量,通過getIndex()方法獲取到某一個(gè)屬性,最后通過switch語句判斷屬性并進(jìn)行相應(yīng)的操作;
- 在TypedArray使用結(jié)束后,需要調(diào)用recycle()方法回收它。
3、構(gòu)造方法
當(dāng)我們定義一個(gè)新的類繼承了View或ViewGroup時(shí),系統(tǒng)都會(huì)提示我們重寫它的構(gòu)造方法。View / ViewGroup中又四個(gè)構(gòu)造方法可以重寫,它們分別有一、二、三、四個(gè)參數(shù)。四個(gè)參數(shù)的構(gòu)造方法我們通常用不到,因此這個(gè)章節(jié)中我們主要介紹一個(gè)參數(shù)、兩個(gè)參數(shù)和三個(gè)參數(shù)的構(gòu)造方法(這里以CustomMenu控件為例)。
3.1、一個(gè)參數(shù)的構(gòu)造方法
構(gòu)造方法的代碼:
public CustomMenu(Context context) { …… }
這個(gè)構(gòu)造方法只有一個(gè)參數(shù)Context上下文。當(dāng)我們?cè)贘AVA代碼中直接通過new關(guān)鍵在創(chuàng)建這個(gè)控件時(shí),就會(huì)調(diào)用這個(gè)方法。
3.2、兩個(gè)參數(shù)的構(gòu)造方法
public CustomMenu(Context context, AttributeSet attrs) { …… }
這個(gè)構(gòu)造方法有兩個(gè)參數(shù):Context上下文和AttributeSet屬性集。當(dāng)我們需要在自定義控件中獲取屬性時(shí),就默認(rèn)調(diào)用這個(gè)構(gòu)造方法。AttributeSet對(duì)象就是這個(gè)控件中定義的所有屬性。
我們可以通過AttributeSet對(duì)象的getAttributeCount()方法獲取屬性的個(gè)數(shù),通過getAttributeName()方法獲取到某條屬性的名稱,通過getAttributeValue()方法獲取到某條屬性的值。
注意:不管有沒有使用自定義屬性,都會(huì)默認(rèn)調(diào)用這個(gè)構(gòu)造方法,“使用了自定義屬性就會(huì)默認(rèn)調(diào)用三個(gè)參數(shù)的構(gòu)造方法”的說法是錯(cuò)誤的。
3.3、三個(gè)參數(shù)的構(gòu)造方法
public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) { …… }
這個(gè)構(gòu)造方法中有三個(gè)參數(shù):Context上下文、AttributeSet屬性集和defStyleAttr自定義屬性的引用。這個(gè)構(gòu)造方法不會(huì)默認(rèn)調(diào)用,必須要手動(dòng)調(diào)用,這個(gè)構(gòu)造方法和兩個(gè)參數(shù)的構(gòu)造方法的唯一區(qū)別就是這個(gè)構(gòu)造方法給我們默認(rèn)傳入了一個(gè)默認(rèn)屬性集。
defStyleAttr指向的是自定義屬性的<declare-styleable>標(biāo)簽中定義的自定義屬性集,我們?cè)趧?chuàng)建TypedArray對(duì)象時(shí)需要用到defStyleAttr。
3.4、三個(gè)構(gòu)造方法的整合
一般情況下,我們會(huì)將這三個(gè)構(gòu)造方法串聯(lián)起來,即層層調(diào)用,讓最終的業(yè)務(wù)處理都集中在三個(gè)參數(shù)的構(gòu)造方法。我們讓一參的構(gòu)造方法引用兩參的構(gòu)造方法,兩參的構(gòu)造方法引用三參的構(gòu)造方法。示例代碼如下:
public CustomMenu(Context context) { this(context, null);
} public CustomMenu(Context context, AttributeSet attrs) { this(context, attrs, 0);
} public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 業(yè)務(wù)代碼 }
這樣一來,就可以保證無論使用什么方式創(chuàng)建這個(gè)控件,最終都會(huì)到三個(gè)參數(shù)的構(gòu)造方法中處理,減少了重復(fù)代碼。
4、onMeasure()
onMeasure()方法中主要負(fù)責(zé)測(cè)量,決定控件本身或其子控件所占的寬高。我們可以通過onMeasure()方法提供的參數(shù)widthMeasureSpec和heightMeasureSpec來分別獲取控件寬度和高度的測(cè)量模式和測(cè)量值(測(cè)量 = 測(cè)量模式 + 測(cè)量值)。
widthMeasureSpec和heightMeasureSpec雖然只是int類型的值,但它們是通過MeasureSpec類進(jìn)行了編碼處理的,其中封裝了測(cè)量模式和測(cè)量值,因此我們可以分別通過MeasureSpec.getMode(xMeasureSpec)和MeasureSpec. getSize(xMeasureSpec)來獲取到控件或其子View的測(cè)量模式和測(cè)量值。
測(cè)量模式分為以下三種情況:
- EXACTLY:當(dāng)寬高值設(shè)置為具體值時(shí)使用,如100DIP、match_parent等,此時(shí)取出的size是精確的尺寸;
- AT_MOST:當(dāng)寬高值設(shè)置為wrap_content時(shí)使用,此時(shí)取出的size是控件最大可獲得的空間;
- UNSPECIFIED:當(dāng)沒有指定寬高值時(shí)使用(很少見)。</pre>
onMeasure()方法中常用的方法:
- getChildCount():獲取子View的數(shù)量;
- getChildAt(i):獲取第i個(gè)子控件;
- subView.getLayoutParams().width/height:設(shè)置或獲取子控件的寬或高;
- measureChild(child, widthMeasureSpec, heightMeasureSpec):測(cè)量子View的寬高;
- child.getMeasuredHeight/width():執(zhí)行完measureChild()方法后就可以通過這種方式獲取子View的寬高值;
- getPaddingLeft/Right/Top/Bottom():獲取控件的四周內(nèi)邊距;
- setMeasuredDimension(width, height):重新設(shè)置控件的寬高。如果寫了這句代碼,就需要?jiǎng)h除“super. onMeasure(widthMeasureSpec, heightMeasureSpec);”這行代碼。
注意:onMeasure()方法可能被調(diào)用多次,這是因?yàn)榭丶械膬?nèi)容或子View可能對(duì)分配給自己的空間“不滿意”,因此向父空間申請(qǐng)重新分配空間。
5、onDraw()
onDraw()方法負(fù)責(zé)繪制,即如果我們希望得到的效果在Android原生控件中沒有現(xiàn)成的支持,那么我們就需要自己繪制我們的自定義控件的顯示效果。
要學(xué)習(xí)onDraw()方法,我們就需要學(xué)習(xí)在onDraw()方法中使用最多的兩個(gè)類:Paint和Canvas。
注意:每次觸摸了自定義View/ViewGroup時(shí)都會(huì)觸發(fā)onDraw()方法。
5.1、Paint類
Paint畫筆對(duì)象,這個(gè)類中包含了如何繪制幾何圖形、文字和位圖的樣式和顏色信息,指定了如何繪制文本和圖形。畫筆對(duì)象右很多設(shè)置方法,大體上可以分為兩類:一類與圖形繪制有關(guān),一類與文本繪制有關(guān)。
Paint****類中有如下方法:
1、圖形繪制:
- setArgb(int a, int r, int g, int b):設(shè)置繪制的顏色,a表示透明度,r、g、b表示顏色值;
- setAlpha(int a):設(shè)置繪制的圖形的透明度;
- setColor(int color):設(shè)置繪制的顏色;
- setAntiAlias(boolean a):設(shè)置是否使用抗鋸齒功能,抗鋸齒功能會(huì)消耗較大資源,繪制圖形的速度會(huì)減慢;
- setDither(boolean b):設(shè)置是否使用圖像抖動(dòng)處理,會(huì)使圖像顏色更加平滑飽滿,更加清晰;
- setFileterBitmap(Boolean b):設(shè)置是否在動(dòng)畫中濾掉Bitmap的優(yōu)化,可以加快顯示速度;
- setMaskFilter(MaskFilter mf):設(shè)置MaskFilter來實(shí)現(xiàn)濾鏡的效果;
- setColorFilter(ColorFilter cf):設(shè)置顏色過濾器,可以在繪制顏色時(shí)實(shí)現(xiàn)不同顏色的變換效果;
- setPathEffect(PathEffect pe):設(shè)置繪制的路徑的效果;
- setShader(Shader s):設(shè)置Shader繪制各種漸變效果;
- setShadowLayer(float r, int x, int y, int c):在圖形下面設(shè)置陰影層,r為陰影角度,x和y為陰影在x軸和y軸上的距離,c為陰影的顏色;
- setStyle(Paint.Style s):設(shè)置畫筆的樣式:FILL實(shí)心;STROKE空心;FILL_OR_STROKE同時(shí)實(shí)心與空心;
- setStrokeCap(Paint.Cap c):當(dāng)設(shè)置畫筆樣式為STROKE或FILL_OR_STROKE時(shí),設(shè)置筆刷的圖形樣式;
- setStrokeJoin(Paint.Join j):設(shè)置繪制時(shí)各圖形的結(jié)合方式;
- setStrokeWidth(float w):當(dāng)畫筆樣式為STROKE或FILL_OR_STROKE時(shí),設(shè)置筆刷的粗細(xì)度;
- setXfermode(Xfermode m):設(shè)置圖形重疊時(shí)的處理方式;
2、文本繪制:
- setTextAlign(Path.Align a):設(shè)置繪制的文本的對(duì)齊方式;
- setTextScaleX(float s):設(shè)置文本在X軸的縮放比例,可以實(shí)現(xiàn)文字的拉伸效果;
- setTextSize(float s):設(shè)置字號(hào);
- setTextSkewX(float s):設(shè)置斜體文字,s是文字傾斜度;
- setTypeFace(TypeFace tf):設(shè)置字體風(fēng)格,包括粗體、斜體等;
- setUnderlineText(boolean b):設(shè)置繪制的文本是否帶有下劃線效果;
- setStrikeThruText(boolean b):設(shè)置繪制的文本是否帶有刪除線效果;
- setFakeBoldText(boolean b):模擬實(shí)現(xiàn)粗體文字,如果設(shè)置在小字體上效果會(huì)非常差;
- setSubpixelText(boolean b):如果設(shè)置為true則有助于文本在LCD屏幕上顯示效果;
3、其他方法: - getTextBounds(String t, int s, int e, Rect b):將頁面中t文本從s下標(biāo)開始到e下標(biāo)結(jié)束的所有字符所占的區(qū)域?qū)捀叻庋b到b這個(gè)矩形中;
- clearShadowLayer():清除陰影層;
- measureText(String t, int s, int e):返回t文本中從s下標(biāo)開始到e下標(biāo)結(jié)束的所有字符所占的寬度;
- reset():重置畫筆為默認(rèn)值。
這里需要就幾個(gè)方法解釋一下:
1、setPathEffect(PathEffect pe):設(shè)置繪制的路徑的效果:
常見的有以下幾種可選方案:
- CornerPathEffect:可以用圓角來代替尖銳的角;
- DathPathEffect:虛線,由短線和點(diǎn)組成;
- DiscretePathEffect:荊棘狀的線條;
- PathDashPathEffect:定義一種新的形狀并將其作為原始路徑的輪廓標(biāo)記;
- SumPathEffect:在一條路徑中順序添加參數(shù)中的效果;
- ComposePathEffect:將兩種效果組合起來,先使用第一種效果,在此基礎(chǔ)上應(yīng)用第二種效果。
2、setXfermode(Xfermode m):設(shè)置圖形重疊時(shí)的處理方式:
關(guān)于Xfermode的多種效果,我們可以參考下面一張圖:

在使用的時(shí)候,我們需要通過paint. setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX))來設(shè)置,XXX是上圖中的某種模式對(duì)應(yīng)的常量參數(shù),如DST_OUT。
這16中情況的具體解釋如下:
1.PorterDuff.Mode.CLEAR:所繪制不會(huì)提交到畫布上。
2.PorterDuff.Mode.SRC:顯示上層繪制圖片
3.PorterDuff.Mode.DST:顯示下層繪制圖片
4.PorterDuff.Mode.SRC_OVER:正常繪制顯示,上下層繪制疊蓋。
5.PorterDuff.Mode.DST_OVER:上下層都顯示。下層居上顯示。
6.PorterDuff.Mode.SRC_IN:取兩層繪制交集。顯示上層。
7.PorterDuff.Mode.DST_IN:取兩層繪制交集。顯示下層。
8.PorterDuff.Mode.SRC_OUT:上層繪制非交集部分。
9.PorterDuff.Mode.DST_OUT:取下層繪制非交集部分。
10.PorterDuff.Mode.SRC_ATOP:取下層非交集部分與上層交集部分
11.PorterDuff.Mode.DST_ATOP:取上層非交集部分與下層交集部分
12.PorterDuff.Mode.XOR:異或:去除兩圖層交集部分
13.PorterDuff.Mode.DARKEN:取兩圖層全部區(qū)域,交集部分顏色加深
14.PorterDuff.Mode.LIGHTEN:取兩圖層全部,點(diǎn)亮交集部分顏色
15.PorterDuff.Mode.MULTIPLY:取兩圖層交集部分疊加后顏色
16.PorterDuff.Mode.SCREEN:取兩圖層全部區(qū)域,交集部分變?yōu)橥该魃?/p>
5.2、Canvas類
Canvas即畫布,其上可以使用Paint畫筆對(duì)象繪制很多東西。
Canvas****對(duì)象中可以繪制:
drawArc():繪制圓??;
drawBitmap():繪制Bitmap圖像;
drawCircle():繪制圓圈;
drawLine():繪制線條;
drawOval():繪制橢圓;
drawPath():繪制Path路徑;
drawPicture():繪制Picture圖片;
drawRect():繪制矩形;
drawRoundRect():繪制圓角矩形;
drawText():繪制文本;
drawVertices():繪制頂點(diǎn)。
Canvas****對(duì)象的其他方法:canvas.save():把當(dāng)前繪制的圖像保存起來,讓后續(xù)的操作相當(dāng)于是在一個(gè)新圖層上繪制;
canvas.restore():把當(dāng)前畫布調(diào)整到上一個(gè)save()之前的狀態(tài);
canvas.translate(dx, dy):把當(dāng)前畫布的原點(diǎn)移到(dx, dy)點(diǎn),后續(xù)操作都以(dx, dy)點(diǎn)作為參照;
canvas.scale(x, y):將當(dāng)前畫布在水平方向上縮放x倍,豎直方向上縮放y倍;
canvas.rotate(angle):將當(dāng)前畫布順時(shí)針旋轉(zhuǎn)angle度。</pre>
6、onLayout()
onLayout()方法負(fù)責(zé)布局,大多數(shù)情況是在自定義ViewGroup中才會(huì)重寫,主要用來確定子View在這個(gè)布局空間中的擺放位置。
onLayout(boolean changed, int l, int t, int r, int b)方法有5個(gè)參數(shù),其中changed表示這個(gè)控件是否有了新的尺寸或位置;l、t、r、b分別表示這個(gè)View相對(duì)于父布局的左/上/右/下方的位置。
以下是onLayout()方法中常用的方法:
- getChildCount():獲取子View的數(shù)量;
- getChildAt(i):獲取第i個(gè)子View
- getWidth/Height():獲取onMeasure()中返回的寬度和高度的測(cè)量值;
- child.getLayoutParams():獲取到子View的LayoutParams對(duì)象;
- child.getMeasuredWidth/Height():獲取onMeasure()方法中測(cè)量的子View的寬度和高度值;
- getPaddingLeft/Right/Top/Bottom():獲取控件的四周內(nèi)邊距;
- child.layout(l, t, r, b):設(shè)置子View布局的上下左右邊的坐標(biāo)
7、其他方法
7.1、generateLayoutParams()
generateLayoutParams()方法用在自定義ViewGroup中,用來指明子控件之間的關(guān)系,即與當(dāng)前的ViewGroup對(duì)應(yīng)的LayoutParams。我們只需要在方法中返回一個(gè)我們想要使用的LayoutParams類型的對(duì)象即可。
在generateLayoutParams()方法中需要傳入一個(gè)AttributeSet對(duì)象作為參數(shù),這個(gè)對(duì)象是這個(gè)ViewGroup的屬性集,系統(tǒng)根據(jù)這個(gè)ViewGroup的屬性集來定義子View的布局規(guī)則,供子View使用。
例如,在自定義流式布局中,我們只需要關(guān)心子控件之間的間隔關(guān)系,因此我們需要在generateLayoutParams()方法中返回一個(gè)new MarginLayoutParams()即可。
7.2、onTouchEvent()
onTouchEvent()方法用來監(jiān)測(cè)用戶手指操作。我們通過方法中MotionEvent參數(shù)對(duì)象的getAction()方法來實(shí)時(shí)獲取用戶的手勢(shì),有UP、DOWN和MOVE三個(gè)枚舉值,分別表示用于手指抬起、按下和滑動(dòng)的動(dòng)作。每當(dāng)用戶有操作時(shí),就會(huì)回掉onTouchEvent()方法。
7.3、onScrollChanged()
如果我們的自定義View / ViewGroup是繼承自ScrollView / HorizontalScrollView等可以滾動(dòng)的控件,就可以通過重寫onScrollChanged()方法來監(jiān)聽控件的滾動(dòng)事件。
這個(gè)方法中有四個(gè)參數(shù):l和t分別表示當(dāng)前滑動(dòng)到的點(diǎn)在水平和豎直方向上的坐標(biāo);oldl和oldt分別表示上次滑動(dòng)到的點(diǎn)在水平和豎直方向上的坐標(biāo)。我們可以通過這四個(gè)值對(duì)滑動(dòng)進(jìn)行處理,如添加屬性動(dòng)畫等。
7.4、invalidate()
invalidate()方法的作用是請(qǐng)求View樹進(jìn)行重繪,即draw()方法,如果視圖的大小發(fā)生了變化,還會(huì)調(diào)用layout()方法。
一般會(huì)引起invalidate()****操作的函數(shù)如下:
<pre style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 12px !important; font-family: "Courier New" !important;">1) 直接調(diào)用invalidate()方法,請(qǐng)求重新draw(),但只會(huì)繪制調(diào)用者本身;
- 調(diào)用setSelection()方法,請(qǐng)求重新draw(),但只會(huì)繪制調(diào)用者本身;
- 調(diào)用setVisibility()方法,會(huì)間接調(diào)用invalidate()方法,繼而繪制該View;
- 調(diào)用setEnabled()方法,請(qǐng)求重新draw(),但不會(huì)重新繪制任何視圖,包括調(diào)用者本身。
7.5、postInvalidate()
功能與invalidate()方法相同,只是postInvalidate()方法是異步請(qǐng)求重繪視圖。
7.6、requestLayout()
requestLayout()方法只是對(duì)View樹進(jìn)行重新布局layout過程(包括measure()過程和layout()過程),不會(huì)調(diào)用draw()過程,即不會(huì)重新繪制任何視圖,包括該調(diào)用者本身。
7.7、requestFocus()
請(qǐng)求View樹的draw()過程,但只會(huì)繪制需要重繪的視圖,即哪個(gè)View或ViewGroup調(diào)用了這個(gè)方法,就重繪哪個(gè)視圖。
8、總結(jié)
最后,讓我們來總覽一下自定義View / ViewGroup時(shí)調(diào)用的各種函數(shù)的順序,如下圖所示:

在這些方法中:
- onMeasure()會(huì)在初始化之后調(diào)用一到多次來測(cè)量控件或其中的子控件的寬高;
- onLayout()會(huì)在onMeasure()方法之后被調(diào)用一次,將控件或其子控件進(jìn)行布局;
- onDraw()會(huì)在onLayout()方法之后調(diào)用一次,也會(huì)在用戶手指觸摸屏幕時(shí)被調(diào)用多次,來繪制控件。