4.2 碎片的使用方式
4.2.1 碎片的簡單用法
建立一個左側碎片布局 fragment_left.xml,代碼如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="按鈕"/>
</LinearLayout>
然後建立右側碎片布局fragment_right.xml,代碼如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#00ff00">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="這是右邊的fragment"/>
</LinearLayout>
将布局的背景色設定成綠色,并放置了一個 TextView 用于顯示一段文本
建議使用 support-v4 庫中的 Fragment,因為它可以讓碎片在所有 Android 系統版本中保持功能一緻性
LeftFragment 的代碼如下所示:
public class LeftFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_left, container, false);
return view;
}
}
這裡僅僅是重寫了 Fragment 的 onCreateView()方法
然後在這個方法中通過 LayoutInflater 的 inflate()方法将剛才定義的 fragment_left 布局動态加載進來
接着我們用同樣的方法再建立一個 RightFragment:
public class RightFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_right, container, false);
return view;
}
}
接下來修改 activity_fragment.xml 中的代碼,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/fragment_left"
android:name="com.wonderful.myfirstcode.inquiry_fragment.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/fragment_right"
android:name="com.wonderful.myfirstcode.inquiry_fragment.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
可以看到,我們使用了<fragment>标簽在布局中添加碎片
通過 android:name 屬性來顯式指明要添加的碎片類名,注意一 定要将類的包名也加上
這樣最簡單的碎片示例就已經寫好了,運作一下程式,(平闆上)效果如圖:
4.2.2 動态添加碎片
碎片真正的強大之處在于,它可以在程式運作時動态地添加到活動當中
根據具體情況來動态地添加碎片,你就 可以将程式界面定制得更加多樣化
在上一節代碼的基礎上繼續完善,建立 fragment_another_right.xml,代碼如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#ffff00">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="這是另外一個右邊的fragment"/>
</LinearLayout>
然後建立 AnotherRightFragment 作為另一個右側碎片,代碼如下所示:
public class AnotherRightFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_another_right, container, false);
return view;
}
}
接下來看一下如何将它動态地添加到活動當中。修改 activity_fragment.xml,代碼如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/fragment_left"
android:name="com.wonderful.myfirstcode.inquiry_fragment.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/right_layout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<!--
<fragment
android:id="@+id/fragment_right"
android:name="com.wonderful.myfirstcode.inquiry_fragment.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
-->
</LinearLayout>
可以看到,現在将右側碎片放在了一個 FrameLayout 中
下面在代碼中向 FrameLayout 裡添加内容,進而實作動态添加碎片的功能
修改 Activity 中的代碼,如下所示:
public class FragmentActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
replaceFragment(new RightFragment());
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.button:
replaceFragment(new AnotherRightFragment());
break;
default:
break;
}
}
private void replaceFragment(Fragment fragment){
// 擷取FragmentManager
FragmentManager fragmentManager = getSupportFragmentManager();
// 開啟事務
FragmentTransaction transaction = fragmentManager.beginTransaction();
// 添加或替換碎片
transaction.replace(R.id.right_layout,fragment);
// 送出事務
transaction.commit();
}
}
上述代碼,給左側碎片中的按鈕注冊了一個點選事件
調用 replaceFragment() 方法動态添加碎片
結合代碼可看出,動态添加碎片主要分為 5 步
1. 建立待添加的碎片執行個體
2. 擷取 FragmentManager,在活動中可以直接調用 getSupportFragmentManager()方法得到
3. 開啟一個事務,通過調用 beginTransaction()方法開啟
4. 向容器内添加或替換碎片,使用 replace() 方法實作,需要傳入容器的 id 和待添加的碎片執行個體。
5. 送出事務,調用 commit()方法來完成。
重新運作程式,效果如圖:
4.2.3 在碎片中模拟傳回棧
點選按鈕添加了一個碎片之後,按下 Back 鍵程式就會直接退出
如果這裡我們想模仿類似于傳回棧的效果,按下 Back 鍵可以回到上一個碎片,該如何實作呢?
FragmentTransaction 中提供了一個 addToBackStack() 方法
可以用于将一個事務添加到傳回棧中,修改 Activity 中的代碼,如下所示:
public class FragmentActivity extends AppCompatActivity implements View.OnClickListener {
. . .
private void replaceFragment(Fragment fragment){
// 擷取FragmentManager
FragmentManager fragmentManager = getSupportFragmentManager();
// 開啟事務
FragmentTransaction transaction = fragmentManager.beginTransaction();
// 添加或替換碎片
transaction.replace(R.id.right_layout,fragment);
// 用于描述傳回棧的狀态
transaction.addToBackStack(null);
// 送出事務
transaction.commit();
}
}
在事務送出之前調用了 FragmentTransaction 的 addToBackStack() 方法
它可以接收一個名字用于描述傳回棧的狀态,一般傳入 null 即可
4.2.4 碎片和活動之間進行通信
為了友善碎片和活動之間進行通信,FragmentManager 提供了一個類似于 findViewById() 的方法
專門用于從布局檔案中擷取碎片的執行個體。在活動中調用碎片裡的方法:
RightFragment rightFragment = (RightFragment) getFragmentManager().findFragmentById(R.id.right_fragment);
在碎片中調用活動裡的方法:通過調用 getActivity()方法來得到和目前碎片相關聯的活動執行個體
代碼如下所示:
MainActivity activity = (MainActivity) getActivity();
4.3 碎片的生命周期
和活動 Acitvity 相似,Fragment 類中也提供了一系列的回調方法,以覆寫碎片生命周期的每個環節。其中,活動中有的回調方法,碎片中幾乎都有,不過碎片還提供了一些附加的回調方法,重點來看下這幾個回調:
- onAttach() 當碎片和活動建立關聯的時候調用。
- onCreateView() 為碎片建立視圖(加載布局)時調用。
- onActivityCreated() 確定與碎片相關聯的活動一定已經建立完畢的時候調用。
- onDestroyView() 當與碎片關聯的視圖被移除的時候調用。
- onDetach() 當碎片和活動解除關聯的時候調用。
另外,在碎片中你也可以通過onSaveInstanceState()方法來儲存資料
因為進入停止狀态的碎片有可能在系統記憶體不足的時候被回收
儲存下來的資料在 onCreate()、onCreateView() 和 onActivityCreated()這三個方法中
你都可以重新得到,它們都含有一個 Bundle 類型的 savedInstanceState 參數
4.5 碎片的最佳實踐——一個簡易版的新聞應用
添加後面需要用到的 RecyclerView 依賴庫
接下來,準備好一個新聞的實體類,建立類 News,代碼如下所示:
/**
* 新聞實體類
* Created by KXwon on 2016/12/12.
*/
public class News {
private String title; // 新聞标題
private String content; // 新聞内容
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
接着建立一個 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:gravity="center"
android:textSize="20sp"
android:padding="10dp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000"/>
<TextView
android:id="@+id/news_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:textSize="18sp"
android:padding="15dp"/>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:background="#000"/>
</RelativeLayout>
新聞内容的布局主要分為兩個部分
頭部顯示新聞标題,正文顯示新聞内容
中間使用一條細線分隔開
然後再建立一個 NewsContentFragment 類,如下:
/**
* 新聞内容fragment
* Created by KXwon on 2016/12/12.
*/
public class NewsContentFragment extends Fragment {
private View view;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
view = inflater.inflate(R.layout.news_content_frag, container, false);
return view;
}
/**
* 将新聞标題和新聞内容顯示在界面上
* @param newsTitle 标題
* @param newsContent 内容
*/
public void refresh(String newsTitle, String newsContent) {
View visibilityLayout = view.findViewById(R.id.visibility_layout);
visibilityLayout.setVisibility(View.VISIBLE);
TextView newsTitleText = (TextView) view.findViewById (R.id.news_title);
TextView newsContentText = (TextView) view.findViewById(R.id.news_content);
newsTitleText.setText(newsTitle); // 重新整理新聞的标題
newsContentText.setText(newsContent); // 重新整理新聞的内容
}
}
這樣就把新聞内容的碎片和布局建立好了
但它們都是在雙頁模式下使用的,若要在單頁模式中使用
還需建立一個活動 NewsContentActivity,其布局 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.wonderful.myfirstcode.chapter4.simple_news.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
這裡直接在布局中引入了 NewsContentFragment
相當于把 news_content_frag 布局的内容自動加了進來
然後編寫 NewsContentActivity 的代碼,如下:
public class NewsContentActivity extends AppCompatActivity {
/**
* 建構Intent,傳遞所需資料
* @param context
* @param newsTitle
* @param newsContent
*/
public static void actionStart(Context context, String newsTitle, String newsContent) {
Intent intent = 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");
String newsContent = getIntent().getStringExtra("news_content");
// 擷取 NewsContentFragment 執行個體
NewsContentFragment newsContentFragment = (NewsContentFragment) getSupportFragmentManager()
.findFragmentById(R.id.news_content_fragment);
// 重新整理 NewsContentFragment 界面
newsContentFragment.refresh(newsTitle, newsContent);
}
}
在 onCreate() 方法中通過 Intent 擷取傳入的新聞标題和内容
然後調用 FragmentManager 的 findFragmentById() 方法得到 NewsContentFragment 的執行個體
接着調用它的 refresh() 方法,并将新聞的标題和内容傳入,顯示資料
(關于 actionStart() 方法可以閱讀前面的探究活動2.5.2相關筆記。)
接下來還需再建立顯示新聞清單的布局 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>
建立 news_item.xml 作為 上述 RecyclerView 子項的布局:
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textSize="18sp"
android:padding="10dp"/>
子項的布局就隻有一個 TextView
新聞清單和子項布局都建立好了,接下來就需要一個用于展示新聞清單的地方
這裡建立 NewsTitleFragment 作為展示新聞清單的碎片:
/**
* 新聞清單fragment
* Created by KXwon on 2016/12/12.
*/
public class NewsTitleFragment extends Fragment{
private boolean isTowPane;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.news_content_frag, container, false);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_layout)!= null){
// 可以找到 news_content_layout 布局時,為雙頁模式
isTowPane = true;
}else {
// 找不到 news_content_layout 布局時,為單頁模式
isTowPane = false;
}
}
}
為實作上述 onActivityCreated() 方法中判斷目前時雙頁還是單頁模式
接下來在 NewsTitleFragemt 中建立一個内部類 NewsAdapter 來作為 RecyclerView 的擴充卡
如下:
public class NewsTitleFragment extends Fragment{
private boolean isTowPane;
. . .
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()).inflate(R.layout.news_item, parent, false);
final ViewHolder holder = new ViewHolder(view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
News news = mNewsList.get(holder.getAdapterPosition());
if (isTwoPane) {
// 若是雙頁模式,則重新整理 NewsContentFragment 中的内容
NewsContentFragment newsContentFragment = (NewsContentFragment)
getFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(), news.getContent());
} else {
// 若是單頁模式,則直接啟動 NewsContentActivity
NewsContentActivity.actionStart(getActivity(), 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();
}
}
需要注意的是,這裡把擴充卡寫成内部類是為了直接通路 NewsTitleFragment 的變量
比如:isTowPane
現在還剩最後一步收尾工作,就是向 RecyclerView 中填充資料了
修改 NewsTitleFragment 中的代碼,如下所示:
public class NewsTitleFragment extends Fragment{
. . .
@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;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_layout) != null) {
// 可以找到news_content_layout布局時,為雙頁模式
isTwoPane = true;
} else {
// 找不到news_content_layout布局時,為單頁模式
isTwoPane = false;
}
}
/**
* 初始化50條模拟新聞資料
* @return
*/
private List<News> getNews() {
List<News> newsList = new ArrayList<>();
for (int i = 1; i <= 50; i++) {
News news = new News();
news.setTitle("This is news title " + i);
news.setContent(getRandomLengthContent("新聞内容吼吼吼" + i + "!"));
newsList.add(news);
}
return newsList;
}
/**
* 随機生成不同長度的新聞内容
* @param content
* @return
*/
private String getRandomLengthContent(String content) {
Random random = new Random();
int length = random.nextInt(20) + 1;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(content);
}
return builder.toString();
}
. . .
}
到這裡,所有的代碼編寫工作就完成了,運作程式,效果如下:
點選一條新聞,會啟動一個新的活動來顯示新聞内容: