Android开发基础4 - 探究碎片

本篇文章主要介绍以下几个知识点:

  • 碎片 fragment 的用法;
  • 碎片 fragment 的生命周期;
  • 动态加载布局的技巧,限定符的使用;
  • 实战:简易版的新闻应用

4.1 碎片是什么

  碎片(Fragment)是一种可以嵌入在活动当中的 UI 片段,它能让程序更加合理和充分 地利用大屏幕的空间,因而在平板上应用的非常广泛。

  那么究竟要如何使用碎片才能充分地利用平板屏幕的空间呢?想象我们正在开发一个新闻应用,其中一个界面使用 RecyclerView 展示了一组新闻的标题,当点击了其中一个标题,就打开另一个界面显示新闻的详细内容。若是在手机中设计,我们可以将新闻标题列表放在 一个活动中,将新闻的详细内容放在另一个活动中,如图所示:

但显示在平板上,那么新闻标题列表将会被拉长至填充满整个平板的屏幕,而新闻的标题一般都不会太长,这样将会导致界面上有大量的空白区域,如图所示:

因此,更好的设计方案是将新闻标题列表界面和新闻详细内容界面分别放在两个碎片中,然后在同一个活动里引入这两个碎片,这样就可以将屏幕空间充分地利用起来了,如图所示:

4.2 碎片的使用方式

4.2.1 碎片的简单用法

  这里我们准备先写一个最简单的碎片示例来练练手,在一个活动当中添加两个碎片,并让这两个碎片平分活动空间。
新建一个左侧碎片布局 fragment_left.xml,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 用于显示一段文本。

  接着新建一个 LeftFragment 类,继承自 Fragment。注意,这里可能会有两个不同包 下的 Fragment 供你选择,一个是系统内置的android.app.Fragment,一个是 support-v4 库中的 android.support.v4.app.Fragment 。这里强烈建议使用 support-v4 库中的 Fragment,因为它可以让碎片在所有 Android 系统版本中保持功能一致性。

  LeftFragment 的代码如下所示:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
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 中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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>

  可以看到,我们使用了标签在布局中添加碎片,通过 android:name 属性来显式指明要添加的碎片类名,注意一 定要将类的包名也加上。

  这样最简单的碎片示例就已经写好了,运行一下程序,(平板上)效果如图:

4.2.2 动态添加碎片

  碎片真正的强大之处在于,它可以在程序运行时动态地添加到活动当中。根据具体情况来动态地添加碎片,你就 可以将程序界面定制得更加多样化。

  在上一节代码的基础上继续完善,新建 fragment_another_right.xml,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 作为另一个右侧碎片,代码如下所示:

1
2
3
4
5
6
7
8
9
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,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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 中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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() 的方法,专门用于从布局文件中获取碎片的实例。在活动中调用碎片里的方法:

1
RightFragment rightFragment = (RightFragment) getFragmentManager().findFragmentById(R.id.right_fragment);

  在碎片中调用活动里的方法:通过调用 getActivity()方法来得到和当前碎片相关联的活动实例,代码如下所示:

1
MainActivity activity = (MainActivity) getActivity();

4.3 碎片的生命周期

  每个活动在其生命周期内可能会有四种状态:运行状态、暂停 状态、停止状态和销毁状态。类似地,每个碎片在其生命周期内也可能会经历这几种状态,只不过在一些细小的地方会有部分区别。

运行状态:
  当一个碎片是可见的,并且它所关联的活动正处于运行状态时,该碎片也处于运行状态。

暂停状态:
  当一个活动进入暂停状态时(由于另一个未占满屏幕的活动被添加到了栈顶),与它相关联的可见碎片就会进入到暂停状态。

停止状态:
  当一个活动进入停止状态时,与它相关联的碎片就会进入到停止状态。或者通过调用 FragmentTransaction 的 remove()、replace()方法将碎片从活动中移除,但若在事务提交之前调用 addToBackStack() 方法,这时的碎片也会进入到停止状态。总的来说,进入停止状态的碎片对用户来说是完全不可见的,有可能会被系统回收。

销毁状态:
  碎片总是依附于活动而存在的,因此当活动被销毁时,与它相关联的碎片就会进入到销毁状态。或者通过调用 FragmentTransaction 的 remove()、replace()方法将碎片从活动中移除,但在事务提交之前并没有调用 addToBackStack()方法,这时的碎片也会进入 到销毁状态。

  和活动 Acitvity 相似,Fragment 类中也提供了一系列的回调方法,以覆盖碎片生命周期的每个环节。其中,活动中有的回调方法,碎片中几乎都有,不过碎片还提供了一些附加的回调方法,重点来看下这几个回调:

  • onAttach() 当碎片和活动建立关联的时候调用。
  • onCreateView() 为碎片创建视图(加载布局)时调用。
  • onActivityCreated() 确保与碎片相关联的活动一定已经创建完毕的时候调用。
  • onDestroyView() 当与碎片关联的视图被移除的时候调用。
  • onDetach() 当碎片和活动解除关联的时候调用。

  碎片完整的生命周期可参考源自 Android 官网图的示意图:

  另外,在碎片中你也可以通过 onSaveInstanceState()方法来保存数据, 因为进入停止状态的碎片有可能在系统内存不足的时候被回收。保存下来的数据在 onCreate()、onCreateView() 和 onActivityCreated()这三个方法中你都可以重新得到,它们都含有一个 Bundle 类型的 savedInstanceState 参数。

4.4 动态加载布局的技巧

4.4.1 使用限定符

  现有个需求:在平板上使用双页模式,在手机上显示单页模式。那么怎样才能在运行时判断程序应该是使用双页模式还是单页模式呢?这就需要借助限定符(Qualifiers)来实现了。

  我们通过一个例子来学习一下它的用法,修改项目中 activity_fragment.xml 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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="match_parent"
android:layout_height="match_parent" />

</LinearLayout>

  这里将多余的代码都删掉,只留下一个左侧碎片,并让它充满整个父布局。接着在 res 目录下新建 layout-large 文件夹,在这个文件夹下新建一个布局,也叫做 activity_fragment.xml, 代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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="3"/>

</LinearLayout>

  可以看到,layout/activity_fragment 布局只包含了一个碎片,即单页模式,而 layout-large/activity_fragment 布局包含了两个碎片,即双页模式。其中 large 就是一个限定符,那些屏幕被认 为是 large 的设备就会自动加载 layout-large 文件夹下的布局,而小屏幕的设备则还是会加载 layout 文件夹下的布局。

  然后将 Activity 中 replaceFragment() 方法注释掉,并在平板模拟器上重新运行程序, 效果如图:

  再启动一个手机模拟器,并在这个模拟器上重新运行程序,效果如图:

  Android 中一些常见的限定符可以参考下表:

4.4.2 使用最小宽度限定符

  有时候我们希望可以更加灵活地为不同设备加载布局,不管它们是不是被系统 认定为 “ large ”,这时就可以使用最小宽度限定符(Smallest-width Qualifier)了。

  最小宽度限定符允许我们对屏幕的宽度指定一个最小指(以 dp 为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。

  在 res 目录下新建 layout-sw600dp 文件夹,然后在这个文件夹下新建 activity_fragment .xml布局,代码与上面 layout-large/activity_fragment 中的一样。

1
...

  这就意味着,当程序运行在屏幕宽度大于 600dp 的设备上时,会加载 layout-sw600dp/ activity_fragment 布局,当程序运行在屏幕宽度小于 600dp 的设备上时,则仍然加载默认的 layout/activity_fragment 布局。

4.5 碎片的最佳实践——一个简易版的新闻应用

  需求:一个简易的新闻应用,可以同时兼容手机和平板。

  首先,在 app/build.gradle 中添加后面需要用到的 RecyclerView 依赖库:

1
2
3
4
5
6
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:recyclerview-v7:24.2.1'
testCompile 'junit:junit:4.12'
}

  接下来,准备好一个新闻的实体类,新建类 News,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 新闻实体类
* 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 布局,作为新闻内容的布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<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 类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 新闻内容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 中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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 的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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,如下:

1
2
3
4
5
6
7
8
9
10
11
<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 子项的布局:

1
2
3
4
5
6
7
8
9
<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 作为展示新闻列表的碎片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 新闻列表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() 方法中判断当前时双页还是单页模式,需要借助限定符,首先修改主布局 activity_news.xml 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
<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:id="@+id/news_title_fragment"
android:name="com.wonderful.myfirstcode.chapter4.simple_news.NewsTitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</FrameLayout>

  上述代码表示,在单页模式下只会加载一个新闻标题的碎片。

  然后在 res 目录下新建 layout-sw600dp 文件夹,在这个文件夹下再新建一个 activity_news.xml 文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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/news_title_fragment"
android:name="com.wonderful.myfirstcode.chapter4.simple_news.NewsTitleFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />

<FrameLayout
android:id="@+id/news_content_layout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" >

<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" />
</FrameLayout>

</LinearLayout>

  上述代码表示,在双页模式下会同时加载两个碎片,并将新闻内容碎片放在 FrameLayout 布局下,这个布局 id 正是 news_content_layout。因此能找到这个 id 的时候就是双页模式,否则就是单页模式。

  现在已经将绝大多数工作完成了,剩下至关重要的一点,就是在 NewsTitleFragemt 中通过 RecyclerView 将新闻列表展示出来。接下来在 NewsTitleFragemt 中新建一个内部类 NewsAdapter 来作为 RecyclerView 的适配器,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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();
}

. . .
}

  到这里,所有的代码编写工作就完成了,运行程序,效果如下:

  点击一条新闻,会启动一个新的活动来显示新闻内容:

  接下来把程序在平板上运行,同样点击一条新闻,效果如下:

  好了,关于碎片的内容学习到这,下篇文章将学习广播机制…

Jason Xu wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!