第二行程式碼學習筆記——第四章:手機平板要相容——探究碎片
本章要點
作為一名專業的Android開發人員,能夠同時相容手機和平板的開發時我們必須要做到的事情。
4.1 碎片是什麼
碎片(Fragment)是一種可以巢狀在活動當中的UI片段,它能讓程式更加合理和充分的利用大螢幕的控制元件。
4.2 碎片的使用方式
開始我們的碎片之旅,建立FragmentTest專案。
4.2.1 碎片的簡易用法
最簡單碎片,在一個活動中新增兩個碎片,並讓這兩個碎片平分活動控制元件。
新建左碎片佈局left_fragment.xml,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn"
android:text="Button"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
新建右側碎片,right_fragment.xml,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="#00ff00"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="This is right fragment"/>
</LinearLayout>
新建LeftFragment類和RrightFragment類,並讓它們繼承自support-v4包下的Fragment,重寫onCreateView()來載入佈局。
LeftFragment,程式碼如下:
public class LeftFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.left_fragment, container, false);
return view;
}
}
RightFragment,程式碼如下:
public class RightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.right_fragment, container, false);
return view;
}
}
修改activity_main.xml中的程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.hjw.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.example.hjw.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
</LinearLayout>
屬性:
android:name 指明新增碎片的類名(包名+類名)。
執行效果:
4.2.2 動態新增碎片
新建another_right_fragment.xml,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="#ffff00"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:textSize="20sp"
android:text="This is another right fragment"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
建立AnotherRightFragment類,程式碼如下:
public class AnotherRightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.another_right_fragment, container, false);
return view;
}
}
修改activity_main.xml中的程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.hjw.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<FrameLayout
android:id="@+id/right_layout"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"></FrameLayout>
</LinearLayout>
實現動態新增Fragment,修改MainActivity中的程式碼如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn= (Button) findViewById(R.id.btn);
btn.setOnClickListener(this);
replaceFragmetn(new RightFragment());
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn:
replaceFragmetn(new AnotherRightFragment());
break;
default:
break;z
}
}
public void replaceFragmetn(Fragment fragment){
FragmentManager fragmentManager=getSupportFragmentManager();
FragmentTransaction transaction=fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout,fragment);
transaction.commit();
}
}
動態添加布局5步驟:
- 建立待新增碎片的例項。
- 獲取FragmentManager,在活動中直接通過呼叫getSupportFragmentManager()得到。
- 開啟一個事務,通過beginTransaction()開啟。
- 向容器新增或替換佈局,一般使用replace()實現,需要傳入容器的id和待添加布局的例項。
- 提交事務,commit()。
重啟,點選一下按鈕,效果圖:
4.2.3 在碎片中模擬返回棧
模仿類似於返回棧的效果好,Back返回上一個碎片。
FragmentTransaction提供了addToBackStack()方法,一般傳入null。修改MainActivity中的程式碼如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
......
public void replaceFragmetn(Fragment fragment){
FragmentManager fragmentManager=getSupportFragmentManager();
FragmentTransaction transaction=fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout,fragment);
transaction.addToBackStack(null);
transaction.commit();
}
}
重新執行程式,點選按鈕將AnotherRightFragment新增到活動中,按下Back鍵,會RightFragment,再按下Back鍵,RightFragment也會消失,再按下Back鍵,程式才會退出。
4.2.4 碎片與活動之間進行通訊
活動中呼叫碎片的方法,FragmentManager提供了一個類似於findFragmentById()的方法,從佈局中獲取碎片的例項,程式碼如下:
RightFragment rightFragment = (RightFragment) getSupportFragmentManager().findFragmentById(R.id.right_fragment);
碎片中呼叫活動的方法,呼叫getActivity()方法來得到和當前碎片相關聯的活動,程式碼如下:
MainActivity activity = (MainActivity) getActivity();
這樣,碎片就可以呼叫活動中的方法了,當碎片需要使用Context物件時,也可以使用getActivity()方法,因為獲取到的活動本身就是一個Context物件。
4.3 碎片的生命週期
碎片自己的生命週期。
4.3.1 碎片的狀態和回撥
碎片和活動一樣,生命週期會有4中狀態:
- 執行狀態
當一個碎片時可見的,並且它關聯的活動正處於執行狀態時,該對片也處於執行狀態。 - 暫停狀態
當一個活動進入暫停狀態時,與它相關聯的可見碎片就會進入到暫停狀態。 - 停止狀態
當一個活動進入停止狀態時,與它相關聯的碎片就會進入停止狀態,或呼叫FragmentTransaction的revome(),replace()將碎片從活動中移除, 如果在提交事務之前呼叫addToBackStack()方法,碎片也會進入停止狀態。 - 銷燬狀態
碎片總是依附於活動而存在的,當活動銷燬時,與它關聯的碎片也會銷燬狀態。或呼叫FragmentTransaction的revome(),replace()將碎片從活動中移除, 如果在提交事務之前並沒有呼叫addToBackStack()方法,這時的碎片就會進入銷燬狀態。
活動中的回撥方法,碎片幾乎都有,碎片還提供了一些附加的回撥方法:
- onAttach()。 碎片和活動建立關聯的時候呼叫。
- onCreateView()。 碎片建立檢視(載入佈局)時呼叫。
- onActivityCreated()。 確保與碎片相關聯的活動一定已經建立完畢呼叫。
- onDestroyView()。 當與碎片相關聯的檢視被移除的時候呼叫。
- onDetach()。 當碎片與活動解除關聯的時候呼叫。
碎片完整的生命週期圖:
4.3.2 體驗碎片的生命週期
修改RightFragment中的程式碼,如下:
public class RightFragment extends Fragment {
private static final String TAG = "RightFragment";
@Override
public void onAttach(Context context) {
super.onAttach(context);
Log.d(TAG, "onAttach: ");
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate: ");
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.d(TAG, "onCreateView: ");
View view = LayoutInflater.from(getContext()).inflate(R.layout.right_fragment, container, false);
MainActivity activity = (MainActivity) getActivity();
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.d(TAG, "onActivityCreated: ");
}
@Override
public void onStart() {
super.onStart();
Log.d(TAG, "onStart: ");
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume: ");
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause: ");
}
@Override
public void onStop() {
super.onStop();
Log.d(TAG, "onStop: ");
}
@Override
public void onDestroyView() {
super.onDestroyView();
Log.d(TAG, "onDestroyView: ");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy: ");
}
@Override
public void onDetach() {
super.onDetach();
Log.d(TAG, "onDetach: ");
}
}
執行程式,觀察logcat列印資訊:
點選LeftFragment中的按鈕,觀察logcat列印資訊:
按下Back鍵,RightFragment重新回到執行狀態,觀察logcat列印資訊:
再次按下Back鍵,觀察logcat列印資訊:
4.4 動態載入佈局的技巧
Android中動態載入佈局的計技巧。
4.4.1 使用限定符
判斷程式應該是單頁還是雙頁模式:限定符(Qualifiers)。
修改FragmentTest專案中的activity.main.xml檔案,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.hjw.fragmenttest.LeftFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
在res目錄下layout_large資料夾,新建activity.main.xml,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.hjw.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.example.hjw.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="match_parent"/>
</LinearLayout>
large是一個限定符,修改Macitivty中的程式碼:replaceFragment()程式碼注掉。執行平板效果如下:
在啟動一個手機模擬器,執行效果如下:
限定符的引數:
4.4.2 使用最小寬度限定符
最小限定符(Smallest-width-Qualifier)
在res目錄新建layout-sw600dp資料夾,新建activity_main.xml中的檔案:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.hjw.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.example.hjw.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="match_parent"/>
</LinearLayout>
螢幕寬度大於600dp,會載入layout-600dp中的佈局,小於600載入預設佈局。
4.5 碎片的最佳實踐——一個簡易版的新聞應用
新聞應用
新建FragmentBestPractice專案。
新增依賴:
compile 'com.android.support:recyclerview-v7:24.2.1'
新建News實體類:
public class News {
private String title;
private String content;
public void setTitle(String title) {
this.title = title;
}
public void setContent(String content) {
this.content = content;
}
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
}
新建佈局檔案news_content_frag.xml,作為新聞內容的佈局,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<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">
<TextView
android:id="@+id/tv_news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android: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/tv_news_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:background="#000" />
</RelativeLayout>
新建NewsContentFragment類載入news_content_frag佈局,繼承自Fragment,程式碼如下:
public class NewsContentFragment extends Fragment {
private View view;
private TextView tv_news_title,tv_news_content;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
view = LayoutInflater.from(getContext()).inflate(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);
tv_news_title= (TextView) view.findViewById(R.id.tv_news_title);
tv_news_content= (TextView) view.findViewById(R.id.tv_news_content);
tv_news_title.setText(newsTitle); //重新整理新聞的標題
tv_news_content.setText(newsContent); //重新整理新聞的頭部
}
}
單頁模式使用,建立NewsContentActivity,指定佈局名為news_content,引入NewsContentFragment佈局,修改佈局檔案如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="@+id/news_content_fragment"
android:name="com.example.hjw.fragmentbestpractice.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
修改NewsContentActivity 中的程式碼,如下:
public class NewsContentActivity extends AppCompatActivity {
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);
Intent intent = getIntent();
String news_title = intent.getStringExtra("news_title"); //獲取新聞的標題
String news_content = intent.getStringExtra("news_content"); //獲取新聞的內容
NewsContentFragment newsContentFragment= (NewsContentFragment) getSupportFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news_title,news_content); //重新整理NewsContentFragment介面
}
}
新建news_title_frag.xml佈局,用於顯示新聞的標題列表的佈局,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<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中子項的佈局,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingBottom="15dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:singleLine="true"
android:textSize="18sp" />
新建NewsTitleFragment作為展示新聞列表的碎片,onCreateView載入news_title_frag.xml佈局,程式碼如下:
public class NewsTitleFragment extends Fragment{
private boolean isTowPane;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.news_title_frag, container, false);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_layout)!=null){
isTowPane=true; //可以找到news_content_layout佈局,為雙頁
}else{
isTowPane=false; //找不到news_content_layout佈局,為單頁
}
}
接下來我們修改main_activity.xml,單頁模式中的程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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.hjw.fragmentbestpractice.NewsTitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
新建layout-600dp資料夾,新建main_activity.xml佈局,雙頁模式程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/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.hjw.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.hjw.fragmentbestpractice.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</LinearLayout>
接下來在NewsTitleFragment中通過RecyclerView展示新聞列表,在NewsTitleFragment中新建內部類NewsAdapter作為RecyclerView的介面卡,程式碼如下所示:
public class NewsTitleFragment extends Fragment{
private boolean isTowPane;
......
class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder>{
private List<News> mData;
public NewsAdapter(List<News> mData) {
this.mData = mData;
}
@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 = mData.get(holder.getAdapterPosition());
if (isTowPane){
//如果是雙頁,重新整理NewsContentFragment中的內容
NewsContentFragment newsContentFragment= (NewsContentFragment) getFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(),news.getContent());
}else{
//如果是單頁,直接啟動NewsContentActivity
NewsContentActivity.actionStart(getContext(),news.getTitle(),news.getContent());
}
}
});
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
News news = mData.get(position);
holder.tv_news_title.setText(news.getTitle());
}
@Override
public int getItemCount() {
return mData.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
TextView tv_news_title;
public ViewHolder(View itemView) {
super(itemView);
tv_news_title= (TextView) itemView.findViewById(R.id.tv_news_title);
}
}
}
}
通過onCreateViewHolder方法中註冊的點選事件,獲取到點選項News的例項,通過isTwoPane判斷是單頁還是雙頁,更新裡面的資料,修改NewsTitleFragment中的程式碼如下:
public class NewsTitleFragment extends Fragment{
private boolean isTowPane;
private RecyclerView news_title_recycler_view;
List<News> newsList=new ArrayList<>();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.news_title_frag, container, false);
news_title_recycler_view = (RecyclerView) view.findViewById(R.id.news_title_recycler_view);
LinearLayoutManager layoutManager=new LinearLayoutManager(getContext());
news_title_recycler_view.setLayoutManager(layoutManager);
NewsAdapter adapter = new NewsAdapter(getNews());
news_title_recycler_view.setAdapter(adapter);
return view;
}
public List<News> getNews() {
for (int i = 1; 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 StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(content);
}
return builder.toString();
}
...
}
執行效果:
單頁模式的執行效果:
點選子選項跳轉:
雙頁模式的執行效果圖:
4.5 小結與點評
本章我們瞭解碎片的基本概念,以及使用場景,掌握了碎片的常用方法,學習了碎片的生命週期,以及動態載入佈局。