随着社会的发展,我们小时候要写日记的场景慢慢的淡出了视野。但是很多时候我们仍然希望能够记录下自己生活的点点滴滴,借助学习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;
}
}
运行程序: