Android学习记录——4.碎片

秒速五厘米 2023-05-30 09:11 134阅读 0赞

文章目录

  • 1.碎片是什么
  • 2.碎片的使用方式
    • 2.1 碎片的简单用法
    • 2.2 动态添加碎片
    • 2.3 在碎片中模拟返回栈
    • 2.4 碎片和活动之间进行通信
  • 3.碎片的生命周期
    • 3.1 碎片的状态和回调
    • 3.2 体验碎片的生命周期
  • 4.动态加载布局的技巧
    • 4.1 使用限定符
    • 4.2 使用最小宽度限定符
  • 5.碎片的最佳实践——简易版的新闻应用

1.碎片是什么

碎片(Fragment)是一种可以嵌入在活动当中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间,因而在平板上应用得非常广泛。虽然碎片对你来说应该是个全新的概念,但我相信你学习起来应该毫不费力,因为它和活动实在是太像了,同样都能包含布局,同样都有自己的生命周期。你甚至可以将碎片理解成一个迷你型的活动,虽然这个迷你型的活动有可能和普通的活动是一样大的。
那么究竟要如何使用碎片才能充分地利用平板屏幕的空间呢?想象我们正在开发一个新闻应用,其中一个界面使用RecyclerView展示了一组新闻的标题,当点击了其中一个标题时,就打开另一个界面显示新闻的详细内容。如果是在手机中设计,我们可以将新闻标题列表放在一个活动中,将新闻的详细内容放在另一个活动中,如图所示。
在这里插入图片描述
可是如果在平板上也这么设计,那么新闻标题列表将会被拉长至填充满整个平板的屏幕,而新闻的标题一般都不会太长,这样将会导致界面上有大量的空白区域,如图所示。
在这里插入图片描述
因此,更好的设计方案是将新闻标题列表界面和新闻详细内容界面分别放在两个碎片中,,然后在同一个活动里引入这两个碎片,这样就可以将屏幕空间充分地利用起来了,如图所示。
在这里插入图片描述

2.碎片的使用方式

介绍了这么多抽象的东西,也是时候学习一下碎片的具体用法了。你已经知道,碎片通常都 是在平板开发中使用的,因此我们首先要做的就是创建一个平板模拟器。创建模拟器的方法我们在之前已经学过了,创建完成后启动平板模拟器,如图所示:
在这里插入图片描述

2.1 碎片的简单用法

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

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:orientation="vertical"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent">
  7. <Button
  8. android:id="@+id/button"
  9. android:layout_width="wrap_content"
  10. android:layout_height="wrap_content"
  11. android:layout_gravity="center_horizontal"
  12. android:text="Button"/>
  13. </LinearLayout>

这个布局非常简单,只放置了一个按钮,并让它水平居中显示。然后新建右侧碎片布局 right_fragment.xml,代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:orientation="vertical"
  5. android:background="#00ff00"
  6. android:layout_width="match_parent"
  7. android:layout_height="match_parent">
  8. <TextView
  9. android:layout_width="wrap_content"
  10. android:layout_height="wrap_content"
  11. android:layout_gravity="center_horizontal"
  12. android:textSize="20sp"
  13. android:text="This is right fragment"/>
  14. </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库也一起引入进来。
现在编写一下LeftFragment中的代码,如下所示:

  1. package com.mxt.fragmenttest;
  2. import android.os.Bundle;
  3. import android.support.v4.app.Fragment;
  4. import android.view.LayoutInflater;
  5. import android.view.View;
  6. import android.view.ViewGroup;
  7. public class LeftFragment extends Fragment {
  8. @Override
  9. public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
  10. View view = inflater.inflate(R.layout.left_fragment,container,false);
  11. return view;
  12. }
  13. }

这里仅仅是重写了 Fragment的onCreateView()方法,然后在这个方法中通过Layoutlnflater的inflate()方法将刚才定义的left_fragment布局动态加载进来,整个方法简单明了。接着我们用同样的方法再新建一个RightFragment,代码如下所示:

  1. package com.mxt.fragmenttest;
  2. import android.app.Fragment;
  3. import android.os.Bundle;
  4. import android.view.LayoutInflater;
  5. import android.view.View;
  6. import android.view.ViewGroup;
  7. public class RightFragment extends Fragment {
  8. @Override
  9. public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
  10. View view = inflater.inflate(R.layout.right_fragment,container,false);
  11. return view;
  12. }
  13. }

基本上代码都是相同的,相信已经没有必要再做什么解释了。接下来修改activity_main.xml 中的代码,如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:orientation="horizontal"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. tools:context=".MainActivity">
  8. <fragment
  9. android:id="@+id/left_fragment"
  10. android:name="com.mxt.fragmenttest.LeftFragment"
  11. android:layout_width="0dp"
  12. android:layout_height="match_parent"
  13. android:layout_weight="1"/>
  14. <fragment
  15. android:id="@+id/right_fragment"
  16. android:name="com.mxt.fragmenttest.RightFragment"
  17. android:layout_width="0dp"
  18. android:layout_height="match_parent"
  19. android:layout_weight="1"/>
  20. </LinearLayout>

可以看到,我们使用了<fragment>标签在布局中添加碎片,其中指定的大多数属性都是你熟悉的,只不过这里还需要通过android:name属性来显式指明要添加的碎片类名,注意一定要将类的包名也加上。
这样最简单的碎片示例就已经写好了,现在运行一下程序,效果如图所示:
在这里插入图片描述
正如我们所期待的一样,两个碎片平分了整个活动的布局。不过这个例子实在是太简单了, 在真正的项目中很难有什么实际的作用,因此我们马上来看一看,关于碎片更加高级的使用技巧。

2.2 动态添加碎片

在上一节当中,你已经学会了在布局文件中添加碎片的方法,不过碎片真正的强大之处在于,它可以在程序运行时动态地添加到活动当中。根据具体情况来动态地添加碎片,你就可以将程序界面定制得更加多样化。
我们还是在上一节代码的基础上继续完善,新建another_right_fragment.xml,代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:orientation="vertical"
  5. android:background="#ffff00"
  6. android:layout_width="match_parent"
  7. android:layout_height="match_parent">
  8. <TextView
  9. android:layout_width="wrap_content"
  10. android:layout_height="wrap_content"
  11. android:layout_gravity="center_horizontal"
  12. android:textSize="20sp"
  13. android:text="This is another right fragment"/>
  14. </LinearLayout>

这个布局文件的代码和right_fragment.xml中的代码基本相同,只是将背景色改成了黄色,并 将显示的文字改了改。然后新建AnotherRightFragment作为另一个右侧碎片,代码如下所示:

  1. package com.mxt.fragmenttest;
  2. import android.os.Bundle;
  3. import android.support.v4.app.Fragment;
  4. import android.view.LayoutInflater;
  5. import android.view.View;
  6. import android.view.ViewGroup;
  7. public class AnotherRightFragment extends Fragment {
  8. @Override
  9. public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
  10. View view = inflater.inflate(R.layout.another_right_fragment,container,false);
  11. return view;
  12. }
  13. }

代码同样非常简单,在onCreateView()方法中加载了刚刚创建的another_right_firagment布局。这样我们就准备好了另一个碎片,接下来看一下如何将它动态地添加到活动当中。修改 activity_main.xml,代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:orientation="horizontal"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. tools:context=".MainActivity">
  8. <fragment
  9. android:id="@+id/left_fragment"
  10. android:name="com.mxt.fragmenttest.LeftFragment"
  11. android:layout_width="0dp"
  12. android:layout_height="match_parent"
  13. android:layout_weight="1"/>
  14. <FrameLayout
  15. android:id="@+id/right_layout"
  16. android:layout_width="0dp"
  17. android:layout_height="match_parent"
  18. android:layout_weight="1"/>
  19. </LinearLayout>

可以看到,现在将右侧碎片替换成了一个FrameLayout中,还记得这个布局吗?在上一篇博客中我们学过,这是Android中最简单的一种布局,所有的控件默认都会摆放在布局的左上角。由于这里仅需要在布局里放入一个碎片,不需要任何定位,因此非常适合使用FrameLayout。
下面我们将在代码中向FrameLayout里添加内容,从而实现动态添加碎片的功能。修改MainActivity中的代码,如下所示:

  1. package com.mxt.fragmenttest;
  2. import android.support.v4.app.Fragment;
  3. import android.support.v4.app.FragmentManager;
  4. import android.support.v4.app.FragmentTransaction;
  5. import android.support.v7.app.AppCompatActivity;
  6. import android.os.Bundle;
  7. import android.view.View;
  8. import android.widget.Button;
  9. public class MainActivity extends AppCompatActivity implements View.OnClickListener {
  10. @Override
  11. protected void onCreate(Bundle savedInstanceState) {
  12. super.onCreate(savedInstanceState);
  13. setContentView(R.layout.activity_main);
  14. Button button = (Button)findViewById(R.id.button);
  15. button.setOnClickListener(this);
  16. replceFragment(new RightFragment());
  17. }
  18. @Override
  19. public void onClick(View view) {
  20. switch (view.getId()){
  21. case R.id.button:
  22. replceFragment(new AnotherRightFragment());
  23. break;
  24. default:
  25. break;
  26. }
  27. }
  28. private void replceFragment(Fragment fragment){
  29. FragmentManager fragmentManager = getSupportFragmentManager();
  30. FragmentTransaction transaction = fragmentManager.beginTransaction();
  31. transaction.replace(R.id.right_layout,fragment);
  32. transaction.commit();
  33. }
  34. }

可以看到,首先我们给左侧碎片中的按钮注册了一个点击事件,然后调用 replaceFragment()方法动态添加了 RightFragment这个碎片。当点击左侧碎片中的按钮时,又会调用replaceFragment()方法将右侧碎片替换成AnotherRightFragment 。结 合 replaceFragment()方法中的代码可以看出,动态添加碎片主要分为5步:

  1. 创建待添加的碎片实例。
  2. 获取 FragmentManager,在活动中可以直接通过调用 getSupportFragmentManager()方 法得到。
  3. 开启一个事务,通过调用beginTransaction()方法开启。
  4. 向容器内添加或替换碎片,一般使用replace ()方法实现,需要传入容器的id和待添加 的碎片实例。
  5. 提交事务,调用commit ()方法来完成。

这样就完成了在活动中动态添加碎片的功能,重新运行程序,可以看到和之前相同的界面, 然后点击一下按钮,效果如图所示。

2.3 在碎片中模拟返回栈

在上一小节中,我们成功实现了向活动中动态添加碎片的功能,不过你尝试一下就会发现, 通过点击按钮添加了一个碎片之后,这时按下Back键程序就会直接退出。如果这里我们想模仿类似于返回栈的效果,按下Back键可以回到上一个碎片,该如何实现呢?
其实很简单,FragmentTransaction中提供了一个addToBackStack()方法,可以用于将一个事务添加到返回栈中,修改MainActivity中的代码,如下所示:

  1. package com.mxt.fragmenttest;
  2. import android.support.v4.app.Fragment;
  3. import android.support.v4.app.FragmentManager;
  4. import android.support.v4.app.FragmentTransaction;
  5. import android.support.v7.app.AppCompatActivity;
  6. import android.os.Bundle;
  7. import android.view.View;
  8. import android.widget.Button;
  9. public class MainActivity extends AppCompatActivity implements View.OnClickListener {
  10. @Override
  11. protected void onCreate(Bundle savedInstanceState) {
  12. super.onCreate(savedInstanceState);
  13. setContentView(R.layout.activity_main);
  14. Button button = (Button)findViewById(R.id.button);
  15. button.setOnClickListener(this);
  16. replceFragment(new RightFragment());
  17. }
  18. @Override
  19. public void onClick(View view) {
  20. switch (view.getId()){
  21. case R.id.button:
  22. replceFragment(new AnotherRightFragment());
  23. break;
  24. default:
  25. break;
  26. }
  27. }
  28. private void replceFragment(Fragment fragment){
  29. FragmentManager fragmentManager = getSupportFragmentManager();
  30. FragmentTransaction transaction = fragmentManager.beginTransaction();
  31. transaction.replace(R.id.right_layout,fragment);
  32. transaction.addToBackStack(null);
  33. transaction.commit();
  34. }
  35. }

这里我们在事务提交之前调用了 FragmentTransaction的addToBackStack()方法,它可以接 收一个名字用于描述返回栈的状态,一般传入null即可。现在重新运行程序,并点击按钮将 AnotherRightFragment添加到活动中,然后按下Back键,你会发现程序并没有退出,而是回到了 RightFragment界面,继续按下Back键,RightFragment界面也会消失,再次按下Back键,程序才会退出。

2.4 碎片和活动之间进行通信

虽然碎片都是嵌入在活动中显示的,可是实际上它们的关系并没有那么亲密。你可以看出, 碎片和活动都是各自存在于一个独立的类当中的,它们之间并没有那么明显的方式来直接进行通信。如果想要在活动中调用碎片里的方法,或者在碎片中调用活动里的方法,应该如何实现呢?
为了方便碎片和活动之间进行通信,FragmentManager提供了一个类似于findViewByld () 的方法,专门用于从布局文件中获取碎片的实例,代码如下所示:

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

调用FragmentManager的findFragmentByld()方法,可以在活动中得到相应碎片的实例, 然后就能轻松地调用碎片里的方法了。
掌握了如何在活动中调用碎片里的方法,那在碎片中又该怎样调用活动里的方法呢?其实这就更简单了,在每个碎片中都可以通过调用getActivity()方法来得到和当前碎片相关联的活动实例,代码如下所示:

  1. MainActivity activity = (MainActivity) getActivity();

有了活动实例之后,在碎片中调用活动里的方法就变得轻而易举了。另外当碎片中需要使用 Context对象时,也可以使用getActivity()方法,因为获取到的活动本身就是一个Context 对象。
这时不知道你心中会不会产生一个疑问:既然碎片和活动之间的通信问题已经解决了,那么碎片和碎片之间可不可以进行通信呢?
说实在的,这个问题并没有看上去那么复杂,它的基本思路非常简单,首先在一个碎片中可以得到与它相关联的活动,然后再通过这个活动去获取另外一个碎片的实例,这样也就实现了不同碎片之间的通信功能,因此这里我们的答案是肯定的。

3.碎片的生命周期

和活动一样,碎片也有自己的生命周期,并且它和活动的生命周期实在是太像了,我相信你很快就能学会,下面我们马上就来看一下。

3.1 碎片的状态和回调

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

  1. 运行状态
    当一个碎片是可见的,并且它所关联的活动正处于运行状态时,该碎片也处于运行状态。
  2. 暂停状态
    当一个活动进入暂停状态时(由于另一个未占满屏幕的活动被添加到了栈顶),与它相关联 的可见碎片就会进入到暂停状态。
  3. 停止状态
    当一个活动进入停止状态时,与它相关联的碎片就会进入到停止状态,或者通过调用 FragmentTransaction的remove() replace()方法将碎片从活动中移除,但如果在事务提交之 前调用addToBackStack()方法,这时的碎片也会进入到停止状态。总的来说,进入停止状态的 碎片对用户来说是完全不可见的,有可能会被系统回收。
  4. 销毁状态
    碎片总是依附于活动而存在的,因此当活动被销毁时,与它相关联的碎片就会进入到销毁状 态。或者通过调用FragmentTransaction的remove() replace()方法将碎片从活动中移除,但 在事务提交之前并没有调用addToBackStackO方法,这时的碎片也会进入到销毁状态。

结合之前的活动状态,相信你理解起来应该毫不费力吧。同样地,Fragment类中也提供了 一系列的回调方法,以覆盖碎片生命周期的每个环节。其中,活动中有的回调方法,碎片中几乎 都有,不过碎片还提供了一些附加的回调方法,那我们就重点看一下这几个回调。

  • onAttach()o当碎片和活动建立关联的时候调用。
  • onCreateView()0为碎片创建视图(加载布局)时调用。
  • onActivityCreated() o确保与碎片相关联的活动一定已经创建完毕的时候调用。
  • onDestroyViewOo当与碎片关联的视图被移除的时候调用。
  • onDetach()o当碎片和活动解除关联的时候调用。
    碎片完整的生命周期示意图可参考下图:
    在这里插入图片描述

3.2 体验碎片的生命周期

具体可参《考第一行代码》书中的内容,这里不再详述。
值得一提的是,在碎片中你也是可以通过onSaveInstanceState()方法来保存数据的, 因为进入停止状态的碎片有可能在系统内存不足的时候被回收。保存下来的数据在onCreate()、 onCreateView()和onActivityCreated()这3个方法中你都可以重新得到,它们都含有一个Bundle类型的savedlnstanceState参数。具体的代码我就不在这里给出了,如果你忘记了该如何编写,可以参考之前的博客。

4.动态加载布局的技巧

虽然动态添加碎片的功能很强大,可以解决很多实际开发中的问题,但是它毕竟只是在一个布局文件中进行一些添加和替换操作。如果程序能够根据设备的分辨率或屏幕大小在运行时来决定加载哪个布局,那我们可发挥的空间就更多了。因此本节我们就来探讨一下Android中动态加载布局的技巧。

4.1 使用限定符

如果你经常使用平板电脑,应该会发现现在很多的平板应用都采用的是双页模式(程序会在左侧的面板上显示一个包含子项的列表,在右侧的面板上显示内容),因为平板电脑的屏幕足够大,完全可以同时显示下两页的内容,但手机的屏幕一次就只能显示一页的内容,因此两个页面需要分开显示。
那么怎样才能在运行时判断程序应该是使用双页模式还是单页模式呢?这就需要借助限定符(Qualifiers )来实现了。下面我们通过一个例子来学习一下它的用法,修改FragmentTest项目中的activity_main.xml文件,代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:orientation="horizontal"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. tools:context=".MainActivity">
  8. <fragment
  9. android:id="@+id/left_fragment"
  10. android:name="com.mxt.fragmenttest.LeftFragment"
  11. android:layout_width="match_parent"
  12. android:layout_height="match_parent" />
  13. </LinearLayout>

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

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:orientation="horizontal"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent">
  7. <fragment
  8. android:id="@+id/left_fragment"
  9. android:name="com.mxt.fragmenttest.LeftFragment"
  10. android:layout_width="0dp"
  11. android:layout_height="match_parent"
  12. android:layout_weight="1"/>
  13. <fragment
  14. android:id="@+id/right_fragment"
  15. android:name="com.mxt.fragmenttest.RightFragment"
  16. android:layout_width="0dp"
  17. android:layout_height="match_parent"
  18. android:layout_weight="3"/>
  19. </LinearLayout>

可以看到,layout/activity_main布局只包含了一个碎片,即单页模式,而layout-large/activity_main布局包含了两个碎片,即双页模式。其中large就是一个限定符,那些屏幕被认为是large的设备就会自动加载layout-large文件夹下的布局,而小屏幕的设备则还是会加载layout 文件夹下的布局。
然后将MainActivity中replaceFragment ()方法里的代码注释掉,并在平板模拟器上重新运行程序,效果会跟在layout-large/activity_main定义的一样。
而在手机上运行,效果则会跟layout/activity_main一样
这样我们就实现了在程序运行时动态加载布局的功能。
Android中一些常见的限定符可以参考下表:
在这里插入图片描述

4.2 使用最小宽度限定符

在上一小节中我们使用large限定符成功解决了单页双页的判断问题,不过很快又有一个 新的问题出现了,large到底是指多大呢?有的时候我们希望可以更加灵活地为不同设备加载布局,不管它们是不是被系统认定为large,这时就可以使用最小宽度限定符(Smallest-width Qualifier) 了。
最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。
在res目录下新建layout-sw600dp文件夹,然后在这个文件夹下新建activity _main.xml布局, 代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:orientation="horizontal"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. tools:context=".MainActivity">
  8. <fragment
  9. android:id="@+id/left_fragment"
  10. android:name="com.mxt.fragmenttest.LeftFragment"
  11. android:layout_width="0dp"
  12. android:layout_height="match_parent"
  13. android:layout_weight="1"/>
  14. <fragment
  15. android:id="@+id/right_fragment"
  16. android:name="com.mxt.fragmenttest.RightFragment"
  17. android:layout_width="0dp"
  18. android:layout_height="match_parent"
  19. android:layout_weight="3"/>
  20. </LinearLayout>

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

5.碎片的最佳实践——简易版的新闻应用

现在你已经将关于碎片的重要知识点都掌握得差不多了,不过在灵活运用方面可能还有些欠缺,因此下面该进入我们本章的最佳实践环节了。
前面有提到过,碎片很多时候都是在平板开发当中使用的,主要是为了解决屏幕空间不能充分利用的问题。那是不是就表明,我们开发的程序都需要提供一个手机版和一个Pad版呢?确实有不少公司都是这么做的,但是这样会浪费很多的人力物力。因为维护两个版本的代码成本很高, 每当增加什么新功能时,需要在两份代码里各写一遍,每当发现一个bug时,需要在两份代码里各修改一次。因此今天我们最佳实践的内容就是,教你如何编写同时兼容手机和平板的应用程序。
还记得在本章开始的时候提到过的一个新闻应用吗?现在我们就将运用本章中所学的知识来编写一个简易版的新闻应用,并且要求它是可以同时兼容手机和平板的。
由于待会在编写新闻列表时会使用到RecyclerView,因此首先需要在app/build.gradle当中添加依赖库,如下所示:

  1. compile 'com.android.support:recyclerview-v7:24.2.1'

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

  1. package com.example.administrator.fragmentbestpractice;
  2. /** * Created by Administrator on 2018-09-25. */
  3. public class News {
  4. private String title;
  5. private String content;
  6. public String getTitle(){
  7. return title;
  8. }
  9. public void setTitle(String title){
  10. this.title = title;
  11. }
  12. public String getContent(){
  13. return content;
  14. }
  15. public void setContent(String content){
  16. this.content = content;
  17. }
  18. }

News类的代码还是比较简单的,title字段表示新闻标题,content字段表示新闻内容。 接着新建布局文件news_content_frag.xml,用于作为新闻内容的布局:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <LinearLayout
  6. android:id="@+id/visibility_layout"
  7. android:layout_width="match_parent"
  8. android:layout_height="match_parent"
  9. android:orientation="vertical"
  10. android:visibility="invisible">
  11. <TextView
  12. android:id="@+id/news_title"
  13. android:layout_width="match_parent"
  14. android:layout_height="wrap_content"
  15. android:gravity="center"
  16. android:padding="10dp"
  17. android:textSize="20sp"/>
  18. <View
  19. android:layout_width="match_parent"
  20. android:layout_height="1dp"
  21. android:background="#000"/>
  22. <TextView
  23. android:id="@+id/news_content"
  24. android:layout_width="match_parent"
  25. android:layout_height="0dp"
  26. android:layout_weight="1"
  27. android:padding="15dp"
  28. android:textSize="18sp"/>
  29. </LinearLayout>
  30. <View
  31. android:layout_width="1dp"
  32. android:layout_height="match_parent"
  33. android:layout_alignParentLeft="true"
  34. android:background="#000"/>
  35. </RelativeLayout>

新闻内容的布局主要可以分为两个部分,头部部分显示新闻标题,正文部分显示新闻内容, 中间使用一条细线分隔开。这里的细线是利用View来实现的,将View的宽或高设置为1dp,再 通过background属性给细线设置一下颜色就可以了。这里我们把细线设置成黑色。
然后再新建一个NewsContentFragment类,继承自Fragment,代码如下所示:

  1. package com.example.administrator.fragmentbestpractice;
  2. import android.os.Bundle;
  3. import android.support.v4.app.Fragment;
  4. import android.view.LayoutInflater;
  5. import android.view.View;
  6. import android.view.ViewGroup;
  7. import android.widget.TextView;
  8. /** * Created by Administrator on 2018-09-25. */
  9. public class NewsContentFragment extends Fragment {
  10. private View view;
  11. @Override
  12. public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  13. view = inflater.inflate(R.layout.news_content_frag,container,false);
  14. return view;
  15. }
  16. public void refresh(String newsTitle,String newsContent){
  17. View visibilityLayout = view.findViewById(R.id.visibility_layout);
  18. visibilityLayout.setVisibility(View.VISIBLE);
  19. TextView newsTitleText = (TextView)view.findViewById(R.id.news_title);
  20. TextView newsContentText = (TextView)view.findViewById(R.id.news_content);
  21. newsTitleText.setText(newsTitle);
  22. newsContentText.setText(newsContent);
  23. }
  24. }

首先在onCreateView()方法里加载了我们刚刚创建的news content frag布局,这个没什么 好解释的。接下来又提供了一个refresh()方法,这个方法就是用于将新闻的标题和内容显示在界面上的。可以看到,这里通过findViewByldO方法分别获取到新闻标题和内容的控件,然后 将方法传递进来的参数设置进去。
这样我们就把新闻内容的碎片和布局都创建好了,但是它们都是在双页模式中使用的,如果想在单页模式中使用的话,我们还需要再创建一个活动。右击com.example.fragmentbestpractice 包—New —>ActivityEmpty Activity ,新建一个 NewsContentActivity ,并将布局名指定成 news content,然后修改news content.xml中的代码,如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6. <fragment
  7. android:id="@+id/news_content_fragmanet"
  8. android:name="com.example.administrator.fragmentbestpractice.NewsContentFragment"
  9. android:layout_width="match_parent"
  10. android:layout_height="match_parent"/>
  11. </LinearLayout>

这里我们充分发挥了代码的复用性,直接在布局中引入了 NewsContentFragment,这样也 就相当于把news_content_frag布局的内容自动加了进来。
然后修改NewsContentActivity中的代码,如下所示:

  1. package com.example.administrator.fragmentbestpractice;
  2. import android.content.Context;
  3. import android.content.Intent;
  4. import android.support.v7.app.AppCompatActivity;
  5. import android.os.Bundle;
  6. public class NewsContentActivity extends AppCompatActivity {
  7. public static void actionStart(Context context,String newsTitle,String newscontent){
  8. Intent intent = new Intent(context,NewsContentActivity.class);
  9. intent.putExtra("news_title",newsTitle);
  10. intent.putExtra("new_content",newscontent);
  11. context.startActivity(intent);
  12. }
  13. @Override
  14. protected void onCreate(Bundle savedInstanceState) {
  15. super.onCreate(savedInstanceState);
  16. setContentView(R.layout.news_content);
  17. String newsTitle = getIntent().getStringExtra("news_title");
  18. String newsContent = getIntent().getStringExtra("new_content");
  19. NewsContentFragment newsContentFragment = (NewsContentFragment)getSupportFragmentManager().findFragmentById(R.id.news_content_fragmanet);
  20. newsContentFragment.refresh(newsTitle,newsContent);
  21. }
  22. }

可以看到,在onCreate()方法中我们通过Intent获取到了传入的新闻标题和新闻内容,然 后调用 FragmentManager 的 findFragmentById()方法得到了 NewsContentFragment 的实例, 接着调用它的refresh()方法,并将新闻的标题和内容传入,就可以把这些数据显示出来了。注 意这里我们还提供了一个actionStartO方法,还记得它的作用吗?如果忘记的话就再去看一遍之前的博客吧。
接下来还需要再创建一个用于显示新闻列表的布局,新建news_title frag.xml,代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6. <android.support.v7.widget.RecyclerView
  7. android:id="@+id/news_title_recycler_view"
  8. android:layout_width="match_parent"
  9. android:layout_height="match_parent"/>
  10. </LinearLayout>

这个布局的代码就非常简单了,里面只有一个用于显示新闻列表的RecyclerView。既然要用到RecyclerView,那么就必定少不了子项的布局。新建news_item.xml作为RecyclerView子项的布局,代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <TextView xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:id="@+id/news_title"
  4. android:layout_width="match_parent"
  5. android:layout_height="wrap_content"
  6. android:maxLines="1"
  7. android:ellipsize="end"
  8. android:textSize="18sp"
  9. android:paddingLeft="10dp"
  10. android:paddingRight="10dp"
  11. android:paddingTop="15dp"
  12. android:paddingBottom="15dp"/>

子项的布局也非常简单,只有一个TextVieW。仔细观察TextView,你会发现其中有几个属性是我们之前没有学过的。android:padding表示给控件的周围加上补白,这样不至于让文本内容会紧靠在边缘上。android: singleLine设置为true表示让这个TextView只能单行显示。 android:ellipsize用于设定当文本内容超出控件宽度时,文本的缩略方式,这里指定成end 表示在尾部进行缩略。
既然新闻列表和子项的布局都已经创建好了,那么接下来我们就需要一个用于展示新闻列表的地方。这里新建NewsTitleFragment作为展示新闻列表的碎片,代码如下所示:

  1. package com.example.administrator.fragmentbestpractice;
  2. import android.os.Bundle;
  3. import android.support.v4.app.Fragment;
  4. import android.support.v7.widget.LinearLayoutManager;
  5. import android.support.v7.widget.RecyclerView;
  6. import android.view.LayoutInflater;
  7. import android.view.View;
  8. import android.view.ViewGroup;
  9. import android.widget.TextView;
  10. import java.util.ArrayList;
  11. import java.util.List;
  12. import java.util.Random;
  13. /** * Created by Administrator on 2018-09-25. */
  14. public class NewsTitleFragment extends Fragment {
  15. private boolean isTwoPane;
  16. @Override
  17. public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState) {
  18. View view = inflater.inflate(R.layout.news_title_frag,container,false);
  19. return view;
  20. }
  21. @Override
  22. public void onActivityCreated(Bundle savedInstanceState) {
  23. super.onActivityCreated(savedInstanceState);
  24. if(getActivity().findViewById(R.id.news_content_layout)!=null){
  25. isTwoPane = true;
  26. }else {
  27. isTwoPane = false;
  28. }
  29. }
  30. }

可以看到,NewsTitleFragment中并没有多少代码,在onCreateView()方法中加载了news_title_frag布局,这个没什么好说的。我们注意看一下onActivityCreated()方法,这个方 法通过在活动中能否找到一个id为news _content_layout的View来判断当前是双页模式还是单页模式,因此我们需要让这个id为news_content_layout的View只在双页模式中才会出现。
那么怎样才能实现这个功能呢?其实并不复杂,只需要借助我们刚刚学过的限定符就可以了。首先修改activity_main.xml中的代码,如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:id="@+id/news_title_layout"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6. <fragment
  7. android:id="@+id/news_title_fragment"
  8. android:name="com.example.administrator.fragmentbestpractice.NewsTitleFragment"
  9. android:layout_width="match_parent"
  10. android:layout_height="match_parent"/>
  11. </LinearLayout>

上述代码表示,在单页模式下,只会加载一个新闻标题的碎片。
然后新建layout-sw600dp文件夹,在这个文件夹下再新建一个activity_main.xml文件,代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="horizontal"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6. <fragment
  7. android:id="@+id/news_title_fragment"
  8. android:name="com.example.administrator.fragmentbestpractice.NewsTitleFragment"
  9. android:layout_width="0dp"
  10. android:layout_height="match_parent"
  11. android:layout_weight="1"/>
  12. <FrameLayout
  13. android:id="@+id/news_content_layout"
  14. android:layout_width="0dp"
  15. android:layout_height="match_parent"
  16. android:layout_weight="3">
  17. <fragment
  18. android:id="@+id/news_content_fragmanet"
  19. android:name="com.example.administrator.fragmentbestpractice.NewsContentFragment"
  20. android:layout_width="match_parent"
  21. android:layout_height="match_parent"/>
  22. </FrameLayout>
  23. </LinearLayout>

可以看出,在双页模式下我们同时引入了两个碎片,并将新闻内容的碎片放在了一个Frame- Layout布局下,而这个布局的id正是news_content_layout。因此,能够找到这个id的时候就是 双页模式,否则就是单面模式。
现在我们已经将绝大部分的工作都完成了,但还剩下至关重要的一点,就是在NewsTitle- Fragment中通过RecyclerView将新闻列表展示出来。我们在NewsTitleFragment中新建一个内部类NewsAdapter来作为RecyclerView的适配器,如下所示:

  1. package com.example.administrator.fragmentbestpractice;
  2. import android.os.Bundle;
  3. import android.support.v4.app.Fragment;
  4. import android.support.v7.widget.LinearLayoutManager;
  5. import android.support.v7.widget.RecyclerView;
  6. import android.view.LayoutInflater;
  7. import android.view.View;
  8. import android.view.ViewGroup;
  9. import android.widget.TextView;
  10. import java.util.ArrayList;
  11. import java.util.List;
  12. import java.util.Random;
  13. /** * Created by Administrator on 2018-09-25. */
  14. public class NewsTitleFragment extends Fragment {
  15. private boolean isTwoPane;
  16. @Override
  17. public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState) {
  18. View view = inflater.inflate(R.layout.news_title_frag,container,false);
  19. RecyclerView newsTitleRecyclerView = (RecyclerView)view.findViewById(R.id.news_title_recycler_view);
  20. LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
  21. newsTitleRecyclerView.setLayoutManager(layoutManager);
  22. NewsAdapter adapter = new NewsAdapter(getNews());
  23. newsTitleRecyclerView.setAdapter(adapter);
  24. return view;
  25. }
  26. private List<News> getNews(){
  27. List<News> newsList = new ArrayList<>();
  28. for (int i = 1;i <= 50;i++){
  29. News news = new News();
  30. news.setTitle("这是新闻标题" + i);
  31. news.setContent(getRandomLengthContent("这是新闻内容" + i + "."));
  32. newsList.add(news);
  33. }
  34. return newsList;
  35. }
  36. private String getRandomLengthContent(String content){
  37. Random random = new Random();
  38. int length = random.nextInt(20) + 1;
  39. StringBuilder builder = new StringBuilder();
  40. for (int i = 0;i < length; i++){
  41. builder.append(content);
  42. }
  43. return builder.toString();
  44. }
  45. @Override
  46. public void onActivityCreated(Bundle savedInstanceState) {
  47. super.onActivityCreated(savedInstanceState);
  48. if(getActivity().findViewById(R.id.news_content_layout)!=null){
  49. isTwoPane = true;
  50. }else {
  51. isTwoPane = false;
  52. }
  53. }
  54. class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder>{
  55. private List<News> mNewList;
  56. class ViewHolder extends RecyclerView.ViewHolder{
  57. TextView newsTitleText;
  58. public ViewHolder(View view){
  59. super(view);
  60. newsTitleText = (TextView)view.findViewById(R.id.news_title);
  61. }
  62. }
  63. public NewsAdapter(List<News> newsList){
  64. mNewList = newsList;
  65. }
  66. @Override
  67. public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType){
  68. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_item,parent,false);
  69. final ViewHolder holder = new ViewHolder(view);
  70. view.setOnClickListener(new View.OnClickListener(){
  71. @Override
  72. public void onClick(View v){
  73. News news = mNewList.get(holder.getAdapterPosition());
  74. if(isTwoPane){
  75. NewsContentFragment newsContentFragment = (NewsContentFragment) getFragmentManager().findFragmentById(R.id.news_content_fragmanet);
  76. newsContentFragment.refresh(news.getTitle(),news.getContent());
  77. } else{
  78. NewsContentActivity.actionStart(getActivity(),news.getTitle(),news.getContent());
  79. }
  80. }
  81. });
  82. return holder;
  83. }
  84. @Override
  85. public void onBindViewHolder(ViewHolder holder,int position){
  86. News news = mNewList.get(position);
  87. holder.newsTitleText.setText(news.getTitle());
  88. }
  89. @Override
  90. public int getItemCount(){
  91. return mNewList.size();
  92. }
  93. }
  94. }

RecyclerView的用法你已经相当熟练了,因此这个适配器的代码对你来说应该没有什么难度吧?需要注意的是,之前我们都是将适配器写成一个独立的类,其实也是可以写成内部类的, 这里写成内部类的好处就是可以直接访问NewsTitleFragment的变量,比如isTwoPane。
观察一下onCreateViewHolder()方法中注册的点击事件,首先获取到了点击项的News实例,然后通过isTwoPane变量来判断当前是单页还是双页模式,如果是单页模式,就启动一个新的活动去显示新闻内容,如果是双页模式,就更新新闻内容碎片里的数据。
现在还剩最后一步收尾工作,就是向RecyclerView中填充数据了。修改NewsTitleFragment中的代码,如下所示:

  1. package com.example.administrator.fragmentbestpractice;
  2. import android.os.Bundle;
  3. import android.support.v4.app.Fragment;
  4. import android.support.v7.widget.LinearLayoutManager;
  5. import android.support.v7.widget.RecyclerView;
  6. import android.view.LayoutInflater;
  7. import android.view.View;
  8. import android.view.ViewGroup;
  9. import android.widget.TextView;
  10. import java.util.ArrayList;
  11. import java.util.List;
  12. import java.util.Random;
  13. /** * Created by Administrator on 2018-09-25. */
  14. public class NewsTitleFragment extends Fragment {
  15. private boolean isTwoPane;
  16. @Override
  17. public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState) {
  18. View view = inflater.inflate(R.layout.news_title_frag,container,false);
  19. RecyclerView newsTitleRecyclerView = (RecyclerView)view.findViewById(R.id.news_title_recycler_view);
  20. LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
  21. newsTitleRecyclerView.setLayoutManager(layoutManager);
  22. NewsAdapter adapter = new NewsAdapter(getNews());
  23. newsTitleRecyclerView.setAdapter(adapter);
  24. return view;
  25. }
  26. private List<News> getNews(){
  27. List<News> newsList = new ArrayList<>();
  28. for (int i = 1;i <= 50;i++){
  29. News news = new News();
  30. news.setTitle("这是新闻标题" + i);
  31. news.setContent(getRandomLengthContent("这是新闻内容" + i + "."));
  32. newsList.add(news);
  33. }
  34. return newsList;
  35. }
  36. private String getRandomLengthContent(String content){
  37. Random random = new Random();
  38. int length = random.nextInt(20) + 1;
  39. StringBuilder builder = new StringBuilder();
  40. for (int i = 0;i < length; i++){
  41. builder.append(content);
  42. }
  43. return builder.toString();
  44. }
  45. @Override
  46. public void onActivityCreated(Bundle savedInstanceState) {
  47. super.onActivityCreated(savedInstanceState);
  48. if(getActivity().findViewById(R.id.news_content_layout)!=null){
  49. isTwoPane = true;
  50. }else {
  51. isTwoPane = false;
  52. }
  53. }
  54. class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder>{
  55. private List<News> mNewList;
  56. class ViewHolder extends RecyclerView.ViewHolder{
  57. TextView newsTitleText;
  58. public ViewHolder(View view){
  59. super(view);
  60. newsTitleText = (TextView)view.findViewById(R.id.news_title);
  61. }
  62. }
  63. public NewsAdapter(List<News> newsList){
  64. mNewList = newsList;
  65. }
  66. @Override
  67. public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType){
  68. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_item,parent,false);
  69. final ViewHolder holder = new ViewHolder(view);
  70. view.setOnClickListener(new View.OnClickListener(){
  71. @Override
  72. public void onClick(View v){
  73. News news = mNewList.get(holder.getAdapterPosition());
  74. if(isTwoPane){
  75. NewsContentFragment newsContentFragment = (NewsContentFragment) getFragmentManager().findFragmentById(R.id.news_content_fragmanet);
  76. newsContentFragment.refresh(news.getTitle(),news.getContent());
  77. } else{
  78. NewsContentActivity.actionStart(getActivity(),news.getTitle(),news.getContent());
  79. }
  80. }
  81. });
  82. return holder;
  83. }
  84. @Override
  85. public void onBindViewHolder(ViewHolder holder,int position){
  86. News news = mNewList.get(position);
  87. holder.newsTitleText.setText(news.getTitle());
  88. }
  89. @Override
  90. public int getItemCount(){
  91. return mNewList.size();
  92. }
  93. }
  94. }

可以看到,onCreateView()方法中添加了 RecyclerView标准的使用方法,在碎片中使用 RecyclerView和在活动中使用几乎是一模一样的,相信没有什么需要解释的。另外,这里调用 了 getNews()方法来初始化50条模拟新闻数据,同样使用了一个getRandomLengthContent()方法来随机生成新闻内容的长度,以保证每条新闻的内容差距比较大,相信你对这个方法肯定不会陌生了。
这样我们所有的编写工作就已经完成了,赶快来运行一下吧!
在这里插入图片描述
在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,134人围观)

还没有评论,来说两句吧...

相关阅读