====================================
====== 第四章:手機(jī)平板要兼顧 — 探究碎片 ======
====================================
4.1 碎片是什么(通過都是在平板中使用)
碎片(fragment)是一種可以嵌入在活動當(dāng)中的UI片段,它能讓程序更加合理和充分地利用大屏幕的空間。碎片和活動是在太像了,同樣能包含布局,同樣有自己的聲明周期。你甚至可以將碎片理解為一個(gè)迷你型的活動。
一個(gè)活動中引入兩個(gè)碎片,相當(dāng)于一個(gè)活動力分為左右兩個(gè)屏幕。
新建一個(gè)FragmentTest
創(chuàng)建兩個(gè)layout的xml文件,一個(gè)left一個(gè)right。
然后新建一個(gè)Fragment的類,繼承自fragment,這時(shí)候發(fā)現(xiàn)有兩個(gè)fragment供選擇,一個(gè)是系統(tǒng)內(nèi)置的android.app.Fragment,一個(gè)是support-v4庫中的 ,強(qiáng)烈建議使用support-v4的Fragment,因?yàn)榭梢宰屗槠谒蠥ndroid系統(tǒng)版本中保持功能一致性。
另外,我們不需要在build.gradle文件中添加support-v4庫的依賴,因?yàn)閎uild.gradle文件中已經(jīng)添加了appcompat-v7庫的依賴,而這個(gè)庫會將support-v4庫也一起引入進(jìn)來。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:id="@+id/left_fragment"
android:layout_weight="1"
android:name="com.example.fragmenttest.LeftFragment"/>
<fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:id="@+id/right_fragment"
android:layout_weight="1"
android:name="com.example.fragmenttest.RightFragment"/>
</LinearLayout>
可以看出,我們使用了<fragment>標(biāo)簽在布局中添加碎片,,只不過這里需要通過android:name屬性來顯性指明要添加的碎片類型(注意這里一定要將類的包名也加上)
4.2.2 動態(tài)添加碎片
碎片的真正強(qiáng)大之處在于,它可以在程序運(yùn)行事動態(tài)的添加到活動當(dāng)中,根據(jù)實(shí)際情況動態(tài)的添加碎片,您就可以將程序界面定制更加多樣化。
新增一個(gè)another_right_fragment.xml,
動態(tài)添加碎片主要分為5步:
1、創(chuàng)建待添加的碎片實(shí)例
2、獲取FragmentManager,在活動中可以直接通過調(diào)用getSupportFragmentManger()方法得到。
3、開啟一個(gè)事物,通過調(diào)用beginTransaction()方法開啟
4、向容器內(nèi)添加或替換碎片,一般使用replace()方法實(shí)現(xiàn),需要傳入容器的id和待添加的碎片實(shí)例
5、提交事務(wù),調(diào)用commit()方法來完成。
4.2.3 在碎片中模擬返回棧
FragmentTransaction中提供了一個(gè)addToBackStack()方法,可以用于將一個(gè)事物添加到返回棧中。修改MainActivity中的代碼。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
…
private void replaceFragment(Fragment fragment) {
FragmentMananger fragmengManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
}
}
addBackToStack(null)可以接受一個(gè)名字用于描述返回棧的狀態(tài),一般傳入null即可。
4.2.4 碎片和活動之間進(jìn)行通信
雖然現(xiàn)在碎片是嵌入到活動中了,但是實(shí)際上他們的關(guān)系并沒有那么親密。你看到,碎片和活動都是各自存在于獨(dú)立的類中的。
為了方便碎片和活動之間進(jìn)行通訊,F(xiàn)ragmentManager提供了一個(gè)類似于findViewById()的方法,專門用于從布局文件中獲取碎片的實(shí)例。如下:
RightFragment rightFragment = (RightFragment)getFragmentManager().findFragmentById(R.id.right_fragment);
那么,如何在碎片里調(diào)用活動呢?在每個(gè)碎片中都可以通過getActivity()方法來得到和當(dāng)前碎片相關(guān)聯(lián)的活動實(shí)例,如下:
MainActivity activity = (MainActivity) getActivity();
getActivity()獲取到的本身就是一個(gè)context對象。
那么,碎片與碎片之間如何通訊呢?碎片可以得到與它關(guān)聯(lián)的活動,通過活動再獲取另一個(gè)碎片的實(shí)例即可。
4.3 碎片的聲明周期
和活動一樣,碎片也有聲明周期,而且它的聲明周期與活動的生命周期實(shí)在是太像了。
4.3.1 碎片的狀態(tài)和回調(diào)
活動有:運(yùn)行狀態(tài)、暫停狀態(tài)、停止?fàn)顟B(tài)和銷毀狀態(tài)四種。
以下是碎片的聲明周期:
1、運(yùn)行狀態(tài):
當(dāng)一個(gè)碎片是可見的,并且它所關(guān)聯(lián)的活動正處于運(yùn)行狀態(tài)時(shí),該碎片也處于運(yùn)行轉(zhuǎn)臺。
2、暫停狀態(tài):
當(dāng)一個(gè)活動進(jìn)入暫停狀態(tài)(由于另一個(gè)未占滿屏幕的活動被添加到了棧頂),與它相關(guān)聯(lián)的可見碎片就會進(jìn)入暫停狀態(tài)
3、停止?fàn)顟B(tài):
當(dāng)一個(gè)活動進(jìn)入停止?fàn)顟B(tài),與它關(guān)聯(lián)的碎片就會進(jìn)入停止?fàn)顟B(tài)?;蛘咄ㄟ^調(diào)用FragmentTransaction的remove()、replace()方法將碎片從活動中移除,但如果在事物提交之前調(diào)用addToBackStack()方法,這時(shí)的碎片也會進(jìn)入到停止?fàn)顟B(tài)。總的來說,進(jìn)入停止?fàn)顟B(tài)的碎片對于用戶來說是完全不可見的,有可能會被系統(tǒng)回收。
4、銷毀狀態(tài):
碎片依附于活動而存在,當(dāng)活動被銷毀時(shí),與他相關(guān)聯(lián)的碎片就會進(jìn)入到銷毀狀態(tài)?;蛘咄ㄟ^調(diào)用FragmentTransaction的remove()、replace()方法將碎片從活動中移除,但是在事物提交之前沒有調(diào)用addBackToStack()方法,這時(shí)的碎片也會進(jìn)入到銷毀狀態(tài)。
Fragment類中也提供了一系列的毀掉方法。以覆蓋碎片生命周期的每個(gè)環(huán)節(jié)。其中,活動中有的回調(diào)方法,碎片中幾乎都有,不過碎片還提供了一些附加的會調(diào)方法,重點(diǎn)看一下這幾個(gè)會調(diào):
onAttach():當(dāng)碎片和活動建立關(guān)聯(lián)的時(shí)候調(diào)用
onCreateView():為碎片創(chuàng)建視圖(加載布局)時(shí)調(diào)用
onActivityCreate():確保與碎片相關(guān)聯(lián)的活動一定已經(jīng)創(chuàng)建完畢時(shí)調(diào)用。
onDestroyView():當(dāng)與碎片關(guān)聯(lián)的視圖被移除的時(shí)候調(diào)用
onDetach():當(dāng)碎片和活動解除關(guān)聯(lián)的時(shí)候調(diào)用。
具體的碎片聲明周期看書本圖片:page153
添加一個(gè)碎片 —> onAttach() —> onCreate() —> onCreateView() —> onStart() —> onResume() —> 碎片已激活 —> onPause() —> onStop() —> onDestroy() —> onDetach() —> 碎片已銷毀
4.3.2 體驗(yàn)一下碎片的聲明周期
修改RightFragment中的代碼
public class RightFragment extends Fragment {
public statci final String TAG = “RightFragment”;
@Override
public void onAttach(Context context) {
super.onAttach(context);
Log.d(TAG, “onAttach”);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, VIewGroup container, Bundle savedInstanceState) {
Log.d(TAG, “onCreateView”);
View view = inflater.inflater(R.layout.right_fragment, container, false);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreate(savedInstanceState);
}
@Override
public void onStart() {
super.onStart();
}
@Overrde
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onStop() {
super.onStop();
}
@Overrde
public void onDestroyView() {
super.onDestroyVew();
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public voide onDetach() {
super.onDetach();
}
}
順便,在碎片中,你也可以通過onSaveInstanceState()方法來保存數(shù)據(jù),因?yàn)樵谶M(jìn)入停止?fàn)顟B(tài)的時(shí)候碎片有可能在系統(tǒng)內(nèi)存不足的時(shí)候被回收。保存下來的數(shù)據(jù)可以在onCreate() onCreateView() onActivityCreate() 這三個(gè)方法中重新得到,因?yàn)檫@三個(gè)方法中都有Bundle類型的savedInstanceState參數(shù)。
4.4 動態(tài)加載布局的技巧
動態(tài)添加碎片的功能畢竟只是一個(gè)布局文件中進(jìn)行添加和替換操作。如果程序能夠根據(jù)設(shè)備的分辨率或屏幕大小來運(yùn)行時(shí)加載某個(gè)布局,那我們可發(fā)揮的空間就更多了。
4.4.1
使用平板電腦經(jīng)常會發(fā)現(xiàn)分左右兩頁,那么如何才能在運(yùn)行時(shí)判斷程序應(yīng)該是使用雙頁模式還是使用單頁模式呢?這就需要借助限定符(Qualifiers)來實(shí)現(xiàn)。
創(chuàng)建兩個(gè)activity_main.xml的文件。一個(gè)只有一個(gè)fragment占滿屏幕,一個(gè)有兩個(gè)fragment,分為左右屏。
其中,放入layout文件夾中的
另一個(gè)放在layout_large文件夾中,
其中,large就是一個(gè)限定符,那些屏幕被認(rèn)為是large的設(shè)備就會自動加載layout_large文件夾下的布局,小屏幕的設(shè)備則會加載layout文件夾下的布局。
Android中常見的限定符如下所示:(參照Page159)
屏幕特征 限定符 描述
大小 small 提供給小屏幕設(shè)備的資源
normal 提供給中等屏幕設(shè)備的資源
large 提供給大屏幕設(shè)備的資源
xlarge 提供給超大屏幕設(shè)備的資源
分辨率 ldpi 提供給低分辨率設(shè)備的資源(120dpi以下)
mdpi 提供給中等分辨率設(shè)備的資源(120dpi ~ 160dpi)
hdpi 提供給高分辨率設(shè)備的資源(160dpi ~ 240dpi)
xhdpi 提供給超高分辨率設(shè)備的資源(240dpi ~ 320dpi)
xxhdpi 提供給超超高分辨率設(shè)備的資源(320dpi ~ 480dpi)
方向 land 提供給橫屏設(shè)備的資源
port 提供給豎屏設(shè)備的資源
4.4.2 使用最小寬度限定符
新的問題出現(xiàn)了,large到底是指多大呢?
最小寬度限定符允許我們對屏幕的寬度指定一個(gè)最小值(以dp為單位),然后以這個(gè)最小值為臨界點(diǎn)。
在res目錄下新建layout-sw600dp,然后在里面新建activity_main.xml布局,這意味著,當(dāng)程序運(yùn)行在屏幕寬度大于600dp的設(shè)備上時(shí),會加載layout-sw600dp/activity_main布局。
4.5 碎片的最佳實(shí)踐 —> 一個(gè)簡易的新聞應(yīng)用
常見一個(gè)新的應(yīng)用FragmentBestPractice。
1、在app/build.gradle中添加recyclerView的依賴庫
2、新增一個(gè)News類,作為model
public class News {
private String title; // 標(biāo)題
private String content; // 內(nèi)容
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
public void setTitle(String title) {
this.title = title;
}
public void setContent(String content) {
this.content = content;
}
3、新建布局,用于新聞內(nèi)容的布局 news_content_frag.xml
<RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“match_parent”
android:layout_height=“match_parent” >
<LinearLayout
android:id=“@+id/visibility_layout”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“vertical”
android:visibility=“invisible” >
<TextView
android:id=“@+id/news_title”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:layout_gravity=“center”
android:padding=“10dp”
android:textSize=“20sp” />
<View
android:layout_width=“match_parent”
android:layout_height=“1dp”
android:background=“#000” />
<TextView
android:id=“@+id/new_content”
android:layout_width=“match_parent”
android:layout_height=“0dp”
android:layout_weight=“1”
android:padding=“15dp”
android:textSize=“18sp” />
</LinearLayout>
<View
android:layout_width=@“1dp”
android:layout_height=“match_parent”
android:layout_alignParentLeft=“true”
android:background=“#000” />
</RelativeLayout>
4、新建一個(gè)NewsContentFragment類,繼承自Fragment,代碼如下
public class NewsContentFragment extends Fragment {
private VIew view;
@Override
public View onCreateVIew(LayoutInflater infalter, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflater(R.layout.news_content_frag, container, false);
return view;
}
public void refresh(String newsTitle, String newsContent) {
View visibilityLayout = view.findVIewById(R.id.visibility_layout);
visibilityLayout.setVisibility(VIew.VISIBLE);
TextView newsTitltText = (TextView) view.findViewById(R.id.news_title);
TextView newsContentText = (TextView) view.findViewById(R.id.new_content);
newsTitleText.setText(newsTitle); // 刷新新聞的標(biāo)題
newsContentText.setText(newsContent); // 刷新新聞的內(nèi)容
}
}
5、上面我們創(chuàng)建的碎片和布局都是在雙頁模式下使用的,現(xiàn)在需要再創(chuàng)建一個(gè)活動,右鍵com.example.fragmentbestpractice包 —> New —> Activity —> Empty Activity,新建一個(gè)NewsContentActivity,并將布局名指定為news_content(新增一個(gè)news_content.xml文件):
<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:orientation=“vertical”
android:layout_width=“match_parent”
android:layout_height=“match_parent” >
<fragment
android:id=“@+id/news_content_fragment”
android:name=“com.example.fragmentbestpractice.NewsContentFragment”
android:layout_width=“match_parent”
android:layotu_height=“match_parent” />
</LinearLayout>
6、修改NewsContentFragment的代碼
public class NewsContentFragment extends AppCompataActivity {
public static void actionStart(Context context, String newsTitle, String newsContent) {
Intent intend = new Intent(context, NewsContentActivity.class);
intent.putExtra(“news_title”, newsTitle);
intent.putExtra(“news_content”, newsContent);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_content);
String newsTitle = getIntent().getStringExtra(“news_title”); // 獲取傳入的新聞標(biāo)題
String newsContent = getIntent().getStingExtra(“news_content”); // 獲取傳入的新聞內(nèi)容
NewsContentFragment newsContentFragment = (NewsContentFragment) getSupportFragmengManager().findFragmentById(R.id.news_content_fragnent);
newsContentFragment.refresh(newsTitle, newsContent); // 刷新NewsContent-Fragment界面
}
}
在onCreate方法中,我們通過Intent獲取到了傳入的新聞標(biāo)題和新聞內(nèi)容
然后通過FragmentManager的findFragmentById()方法得到了NewsContentFragment的實(shí)例。接著調(diào)用它的refresh()方法。
actionStart()方法,作用忘記了。。。回看一下2.6.3小節(jié)。
7、還需要?jiǎng)?chuàng)建一個(gè)用于顯示新聞列表的布局,新建news_title_frag.xml
<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:orientation=“vertical”
android:layout_width=“match_parent”
android:layout_height=“match_parent” >
<android.support.v7.widget.RecyclerView
android:id=“@+id/news_title_recycler_view”
android:layout_width=“match_parent”
android:layout_height=“match_parent” />
</LinearLayout>
8、新建news_item.xml作為RecyclerView子項(xiàng)的布局
<TextView xmlns:android=“http://schemas.android.com/apk/res/android”
android:id=“@+id/new_title”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:singleLine=“true”
android:ellipsize=“end”
android:textSize=“18sp”
android:paddingLeft=“10dp”
android:paddingRight=“10dp”
android:paddingTop=“15dp”
android:paddingBottom=“15dp” />
android:padding的意思表示給控件的周圍加上補(bǔ)白,這樣不至于讓文本內(nèi)容靠在邊緣上
android:singleLine設(shè)置為true表示讓TextView只能單行顯示
Android:ellipsize用于設(shè)定當(dāng)文本內(nèi)容超出控件寬度時(shí),文本的縮略方式,這里的end表示在尾部進(jìn)行縮略。
9、用于展示新聞列表的地方。新建NewsTitleFragment作為展示新聞列表的碎片。
public class NewsTitleFragment extends Fragment {
private boolean isTwoPane;
@Override
public View onCreateView(LayoutInflater inflater, VIewGroup container, Bundle savedInstanceState) {
View view = inflater.inflater(R.id.news_title_frag, container, false);
return view;
}
@Override
public void onActivityCreate(Bundle savedInstanceState) {
super.onActivityCreate(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_layout) != null ) {
isTwoPane = true; // 可以找到news_content_layout布局時(shí),為雙頁模式
} else {
isTwoPane = false; // 找不到news_content_layout布局時(shí),為單頁模式
}
}
}
getActivity()方法用戶在fragment中獲取關(guān)聯(lián)的activity
10、修改activity_main.xml文件
<FramLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:id=“@+id/news_title_layout”
android:layout_width=“match_parent”
android:layout_height=“match_parent” >
<fragment
android:id=“@+id/news_title_fragment”
android:name=“com.example.fragmentbestparctice.NewsTitleFragment”
android:layout_width=“match_parent”
android:layout_height=“match_parent” />
</FrameLayout>
上面代碼中,在單頁模式下,只會加載一個(gè)新聞標(biāo)題的碎片。
11、然后新建layout-sw600dp文件夾,在這個(gè)文件及中新建一個(gè)activity_main.xml文件,代碼如下
<LinearLayout xmlns:android=“http://schemas.android.com/apl/res/android”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“horizontal” >
<fragment
android:id=“@+id/news_title+fragment”
android:name=“com.example.fragmentbestpractice.NewsTitleFragment”
android:layout_width=“0dp”
android:layout_height=“match_parent”
android:layout_weight=“1”/>
<FrameLayout
android:id=“@+id/news_content_layout”
android:layout_width=“0dp”
android:layout_height=“match_parent”
android:layout_weight=“3” />
<fragment
android:id=“@+id/news_content_fragment”
android:name=“com.example.fragmentbestpractice.NewsContentFragment”
android:layout_width=“match_parent”
android:layout_height=“math_parent” />
</FrameLayout>
</LinearLayout>
可以看到,我們在雙頁模式下引入了兩個(gè)碎片,并將新聞內(nèi)容碎片放在FrameLayout布局下,而這個(gè)布局的id正式news_content_layout,因此,能夠找到這個(gè)id的時(shí)候就是雙頁模式。
12、在NewsTitleFragment中通過RecyclerView將新聞列表展示出來,我們在NewsTitleFragment中新建一個(gè)NewsAdapter來作為RecyclerView的適配器。
public class NewsTitltFragment extends Fragment {
private boolean isTwoPane;
…
class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
private List<News> mNewsList;
class ViewHolder extends RecyclerVIew.ViewHolder {
TextView newsTitleText;
public ViewHolder(View view) {
super(view);
newsTitleText = (TextView) view.findViewById(R.id.news_title);
}
}
public NewsAdapter(List<News> newsList) {
mNewsList = newsList;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflater(R.layout.news_item, parent, false);
final ViewHolder holder = new ViewHolder(view);
view.setOnClickLinster(new View.OnClickListener() {
@Override
public void onClick(View v) {
News news = mNewsList.get(holder.getAdapterPosition());
if (isTwoPane) {
// 如果是雙頁模式,則刷新NewsContentFragment中的內(nèi)容
NewsContentFragment newsContentFragment = (NewsContentFragment)getFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(), news.getContent());
} else {
// 如果是單頁模式,則直接啟動NewsContentActivity
NewsContentActivity.actionStart(getActigity(), news.getTitle(), news.getContent());
}
}
});
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
News news = mNewsList.get(position);
holder.newsTitleText.setText(news.getTitle());
}
@Override
public int getItemCount() {
return mNewsList.size();
}
}
需要注意的是,之前我們是將適配器寫成一個(gè)獨(dú)立的類,其實(shí)也可以寫成內(nèi)部類的。這里寫成內(nèi)部類的好處就是可以直接訪問NewsTitleFragment的變量,比如isTwoPane變量;
13、最后的工作,向RecyclerView中填充數(shù)據(jù)。修改NewsTitleFragment中的代碼
@Override
public View onCreateView (LayoutInflater inflater, VIewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.news_title_frag, container, false);
RecyclerView newsTitleRecyclerView = (RecyclerView) view.findViewById(R.id.news_title_recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
newsTitleRecyclerView.setLayoutManager(layoutManager);
NewsAdapter adapter = new NewsAdapter(getNews());
newsTitleRecyclerView.setAdapter(adapter);
return view;
}
private List<News> getNews() {
List<News> newsList = new ArrayList<>();
for (int i = 0; i < 50; i++) {
News news = new News();
news.setTitle(“This is news title ” + i);
news.setContent(getRandomLengthContent(“This is news content ”+ i + “. ” ));
newsList.add(news);
}
return newsList;
}
private String getRandomLengthContent(String content) {
Random random = new Random();
int length = random.nextInt(20) + 1;
StringBuilder builder = new StrignBuilder();
for (int i = 0; i < length; i++ ) {
builder.append(content);
}
return builder.toString();
}
總結(jié):到本章為止,已經(jīng)學(xué)習(xí)完了AndroidUI相關(guān)的重要知識點(diǎn)。但是只是涉及了Android四大組件的第一個(gè)組件活動。