第一行代码之碎片(fragment)
4.2 碎片的使用方式
4.2.1 碎片的简单用法
新建一个左侧碎片布局 fragment_left.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="按钮"/>
</LinearLayout>
然后新建右侧碎片布局fragment_right.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#00ff00">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="这是右边的fragment"/>
</LinearLayout>
将布局的背景色设置成绿色,并放置了一个 TextView 用于显示一段文本
建议使用 support-v4 库中的 Fragment,因为它可以让碎片在所有 Android 系统版本中保持功能一致性
LeftFragment 的代码如下所示:
public class LeftFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_left, container, false);
return view;
}
}
这里仅仅是重写了 Fragment 的 onCreateView()方法
然后在这个方法中通过 LayoutInflater 的 inflate()方法将刚才定义的 fragment_left 布局动态加载进来
接着我们用同样的方法再新建一个 RightFragment:
public class RightFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_right, container, false);
return view;
}
}
接下来修改 activity_fragment.xml 中的代码,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/fragment_left"
android:name="com.wonderful.myfirstcode.inquiry_fragment.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/fragment_right"
android:name="com.wonderful.myfirstcode.inquiry_fragment.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
可以看到,我们使用了<fragment>标签在布局中添加碎片
通过 android:name 属性来显式指明要添加的碎片类名,注意一 定要将类的包名也加上
这样最简单的碎片示例就已经写好了,运行一下程序,(平板上)效果如图:
4.2.2 动态添加碎片
碎片真正的强大之处在于,它可以在程序运行时动态地添加到活动当中
根据具体情况来动态地添加碎片,你就 可以将程序界面定制得更加多样化
在上一节代码的基础上继续完善,新建 fragment_another_right.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#ffff00">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="这是另外一个右边的fragment"/>
</LinearLayout>
然后新建 AnotherRightFragment 作为另一个右侧碎片,代码如下所示:
public class AnotherRightFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_another_right, container, false);
return view;
}
}
接下来看一下如何将它动态地添加到活动当中。修改 activity_fragment.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/fragment_left"
android:name="com.wonderful.myfirstcode.inquiry_fragment.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"/>
<!--
<fragment
android:id="@+id/fragment_right"
android:name="com.wonderful.myfirstcode.inquiry_fragment.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
-->
</LinearLayout>
可以看到,现在将右侧碎片放在了一个 FrameLayout 中
下面在代码中向 FrameLayout 里添加内容,从而实现动态添加碎片的功能
修改 Activity 中的代码,如下所示:
public class FragmentActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment);
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 fragmentManager = getSupportFragmentManager();
// 开启事务
FragmentTransaction transaction = fragmentManager.beginTransaction();
// 添加或替换碎片
transaction.replace(R.id.right_layout,fragment);
// 提交事务
transaction.commit();
}
}
上述代码,给左侧碎片中的按钮注册了一个点击事件
调用 replaceFragment() 方法动态添加碎片
结合代码可看出,动态添加碎片主要分为 5 步
1. 创建待添加的碎片实例
2. 获取 FragmentManager,在活动中可以直接调用 getSupportFragmentManager()方法得到
3. 开启一个事务,通过调用 beginTransaction()方法开启
4. 向容器内添加或替换碎片,使用 replace() 方法实现,需要传入容器的 id 和待添加的碎片实例。
5. 提交事务,调用 commit()方法来完成。
重新运行程序,效果如图:
4.2.3 在碎片中模拟返回栈
点击按钮添加了一个碎片之后,按下 Back 键程序就会直接退出
如果这里我们想模仿类似于返回栈的效果,按下 Back 键可以回到上一个碎片,该如何实现呢?
FragmentTransaction 中提供了一个 addToBackStack() 方法
可以用于将一个事务添加到返回栈中,修改 Activity 中的代码,如下所示:
public class FragmentActivity extends AppCompatActivity implements View.OnClickListener {
. . .
private void replaceFragment(Fragment fragment){
// 获取FragmentManager
FragmentManager fragmentManager = getSupportFragmentManager();
// 开启事务
FragmentTransaction transaction = fragmentManager.beginTransaction();
// 添加或替换碎片
transaction.replace(R.id.right_layout,fragment);
// 用于描述返回栈的状态
transaction.addToBackStack(null);
// 提交事务
transaction.commit();
}
}
在事务提交之前调用了 FragmentTransaction 的 addToBackStack() 方法
它可以接收一个名字用于描述返回栈的状态,一般传入 null 即可
4.2.4 碎片和活动之间进行通信
为了方便碎片和活动之间进行通信,FragmentManager 提供了一个类似于 findViewById() 的方法
专门用于从布局文件中获取碎片的实例。在活动中调用碎片里的方法:
RightFragment rightFragment = (RightFragment) getFragmentManager().findFragmentById(R.id.right_fragment);
在碎片中调用活动里的方法:通过调用 getActivity()方法来得到和当前碎片相关联的活动实例
代码如下所示:
MainActivity activity = (MainActivity) getActivity();
4.3 碎片的生命周期
和活动 Acitvity 相似,Fragment 类中也提供了一系列的回调方法,以覆盖碎片生命周期的每个环节。其中,活动中有的回调方法,碎片中几乎都有,不过碎片还提供了一些附加的回调方法,重点来看下这几个回调:
- onAttach() 当碎片和活动建立关联的时候调用。
- onCreateView() 为碎片创建视图(加载布局)时调用。
- onActivityCreated() 确保与碎片相关联的活动一定已经创建完毕的时候调用。
- onDestroyView() 当与碎片关联的视图被移除的时候调用。
- onDetach() 当碎片和活动解除关联的时候调用。
另外,在碎片中你也可以通过onSaveInstanceState()方法来保存数据
因为进入停止状态的碎片有可能在系统内存不足的时候被回收
保存下来的数据在 onCreate()、onCreateView() 和 onActivityCreated()这三个方法中
你都可以重新得到,它们都含有一个 Bundle 类型的 savedInstanceState 参数
4.5 碎片的最佳实践——一个简易版的新闻应用
添加后面需要用到的 RecyclerView 依赖库
接下来,准备好一个新闻的实体类,新建类 News,代码如下所示:
/**
* 新闻实体类
* Created by KXwon on 2016/12/12.
*/
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_content_frag.xml 布局,作为新闻内容的布局:
<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"
android:visibility="invisible">
<TextView
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp"
android:padding="10dp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000"/>
<TextView
android:id="@+id/news_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:textSize="18sp"
android:padding="15dp"/>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:background="#000"/>
</RelativeLayout>
新闻内容的布局主要分为两个部分
头部显示新闻标题,正文显示新闻内容
中间使用一条细线分隔开
然后再新建一个 NewsContentFragment 类,如下:
/**
* 新闻内容fragment
* Created by KXwon on 2016/12/12.
*/
public class NewsContentFragment extends Fragment {
private View view;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
view = inflater.inflate(R.layout.news_content_frag, container, false);
return view;
}
/**
* 将新闻标题和新闻内容显示在界面上
* @param newsTitle 标题
* @param newsContent 内容
*/
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); // 刷新新闻的内容
}
}
这样就把新闻内容的碎片和布局创建好了
但它们都是在双页模式下使用的,若要在单页模式中使用
还需创建一个活动 NewsContentActivity,其布局 news_content.xml 中的代码如下:
<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:id="@+id/news_content_fragment"
android:name="com.wonderful.myfirstcode.chapter4.simple_news.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
这里直接在布局中引入了 NewsContentFragment
相当于把 news_content_frag 布局的内容自动加了进来
然后编写 NewsContentActivity 的代码,如下:
public class NewsContentActivity extends AppCompatActivity {
/**
* 构建Intent,传递所需数据
* @param context
* @param newsTitle
* @param newsContent
*/
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 = (NewsContentFragment) getSupportFragmentManager()
.findFragmentById(R.id.news_content_fragment);
// 刷新 NewsContentFragment 界面
newsContentFragment.refresh(newsTitle, newsContent);
}
}
在 onCreate() 方法中通过 Intent 获取传入的新闻标题和内容
然后调用 FragmentManager 的 findFragmentById() 方法得到 NewsContentFragment 的实例
接着调用它的 refresh() 方法,并将新闻的标题和内容传入,显示数据
(关于 actionStart() 方法可以阅读前面的探究活动2.5.2相关笔记。)
接下来还需再创建显示新闻列表的布局 news_title_frag.xml,如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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 子项的布局:
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textSize="18sp"
android:padding="10dp"/>
子项的布局就只有一个 TextView
新闻列表和子项布局都创建好了,接下来就需要一个用于展示新闻列表的地方
这里新建 NewsTitleFragment 作为展示新闻列表的碎片:
/**
* 新闻列表fragment
* Created by KXwon on 2016/12/12.
*/
public class NewsTitleFragment extends Fragment{
private boolean isTowPane;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.news_content_frag, container, false);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_layout)!= null){
// 可以找到 news_content_layout 布局时,为双页模式
isTowPane = true;
}else {
// 找不到 news_content_layout 布局时,为单页模式
isTowPane = false;
}
}
}
为实现上述 onActivityCreated() 方法中判断当前时双页还是单页模式
接下来在 NewsTitleFragemt 中新建一个内部类 NewsAdapter 来作为 RecyclerView 的适配器
如下:
public class NewsTitleFragment extends Fragment{
private boolean isTowPane;
. . .
class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
private List<News> mNewsList;
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;
}
@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 = 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(ViewHolder holder, int position) {
News news = mNewsList.get(position);
holder.newsTitleText.setText(news.getTitle());
}
@Override
public int getItemCount() {
return mNewsList.size();
}
}
需要注意的是,这里把适配器写成内部类是为了直接访问 NewsTitleFragment 的变量
比如:isTowPane
现在还剩最后一步收尾工作,就是向 RecyclerView 中填充数据了
修改 NewsTitleFragment 中的代码,如下所示:
public class NewsTitleFragment extends Fragment{
. . .
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, 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;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_layout) != null) {
// 可以找到news_content_layout布局时,为双页模式
isTwoPane = true;
} else {
// 找不到news_content_layout布局时,为单页模式
isTwoPane = false;
}
}
/**
* 初始化50条模拟新闻数据
* @return
*/
private List<News> getNews() {
List<News> newsList = new ArrayList<>();
for (int i = 1; i <= 50; i++) {
News news = new News();
news.setTitle("This is news title " + i);
news.setContent(getRandomLengthContent("新闻内容吼吼吼" + i + "!"));
newsList.add(news);
}
return newsList;
}
/**
* 随机生成不同长度的新闻内容
* @param content
* @return
*/
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();
}
. . .
}
到这里,所有的代码编写工作就完成了,运行程序,效果如下:
点击一条新闻,会启动一个新的活动来显示新闻内容:
推荐阅读
-
Android开发之使用150行代码实现滑动返回效果
-
Android开发之使用150行代码实现滑动返回效果
-
《第一行代码》第二章、探究活动
-
《第一行代码》读书笔记(六):CoordinatorLayout
-
Android碎片fragment实现静态加载的实例代码
-
温习Android基础知识——《第一行代码(第三版)》读书笔记 Chapter 14 高级技巧
-
《第一行代码 第2版》中新建Activity时的Backwards Compatibility 选项框,怎么不见了?
-
荐 「「第一行代码(第二版)」」学习笔记 5 碎片(连载中)
-
《第一行代码》第七章 数据持久化
-
《第一行代码》学习笔记:更强大的滚动控件——RecyclerView(增强版的 ListView)