本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了,
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以后閱讀和查閱,最后,感激感激郭霖先生提供這么好的書籍,
第4章 手機平板要兼顧——探究碎片
平板電腦和手機最大的區別就在于螢屏的大小,一般手機螢屏的大小會在3英寸到6英寸之間,而一般平板電腦螢屏的大小會在7英寸到10英寸之間,
螢屏大小差距過大有可能會讓同樣的界面在視覺效果上有較大的差異,比如一些界面在手機上看起來非常美觀,但在平板電腦上看起來就可能會有控制元件被過分拉長、元素之間空隙過大等情況,作為一名專業的Android開發人員,能夠同時兼顧手機和平板的開發是我們必須做到的事情,Android自3.0版本開始引入了碎片的概念,它可以讓界面在平板上更好地展示,
4.1 碎片是什么
碎片(Fragment)是一種可以嵌入在活動當中的UI片段,它能讓程式更加合理和充分地利用大螢屏的空間,因而在平板上應用得非常廣泛,
它和活動實在是太像了,同樣都能包含布局,同樣都有自己的生命周期,你甚至可以將碎片理解成一個迷你型的活動,雖然這個迷你型的活動有可能和普通的活動是一樣大的,
那么究竟要如何使用碎片才能充分地利用平板螢屏的空間呢?想象我們正在開發一個新聞應用,其中一個界面使用RecyclerView展示了一組新聞的標題,當點擊了其中一個標題時,就打開另一個界面顯示新聞的詳細內容,如果是在手機中設計,我們可以將新聞標題串列放在一個活動中,將新聞的詳細內容放在另一個活動中,如圖所示:

可是如果在平板上也這么設計,那么新聞標題串列將會被拉長至填充滿整個平板的螢屏,而新聞的標題一般都不會太長,這樣將會導致界面上有大量的空白區域,如圖:

因此,更好的設計方案是將新聞標題串列界面和新聞詳細內容界面分別放在兩個碎片中,然后在同一個活動里引入這兩個碎片,這樣就可以將螢屏空間充分地利用起來了,如圖:

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_
android:layout_height="match_parent">
<Button
android:layout_
android:layout_height="wrap_content"
android:id="@+id/button"
android:layout_gravity="center_horizontal"
android:text="Button"/>
</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_
android:layout_height="match_parent">
<TextView
android:layout_
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="This is right fragment"/>
</LinearLayout>
將這個布局的背景色設定成了綠色,并放置了一個TextView用于顯示一段文本,
接著新建一個LeftFragment類,并讓它繼承自Fragment,
(書中所述,:“注意,這里可能會有兩個不同包下的Fragment供你選擇,一個是系統內置的android.app.Fragment,一個是support-v4庫中的android.support.v4.app.Fragment,這里我強烈建議你使用support-v4庫中的Fragment,因為它可以讓碎片在所有Android系統版本中保持功能一致性,
比如說在Fragment中嵌套使用Fragment,這個功能是在Android 4.2系統中才開始支持的,如果你使用的是系統內置的Fragment,那么很遺憾,4.2系統之前的設備運行你的程式就會崩潰,而使用support-v4庫中的Fragment就不會出現這個問題,只要你保證使用的是最新的support-v4庫就可以了,另外,我們并不需要在build.gradle檔案中添加support-v4庫的依賴,因為build.gradle檔案中已經添加了appcompat-v7庫的依賴,而這個庫會將support-v4庫也一起引入進來,”)
實際上,android.support.v4.app.Fragment 沒有;android.app.Fragment 過時;用的是import androidx.fragment.app.Fragment;
現在撰寫一下LeftFragment中的代碼,如下所示:
package com.zhouzhou.fragmenttest;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class LeftFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.left_fragment,container,false);
return view;
}
}
這里僅僅是重寫了Fragment的onCreateView()方法,然后在這個方法中通過LayoutInflater的inflate()方法將剛才定義的left_fragment布局動態加載進來,整個方法簡單明了,接著我們用同樣的方法再新建一個RightFragment,代碼如下所示:
package com.zhouzhou.fragmenttest;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class RightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.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"
android:orientation="horizontal"
android:layout_
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.zhouzhou.fragmenttest.LeftFragment"
android:layout_
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.zhouzhou.fragmenttest.RightFragment"
android:layout_
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
用了<fragment>標簽在布局中添加碎片,其中還需要通過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_
android:layout_height="match_parent">
<TextView
android:layout_
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="Another right fragment"/>
</LinearLayout>
這個布局檔案的代碼和right_fragment.xml中的代碼基本相同,只是將背景色改成了黃色,并將顯示的文字改了改,然后新建AnotherRightFragment作為另一個右側碎片,代碼如下所示:
package com.zhouzhou.fragmenttest;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class AnotherRightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.another_right_fragment,container,false);
return view;
}
}
代碼同樣非常簡單,在onCreateView()方法中加載了剛剛創建的another_right_fragment布局,這樣我們就準備好了另一個碎片,接下來看一下如何將它動態地添加到活動當中,修改activity_main.xml,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.zhouzhou.fragmenttest.LeftFragment"
android:layout_
android:layout_height="match_parent"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/right_layout"
android:layout_
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
可以看到,現在將右側碎片替換成了一個FrameLayout中,在上一章中學過,這是Android中最簡單的一種布局,所有的控制元件默認都會擺放在布局的左上角,由于這里僅需要在布局里放入一個碎片,不需要任何定位,因此非常適合使用FrameLayout,
下面在代碼中向FrameLayout里添加內容,從而實作動態添加碎片的功能,修改MainActivity中的代碼,如下所示:
package com.zhouzhou.fragmenttest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
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 = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout,fragment);
transaction.commit();
}
}
首先給左側碎片中的按鈕注冊了一個點擊事件,然后呼叫replaceFragment()方法動態添加了RightFragment這個碎片,當點擊左側碎片中的按鈕時,又會呼叫replaceFragment()方法將右側碎片替換成AnotherRightFragment,
結合replaceFragment()方法中的代碼可以看出,動態添加碎片主要分為5步,
- 創建待添加的碎片實體,
- 獲取FragmentManager,在活動中可以直接通過呼叫getSupportFragmentManager()方法得到,
- 開啟一個事務,通過呼叫beginTransaction()方法開啟,
- 向容器內添加或替換碎片,一般使用replace()方法實作,需要傳入容器的id和待添加的碎片實體,
- 提交事務,呼叫commit()方法來完成,
這樣就完成了在活動中動態添加碎片的功能,重新運行程式,可以看到和之前相同的界面,然后點擊一下按鈕,效果如圖:

4.2.3 在碎片中模擬回傳堆疊
成功實作了向活動中動態添加碎片的功能,不過嘗試一下就會發現,通過點擊按鈕添加了一個碎片之后,這時按下Back鍵程式就會直接退出,
如果這里想模仿類似于回傳堆疊的效果,按下Back鍵可以回到上一個碎片,該如何實作呢?
其實很簡單,FragmentTransaction中提供了一個addToBackStack()方法,可以用于將一個事務添加到回傳堆疊中,修改MainActivity中的代碼,如下所:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
private void replaceFragment(Fragment fragment) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout,fragment);
//在事務提交之前呼叫了FragmentTransaction的addToBackStack()方法,它可以接收一個名字用于描述回傳堆疊的狀態,一般傳入null即可,
transaction.addToBackStack(null);
transaction.commit();
}
}
現在重新運行程式,并點擊按鈕將AnotherRightFragment添加到活動中,然后按下Back鍵,會發現程式并沒有退出,而是回到了RightFragment界面,繼續按下Back鍵,RightFragment界面也會消失,再次按下Back鍵,程式才會退出,
4.2.4 碎片和活動之間進行通信
雖然碎片都是嵌入在活動中顯示的,可是實際上它們的關系并沒有那么親密,
碎片和活動都是各自存在于一個獨立的類當中的,它們之間并沒有那么明顯的方式來直接進行通信,如果想要在活動中呼叫碎片里的方法,或者在碎片中呼叫活動里的方法,應該如何實作呢?
為了方便碎片和活動之間進行通信,FragmentManager提供了一個類似于findViewById()的方法,專門用于從布局檔案中獲取碎片的實體,代碼如下所示:
RightFragment rightFragment = (RightFragment) getSupportFragmentManager().findFragmentById(R.id.right_fragment);
呼叫FragmentManager的findFragmentById()方法,可以在活動中得到相應碎片的實體,然后就能輕松地呼叫碎片里的方法了,
掌握了如何在活動中呼叫碎片里的方法,那在碎片中又該怎樣呼叫活動里的方法呢?其實這就更簡單了,在每個碎片中都可以通過呼叫getActivity()方法來得到和當前碎片相關聯的活動實體,代碼如下所示:
MainActivity activity = (MainActivity) getActivity();
有了活動實體之后,在碎片中呼叫活動里的方法就變得輕而易舉了,另外當碎片中需要使用Context物件時,也可以使用getActivity()方法,因為獲取到的活動本身就是一個Context物件,
這時不知道你心中會不會產生一個疑問:既然碎片和活動之間的通信問題已經解決了,那么碎片和碎片之間可不可以進行通信呢?
這個問題并沒有看上去那么復雜,它的基本思路非常簡單,首先在一個碎片中可以得到與它相關聯的活動,然后再通過這個活動去獲取另外一個碎片的實體,這樣也就實作了不同碎片之間的通信功能,因此這里我們的答案是肯定的,
4.3 碎片的生命周期
和活動一樣,碎片也有自己的生命周期,并且它和活動的生命周期實在是太像了,
4.3.1 碎片的狀態和回呼
- 運行狀態
當一個碎片是可見的,并且它所關聯的活動正處于運行狀態時,該碎片也處于運行狀態,
- 暫停狀態
當一個活動進入暫停狀態時(由于另一個未占滿螢屏的活動被添加到了堆疊頂),與它相關聯的可見碎片就會進入到暫停狀態,
- 停止狀態
當一個活動進入停止狀態時,與它相關聯的碎片就會進入到停止狀態,或者通過呼叫FragmentTransaction的remove()、replace()方法將碎片從活動中移除,但如果在事務提交之前呼叫addToBackStack()方法,這時的碎片也會進入到停止狀態,總的來說,進入停止狀態的碎片對用戶來說是完全不可見的,有可能會被系統回收,
- 銷毀狀態
碎片總是依附于活動而存在的,因此當活動被銷毀時,與它相關聯的碎片就會進入到銷毀狀態,或者通過呼叫FragmentTransaction的remove()、replace()方法將碎片從活動中移除,但在事務提交之前并沒有呼叫addToBackStack()方法,這時的碎片也會進入到銷毀狀態,
同樣地,Fragment類中也提供了一系列的回呼方法,以覆寫碎片生命周期的每個環節,其中,活動中有的回呼方法,碎片中幾乎都有,不過碎片還提供了一些附加的回呼方法,那我們就重點看一下這幾個回呼,
- onAttach(),當碎片和活動建立關聯的時候呼叫,
- onCreateView(),為碎片創建視圖(加載布局)時呼叫,
- onActivityCreated(),確保與碎片相關聯的活動一定已經創建完畢的時候呼叫,
- onDestroyView(),當與碎片關聯的視圖被移除的時候呼叫,
- onDetach(),當碎片和活動解除關聯的時候呼叫,
碎片完整的生命周期示意圖可參考圖,圖片源自Android官網,

4.3.2 體驗碎片的生命周期
為了能夠更加直觀地體驗碎片的生命周期,還是通過一個例子來實踐一下,例子很簡單,仍然是在FragmentTest專案的基礎上改動的,修改RightFragment中的代碼,如下所示:
package com.zhouzhou.fragmenttest;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class RightFragment extends Fragment {
public static final String TAG = "RightFragment";
@Override
public void onAttach(@NonNull 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(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.d(TAG,"onCreateView");
View view = inflater.inflate(R.layout.right_fragment,container,false);
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");
}
}
在RightFragment中的每一個回呼方法里都加入了列印日志的代碼,然后重新運行程式,這時觀察logcat中的列印資訊,如圖:

可以看到,當RightFragment第一次被加載到螢屏上時,會依次執行onAttach()、onCreate()、onCreateView()、onActivityCreated()、onStart()和onResume()方法,然后點擊LeftFragment中的按鈕,此時列印資訊如圖:

由于AnotherRightFragment替換了RightFragment,此時的RightFragment進入了停止狀態,因此onPause()、onStop()和onDestroyView()方法會得到執行,
當然如果在替換的時候沒有呼叫addToBackStack()方法,此時的RightFragment就會進入銷毀狀態,onDestroy()和onDetach()方法就會得到執行,接著按下Back鍵,RightFragment會重新回到螢屏,列印資訊如圖:

由于RightFragment重新回到了運行狀態,因此onCreateView()、onActivityCreated()、onStart()和onResume()方法會得到執行,注意此時onCreate()方法并不會執行,因為我們借助了addToBackStack()方法使得RightFragment并沒有被銷毀,現在再次按下Back鍵,列印資訊如圖:

依次會執行onPause()、onStop()、onDestroyView()、onDestroy()和onDetach()方法,最終將碎片銷毀掉,這樣碎片完整的生命周期體驗了一遍,另外值得一提的是,在碎片中你也是可以通過onSaveInstanceState()方法來保存資料的,因為進入停止狀態的碎片有可能在系統記憶體不足的時候被回收,保存下來的資料在onCreate()、onCreateView()和onActivityCreated()這3個方法中你都可以重新得到,它們都含有一個Bundle型別的savedInstanceState引數,
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:orientation="horizontal"
android:layout_
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.zhouzhou.fragmenttest.LeftFragment"
android:layout_
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:orientation="horizontal"
android:layout_
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.zhouzhou.fragmenttest.LeftFragment"
android:layout_
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.zhouzhou.fragmenttest.RightFragment"
android:layout_
android:layout_height="match_parent"
android:layout_weight="3"/>
</LinearLayout>
可以看到,layout/activity_main布局只包含了一個碎片,即單頁模式,而layout-large/activity_main布局包含了兩個碎片,即雙頁模式,其中large就是一個限定符,那些螢屏被認為是large的設備就會自動加載layout-large檔案夾下的布局,而小螢屏的設備則還是會加載layout檔案夾下的布局,
然后將MainActivity中replaceFragment()方法里的代碼注釋掉,并在平板模擬器上重新運行程式,效果如圖:

再啟動一個手機模擬器,并在這個模擬器上重新運行程式,效果如圖:

這樣我們就實作了在程式運行時動態加載布局的功能,Android中一些常見的限定符可以參考下表,

4.4.2 使用最小寬度限定符
使用large限定符成功解決了單頁雙頁的判斷問題,不過很快又有一個新的問題出現了,large到底是指多大呢?有的時候我們希望可以更加靈活地為不同設備加載布局,不管它們是不是被系統認定為large,這時就可以使用最小寬度限定符(Smallest-width Qualifier)了,最小寬度限定符允許我們對螢屏的寬度指定一個最小值(以dp為單位),然后以這個最小值為臨界點,螢屏寬度大于這個值的設備就加載一個布局,螢屏寬度小于這個值的設備就加載另一個布局,在res目錄下新建layout-sw600dp檔案夾,然后在這個檔案夾下新建activity_main.xml布局,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_
android:layout_height="match_parent">
<fragment
android:layout_
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/left_fragment"
android:name="com.zhouzhou.fragmenttest.LeftFragment"/>
<fragment
android:layout_
android:layout_height="match_parent"
android:layout_weight="3"
android:id="@+id/right_fragment"
android:name="com.zhouzhou.fragmenttest.RightFragment"/>
</LinearLayout>
這就意味著,當程式運行在螢屏寬度大于等于600dp的設備上時,會加載layout-sw600dp/activity_main布局,當程式運行在螢屏寬度小于600dp的設備上時,則仍然加載默認的layout/activity_main布局,
4.5 碎片的最佳實踐——一個簡易版的新聞應用
前面有提到過,碎片很多時候都是在平板開發當中使用的,主要是為了解決螢屏空間不能充分利用的問題,那是不是就表明,我們開發的程式都需要提供一個手機版和一個Pad版呢?
確實有不少公司都是這么做的,但是這樣會浪費很多的人力物力,因為維護兩個版本的代碼成本很高,每當增加什么新功能時,需要在兩份代碼里各寫一遍,每當發現一個bug時,需要在兩份代碼里各修改一次,因此今天我們最佳實踐的內容就是,教你如何撰寫同時兼容手機和平板的應用程式,
現在就將運用本章中所學的知識來撰寫一個簡易版的新聞應用,并且要求它是可以同時兼容手機和平板的,
新建好一個FragmentBestPractice專案,并且會使用到RecyclerView,要準備好一個新聞的物體類,新建類News,代碼如下所示:
package com.zhouzhou.fragmentbestpractice;
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類的代碼還是比較簡單的,title欄位表示新聞標題,content欄位表示新聞內容,接著新建布局檔案news_content_frag.xml,用于作為新聞內容的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_height="match_parent">
<LinearLayout
android:layout_
android:layout_height="match_parent"
android:id="@+id/visibility_layout"
android:orientation="vertical"
android:visibility="invisible">
<TextView
android:layout_
android:layout_height="wrap_content"
android:id="@+id/news_title"
android:gravity="center"
android:padding="10dp"
android:textSize="20sp"/>
<View
android:layout_
android:layout_height="1dp"
android:background="#000"/>
<TextView
android:layout_
android:layout_height="0dp"
android:id="@+id/news_content"
android:layout_weight="1"
android:padding="15dp"
android:textSize="18sp"/>
</LinearLayout>
<View
android:layout_
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:background="#000"/>
</RelativeLayout>
新聞內容的布局主要可以分為兩個部分,頭部部分顯示新聞標題,正文部分顯示新聞內容,中間使用一條細線分隔開,這里的細線是利用View來實作的,將View的寬或高設定為1dp,再通過background屬性給細線設定一下顏色就可以了,這里我們把細線設定成黑色,
然后再新建一個NewsContentFragment類,繼承自Fragment,代碼如下所示:
package com.zhouzhou.fragmentbestpractice;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class NewsContentFragment extends Fragment {
private View view;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
view = inflater.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);
TextView newsTitleText = (TextView) view.findViewById(R.id.news_title);
TextView newsContentText = (TextView) view.findViewById(R.id.news_content);
newsTitleText.setText(newsTitle);//重繪新聞的標題
newsContentText.setText(newsContent);//重繪新聞內容
}
}
- 首先在onCreateView()方法里加載了我們剛剛創建的news_content_frag布局,
- 又提供了一個refresh()方法,這個方法就是用于將新聞的標題和內容顯示在界面上的,這里通過findViewById()方法分別獲取到新聞標題和內容的控制元件,然后將方法傳遞進來的引數設定進去,
這樣我們就把新聞內容的碎片和布局都創建好了,但是它們都是在雙頁模式中使用的,如果想在單頁模式中使用的話,還需要再創建一個活動,右擊com.zhouzhou.fragmentbestpractice包→New→Activity→EmptyActivity,新建一個NewsContentActivity,并將布局名指定成news_content,然后修改news_content.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_
android:layout_height="match_parent">
<fragment
android:layout_
android:layout_height="match_parent"
android:id="@+id/news_content_fragment"
android:name="com.zhouzhou.fragmentbestpractice.NewsContentFragment"/>
</LinearLayout>
這里我們充分發揮了代碼的復用性,直接在布局中引入了NewsContentFragment,這樣也就相當于把news_content_frag布局的內容自動加了進來,然后修改NewsContentActivity中的代碼,如下所示:
package com.zhouzhou.fragmentbestpractice;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
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);
String newsTitle = getIntent().getStringExtra("news_title");//獲取傳入的新聞標題
String newsContent = getIntent().getStringExtra("news_content");//獲取傳入的新聞的內容
NewsContentFragment newsContentFragment = (NewsContentFragment) getSupportFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(newsTitle,newsContent);//重繪NewsContentFragment界面
}
}
可以看到,在onCreate()方法中通過Intent獲取到了傳入的新聞標題和新聞內容,然后呼叫FragmentManager的findFragmentById()方法得到了NewsContentFragment的實體,接著呼叫它的refresh()方法,并將新聞的標題和內容傳入,就可以把這些資料顯示出來了,
注意這里我們還提供了一個actionStart()方法,如果忘記的話就再去閱讀一遍2.6.3小節吧,接下來還需要再創建一個用于顯示新聞串列的布局,新建news_title_frag.xml,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_
android:layout_height="match_parent"
android:id="@+id/news_title_recycler_view"/>
</LinearLayout>
這個布局里面只有一個用于顯示新聞串列的RecyclerView,既然要用到RecyclerView,那么就必定少不了子項的布局,新建news_item.xml作為RecyclerView子項的布局,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_height="wrap_content"
android:id="@+id/news_title"
android:maxLines="1"
android:ellipsize="end"
android:textSize="18sp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"/>
子項的布局也非常簡單,只有一個TextView,
- android:padding表示給控制元件的周圍加上補白,這樣不至于讓文本內容會緊靠在邊緣上,
- android:maxLines設定為1表示讓這個TextView只能單行顯示,
- android:ellipsize用于設定當文本內容超出控制元件寬度時,文本的縮略方式,end表示在尾部進行縮略,
既然新聞串列和子項的布局都已經創建好了,那么接下來我們就需要一個用于展示新聞串列的地方,這里新建NewsTitleFragment作為展示新聞串列的碎片,代碼如下所示:
package com.zhouzhou.fragmentbestpractice;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class NewsTitleFragment extends Fragment {
private boolean isTwoPane;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.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){
isTwoPane = true;//可以找到news_content_layout布局時,為雙頁模式
}else {
isTwoPane = false;//找不到news_content_layout布局時,為單頁模式
}
}
}
NewsTitleFragment中并沒有多少代碼,在onCreateView()方法中加載了news_title_frag布局,
注意看一下onActivityCreated()方法,這個方法通過在活動中能否找到一個id為news_content_layout的View來判斷當前是雙頁模式還是單頁模式,因此我們需要讓這個id為news_content_layout的View只在雙頁模式中才會出現,那么怎樣才能實作這個功能呢?其實并不復雜,只需要借助我們剛剛學過的限定符就可以了,
首先修改activity_main.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_
android:layout_height="match_parent">
<fragment
android:layout_
android:layout_height="match_parent"
android:id="@+id/news_title_fragment"
android:name="com.zhouzhou.fragmentbestpractice.NewsTitleFragment"/>
</FrameLayout>
上述代碼表示,在單頁模式下,只會加載一個新聞標題的碎片,然后新建layout-sw600dp檔案夾,在這個檔案夾下再新建一個activity_main.xml檔案,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_
android:layout_height="match_parent">
<fragment
android:layout_
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/news_title_fragment"
android:name="com.zhouzhou.fragmentbestpractice.NewsTitleFragment"/>
<FrameLayout
android:layout_
android:layout_height="match_parent"
android:layout_weight="3"
android:id="@+id/news_content_layout"/>
<fragment
android:layout_
android:layout_height="match_parent"
android:id="@+id/news_content_fragment"
android:name="com.zhouzhou.fragmentbestpractice.NewsContentFragment"/>
</LinearLayout>
可以看出,在雙頁模式下我們同時引入了兩個碎片,并將新聞內容的碎片放在了一個FrameLayout布局下,而這個布局的id正是news_content_layout,因此,能夠找到這個id的時候就是雙頁模式,否則就是單面模式,
現在還剩下至關重要的一點,就是在NewsTitleFragment中通過RecyclerView將新聞串列展示出來,在NewsTitleFragment中新建一個內部類NewsAdapter來作為RecyclerView的配接器,如下所示:
package com.zhouzhou.fragmentbestpractice;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class NewsTitleFragment extends Fragment {
private boolean isTwoPane;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.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){
isTwoPane = true;//可以找到news_content_layout布局時,為雙頁模式
}else {
isTwoPane = false;//找不到news_content_layout布局時,為單頁模式
}
}
class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder>{
private List<News> mNewsList;
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull 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 view) {
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(@NonNull ViewHolder holder, int position) {
News news = mNewsList.get(position);
holder.newsTitleText.setText(news.getTitle());
}
@Override
public int getItemCount() {
return mNewsList.size();
}
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;
}
}
}
需要注意的是,之前我們都是將配接器寫成一個獨立的類,其實也是可以寫成內部類的,這里寫成內部類的好處就是可以直接訪問NewsTitleFragment的變數,比如isTwoPane,觀察一下onCreateViewHolder()方法中注冊的點擊事件,首先獲取到了點擊項的News實體,然后通過isTwoPane變數來判斷當前是單頁還是雙頁模式,如果是單頁模式,就啟動一個新的活動去顯示新聞內容,如果是雙頁模式,就更新新聞內容碎片里的資料,
最后就是向RecyclerView中填充資料了,修改NewsTitleFragment中的代碼,如下所示:
package com.zhouzhou.fragmentbestpractice;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class NewsTitleFragment extends Fragment {
private boolean isTwoPane;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable 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("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();
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_layout)!= null){
isTwoPane = true;//可以找到news_content_layout布局時,為雙頁模式
}else {
isTwoPane = false;//找不到news_content_layout布局時,為單頁模式
}
}
class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder>{
private List<News> mNewsList;
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull 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 view) {
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(@NonNull ViewHolder holder, int position) {
News news = mNewsList.get(position);
holder.newsTitleText.setText(news.getTitle());
}
@Override
public int getItemCount() {
return mNewsList.size();
}
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;
}
}
}
可以看到,onCreateView()方法中添加了RecyclerView標準的使用方法,在碎片中使用RecyclerView和在活動中使用幾乎是一模一樣的,另外,這里呼叫了getNews()方法來初始化50條模擬新聞資料,同樣使用了一個getRandomLengthContent()方法來隨機生成新聞內容的長度,以保證每條新聞的內容差距比較大,
這樣所有的撰寫作業就已經完成了,運行一下吧!首先在手機模擬器上運行,效果如圖:

可以看到許多條新聞的標題,然后點擊第一條新聞,會啟動一個新的活動來顯示新聞的內容,效果如圖:

接下來將程式在平板模擬器上運行,同樣點擊第一條新聞,效果如圖:

到這里為止,已經基本將Android UI相關的重要知識點都學完了,后面在很長一段時間內都不會再系統性地介紹UI方面的知識,而是將結合前面所學的UI知識來更好地講解相應章節的內容,
個人學習筆記,針對本人在自學中遇到的問題,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/472332.html
標籤:Android
