本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,感激 ...
本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼: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_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
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_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>
將這個佈局的背景色設置成了綠色,並放置了一個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_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.zhouzhou.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.zhouzhou.fragmenttest.RightFragment"
android:layout_width="0dp"
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_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="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_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.zhouzhou.fragmenttest.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"/>
</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_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.zhouzhou.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:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.zhouzhou.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.zhouzhou.fragmenttest.RightFragment"
android:layout_width="0dp"
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_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/left_fragment"
android:name="com.zhouzhou.fragmenttest.LeftFragment"/>
<fragment
android:layout_width="0dp"
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_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/visibility_layout"
android:orientation="vertical"
android:visibility="invisible">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/news_title"
android:gravity="center"
android:padding="10dp"
android:textSize="20sp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000"/>
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/news_content"
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>
新聞內容的佈局主要可以分為兩個部分,頭部部分顯示新聞標題,正文部分顯示新聞內容,中間使用一條細線分隔開。這裡的細線是利用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_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_width="match_parent"
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_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
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_width="match_parent"
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_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_width="match_parent"
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_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/news_title_fragment"
android:name="com.zhouzhou.fragmentbestpractice.NewsTitleFragment"/>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
android:id="@+id/news_content_layout"/>
<fragment
android:layout_width="match_parent"
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知識來更好地講解相應章節的內容。