隨着社會的發展,我們小時候要寫日記的場景慢慢的淡出了視野。但是很多時候我們仍然希望能夠記錄下自己生活的點點滴滴,藉助學習Android的機會,我們打造一個自己的日記本。
新建工程LikeNotes:
博主寫的文章是記錄學習過程的文章,所以工程看起來比較亂,不會按照app常規的開發流程進行,今天我們主要是學習Fragment的使用,最終實現一個展示日記列表和日記詳細信息的頁面。
Fragment(碎片)概念
Fragment 表示 FragmentActivity 中的行爲或界面的一部分。我們可以在一個 Activity 中組合多個碎片,從而構建多窗格界面,並在多個 Activity 中重複使用某個碎片。我們可以將碎片視爲 Activity 的模塊化組成部分,它具有自己的生命週期,能接收自己的輸入事件,並且您可以在 Activity 運行時添加或移除碎片。
碎片必須始終託管在 Activity 中,其生命週期直接受宿主 Activity 生命週期的影響。例如,當 Activity 暫停時,Activity 的所有碎片也會暫停;當 Activity 被銷燬時,所有碎片也會被銷燬。不過,當 Activity 正在運行(處於已恢復生命週期狀態)時,可以獨立操縱每個碎片,如添加或移除碎片。
Fragment應用場景
Android 在 Android 3.0(API 級別 11)中引入了碎片,主要目的是爲大屏幕(如平板電腦)上更加動態和靈活的界面設計提供支持。由於平板電腦的屏幕尺寸遠勝於手機屏幕尺寸,因而有更多空間可供我們組合和交換界面組件。利用碎片實現此類設計時,無需管理對視圖層次結構做出的複雜更改。通過將 Activity 佈局分成各個碎片,在運行時修改 Activity 的外觀,並在由 Activity 管理的返回棧中保留這些更改。
查閱相關文檔,演示Fragment的例子大多是新聞應用,如下圖:
Fragment的優勢
模塊化(Modularity)
可重用(Reusability)
可適配(Adaptability)
那麼開始我們今天的學習之旅。
不知道大家創建項目的時候有沒有注意到,目前Android已經在創建項目時強制使用androidx包。
AndroidX
是 Android 團隊用於在 Jetpack 中開發、測試、打包和發佈庫以及對其進行版本控制的開源項目。
AndroidX 對原始 Android 支持庫進行了重大改進。與支持庫一樣,AndroidX 與 Android
操作系統分開提供,並與各個 Android 版本向後兼容。AndroidX完全取代了支持庫,不僅提供同等的功能,而且提供了新的庫。此外,AndroidX 還包括以下功能:AndroidX 中的所有軟件包都使用一致的命名空間,以字符串 androidx 開頭。支持庫軟件包已映射到對應的 androidx.*軟件包。
與支持庫不同,AndroidX 軟件包會單獨維護和更新。androidx 軟件包使用嚴格的語義版本控制,從版本 1.0.0 開始。可以單獨更新項目中的 AndroidX 庫。
如果要在新項目中使用 AndroidX,則需要將編譯 SDK 設置爲 Android 9.0(API 級別 28)或更高版本,
並在 gradle.properties 文件中將以下兩個 Android Gradle 插件標記設置爲 true。
android.useAndroidX:如果設置爲 true,Android 插件會使用相應的 AndroidX 庫,而非支持庫。如果未指定,則該標記默認爲 false。
android.enableJetifier:如果設置爲 true,Android 插件會重寫其二進制文件,自動遷移現有的第三方庫以使用 AndroidX。如果未指定,則該標記默認爲 false。
網上也遊蕩了好久,前輩們說在開發一個App的時候,創建一個Activity的基類,然後我們後續創建的Activity都繼承該基類。在基類中實現通用的方法,同時創建一個活動管理器用來管理我們的App。不管怎麼樣,我也決定照着把這個結構給搬過來。
這裏爲了看Log方便,所以直接將Log使用的TAG 使用"LN-
"+類名的形式定義,在logcat過濾的時候,直接輸入我們的應用程序名稱即可。後面有時間我們實現一個簡單的Log工具類,讓輸出更符合我們各自的需求,
注意:TAG的長度最多23個字符,所以我們添加前綴的情況下,Activity的名稱不能超過20個字符,我們先這樣用吧,這裏只是爲了查看方便。
BaseActivity
package com.qiushangge.likenotes.base;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public abstract class BaseActivity extends AppCompatActivity {
// AppCompatActivity名稱
protected final String TAG = "LN-".concat(getClass().getSimpleName());
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityCollector.add(this);
Log.d(TAG, TAG);
//加載佈局文件
setContentView(getActivityLayout());
// 初始化組件
initActivityView();
//實現事件監聽
initActivityListener();
//初始話數據
initActivityData();
}
/**
* 從活動管理器中移除Activity
*/
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.remove(this);
}
/**
* 完成必要的數據初始化
*/
protected abstract void initActivityData();
/**
* 實現事件監聽
*/
protected abstract void initActivityListener();
/**
* 初始化組件
*/
protected abstract void initActivityView();
/**
* @return 返回佈局文件資源id
*/
protected abstract int getActivityLayout();
}
ActivityCollector
package com.qiushangge.likenotes.base;
import android.app.Activity;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
public class ActivityCollector {
private static final String TAG = "LN-ActivityCollector";
/**
* 緩存程序Activity
*/
private static List<Activity> allActivities = new ArrayList<>();
/**
* 向活動管理器中添加Activity
*
* @param activity 創建的Activity實例
*/
public static void add(Activity activity) {
allActivities.add(activity);
Log.d(TAG, "當前創建活動: ".concat(activity.getClass().getSimpleName()));
}
/**
* 從活動管理器中移除Activity
*
* @param activity 銷燬的Activity實例
*/
public static void remove(Activity activity) {
allActivities.remove(activity);
Log.d(TAG, "當前銷燬活動: ".concat(activity.getClass().getSimpleName()));
}
/**
* 銷燬所有Activity實例,退出應用程序
*/
public static void finishAll() {
for (Activity activity : allActivities) {
if (!activity.isFinishing()) {
activity.finish();
}
}
allActivities.clear();
}
}
BaseFragment
package com.qiushangge.likenotes.base;
import androidx.fragment.app.Fragment;
public class BaseFragment extends Fragment {
// 獲取當前Fragment名稱
protected final String TAG = "LN-".concat(getClass().getSimpleName());
}
目前我們就先做這麼多,更多功能後面有需求我們在完善。
修改創建好的工程,讓啓動活動繼承我們的BaseActivity,並實現其抽象方法。
public class NotePageActivity extends BaseActivity {
@Override
protected void initActivityData() {
}
@Override
protected void initActivityListener() {
Log.d(TAG, "initActivityListener: ");
}
@Override
protected void initActivityView() {
Log.d(TAG, "initActivityView: ");
}
@Override
protected int getActivityLayout() {
return R.layout.activity_notes_page;
}
}
這裏我們打印相關log,觀察程序執行情況:
好了,關於base類的簡單封裝就到這裏,看個人編程習慣了。
首先,我們創建一個NoteItem的java類,用來創建一篇日記。日記內容中最常見的屬性有標題,內容以及創建時間,另外爲了後續方便,我們需要給日記再加一個唯一標識。
最終代碼如下:
package com.qiushangge.likenotes.note;
import android.util.Log;
import java.util.Date;
import java.util.UUID;
public class NoteItem {
private static final String TAG = "NoteItem";
//日記唯一標識
private UUID uid;
//日記標題
private String noteTitle;
//日記內容
private String noteContent;
//日記創建時間
private Date dateCreate;
//日記修改時間
private Date dateUpdate;
public NoteItem() {
uid = UUID.randomUUID();
dateCreate = new Date();
Log.d(TAG, uid.toString());
Log.d(TAG, dateCreate.toString());
}
public UUID getUid() {
return uid;
}
public void setUid(UUID uid) {
this.uid = uid;
}
public String getNoteTitle() {
return noteTitle;
}
public void setNoteTitle(String noteTitle) {
this.noteTitle = noteTitle;
}
public String getNoteContent() {
return noteContent;
}
public void setNoteContent(String noteContent) {
this.noteContent = noteContent;
}
public Date getDateCreate() {
return dateCreate;
}
public void setDateCreate(Date dateCreate) {
this.dateCreate = dateCreate;
}
public Date getDateUpdate() {
return dateUpdate;
}
public void setDateUpdate(Date dateUpdate) {
this.dateUpdate = dateUpdate;
}
}
在創建fragment之前我們先來看其一些基本知識,然後完善下我們的BaseFragment類。
創建碎片,我們必須創建 Fragment 的子類(或已有其子類)。Fragment 類的代碼與 Activity 非常相似。它包含與 Activity 類似的回調方法,如 onCreate()、onStart()、onPause() 和 onStop()。實際上,如果要將現有 Android 應用轉換爲碎片,可能只需將代碼從 Activity 的回調方法移入碎片相應的回調方法中。
通常,至少應實現以下生命週期方法:
onCreate()
系統會在創建碎片時調用此方法。當碎片經歷暫停或停止狀態繼而恢復後,如果希望保留此碎片的基本組件,則應在實現中將其初始化。
onCreateView()
系統會在碎片首次繪製其界面時調用此方法。如要爲碎片繪製界面,此方法中返回的 View 必須是碎片佈局的根視圖。如果碎片未提供界面,可以返回 null。
onPause()
系統會將此方法作爲用戶離開片段的第一個信號(但並不總是意味着此片段會被銷燬)進行調用。通常,應在此方法內確認在當前用戶會話結束後仍然有效的。
生命週期示意圖:
同BaseActivity,我們同樣提供三個抽象方法,用來指定佈局文件,初始化控件以及添加事件監聽。
修改後的BaseFragment:
package com.qiushangge.likenotes.base;
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 abstract class BaseFragment extends Fragment {
// 獲取當前Fragment名稱
protected final String TAG = "LN-".concat(getClass().getSimpleName());
protected View fragmentRoot;
/**
* @param savedInstanceState
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
/**
* @param savedInstanceState
*/
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 初始化組件
initFragmentView();
//實現事件監聽
initFragmentListener();
//初始化數據
initFragmentData();
}
/**
* @param inflater
* @param container
* @param savedInstanceState
* @return
*/
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (fragmentRoot == null) {
// 初始化當前的根佈局, 但是不在創建時就添加到 container 裏面
fragmentRoot = inflater.inflate(getFragmentLayout(), container, false);
} else {
if (fragmentRoot.getParent() != null) {
// 把當前 Root 從其父控件中移除
((ViewGroup) fragmentRoot.getParent()).removeView(fragmentRoot);
}
}
return fragmentRoot;
}
/**
*
*/
@Override
public void onPause() {
super.onPause();
}
/**
* 獲取佈局文件
*
* @return
*/
protected abstract int getFragmentLayout();
/**
* 完成必要的數據初始化
*/
protected abstract void initFragmentData();
/**
* 實現事件監聽
*/
protected abstract void initFragmentListener();
/**
* 初始化組件
*/
protected abstract void initFragmentView();
}
這裏我們簡單看下inflate()方法參數:
View inflate (int resource,
ViewGroup root,
boolean attachToRoot)
resource
: 佈局的資源 ID。
root
: 將作爲擴展布局父項的 ViewGroup。
attachToRoot
: 指示是否應在擴展期間將擴展布局附加至 ViewGroup。
接下來我們創建NoteCreateFragment:
修改NoteCreateFragment文件,使其繼承自我們自定義的BaseFragment類,然後使用Alt+Enter快捷鍵快速實現父類抽象方法。
接下來我們去完成今天的NoteCreateFragment佈局頁面。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
tools:context=".fragment.NoteCreateFragment">
<EditText
android:id="@+id/et_note_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/note_title_hint"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_note_create_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
app:layout_constraintTop_toBottomOf="@id/et_note_title" />
<EditText
android:id="@+id/et_note_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@android:color/transparent"
android:gravity="top"
android:hint="@string/note_title_content_hint"
android:singleLine="false"
app:layout_constraintBottom_toTopOf="@id/btn_save"
app:layout_constraintTop_toBottomOf="@id/tv_note_create_date" />
<Button
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/note_save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_note_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
完成後顯示的頁面如下:
相關字符串資源:
<string name="note_title_hint">日記標題</string>
<string name="note_title_content_hint">日記內容</string>
<string name="tv_note_title">標題</string>
<string name="note_save">保存日記</string>
當然了,現在頁面比較醜陋,等後面在同一優化。
NoteCreateFragment實現數據的保存:
public class NoteCreateFragment extends BaseFragment {
//日記記錄實例
private NoteItem noteItem;
private EditText etNodeTitle;
private EditText etNodeContent;
private TextView tvNodeCreateDate;
private Button btnSave;
@Override
protected int getFragmentLayout() {
return R.layout.fragment_note_create;
}
@Override
protected void initFragmentData() {
noteItem = new NoteItem();
}
@Override
protected void initFragmentListener() {
btnSave.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
noteItem.setDateCreate(new Date());
tvNodeCreateDate.setText(noteItem.getDateCreate().toString());
Toast.makeText(getContext(), R.string.note_save_success, Toast.LENGTH_SHORT).show();
Log.d(TAG, "創建日期:"+noteItem.getDateCreate().toString());
Log.d(TAG, "標題:"+noteItem.getNoteTitle());
Log.d(TAG, "日記內容:"+noteItem.getNoteContent());
}
});
etNodeTitle.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
noteItem.setNoteTitle(s.toString());
}
@Override
public void afterTextChanged(Editable s) {
}
});
etNodeContent.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
noteItem.setNoteContent(s.toString());
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
@Override
protected void initFragmentView() {
etNodeTitle = fragmentRoot.findViewById(R.id.et_note_title);
etNodeContent = fragmentRoot.findViewById(R.id.et_note_content);
tvNodeCreateDate = fragmentRoot.findViewById(R.id.tv_note_create_date);
btnSave = fragmentRoot.findViewById(R.id.btn_save);
}
}
在android中我們有下面兩種方式使用Fragment:
Activity 的佈局文件內聲明片段
<?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:name="com.example.news.ArticleListFragment"
android:id="@+id/list"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />
<fragment android:name="com.example.news.ArticleReaderFragment"
android:id="@+id/viewer"
android:layout_weight="2"
android:layout_width="0dp"
android:layout_height="match_parent" />
</LinearLayout>
android:name 屬性指定要在佈局中進行實例化的 Fragment 類。
通過編程方式將片段添加到某個現有 ViewGroup
在 Activity 中執行碎片事務(如添加、移除或替換片段),則必須使用 FragmentTransaction 中的 API。如下所示, NotePageActivity獲取一個 FragmentTransaction 實例:
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
然後,使用 add() 方法添加一個碎片段,指定要添加的片段以及將其插入哪個視圖
NoteCreateFragment fragment = new NoteCreateFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
傳遞到 add() 的第一個參數是 ViewGroup,即應放置碎片的位置,由資源 ID 指定,第二個參數是要添加的碎片。
activity_notes_page.xml代碼:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.NotePageActivity">
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
一般我們會使用FrameLayout用作承載碎片的容器。
一旦通過 FragmentTransaction 做出了更改,就必須調用 commit() 以使更改生效。
NotePageActivity最終代碼如下:
public class NotePageActivity extends BaseActivity {
@Override
protected void initActivityData() {
}
@Override
protected void initActivityListener() {
}
@Override
protected void initActivityView() {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
NoteCreateFragment fragment = new NoteCreateFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}
@Override
protected int getActivityLayout() {
return R.layout.activity_notes_page;
}
}
運行程序: